create-pmos 0.525.0 → 0.527.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 +51 -56
- 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/briefing-premium-board-test.js +1 -1
- package/workspace/changelog.json +50 -0
- package/workspace/ev-everywhere-test.js +2 -2
- package/workspace/guide/agent-capabilities.md +28 -2
- package/workspace/guide/board.md +14 -1
- package/workspace/guide/claude-proposes.md +3 -1
- package/workspace/guide/home.md +11 -5
- package/workspace/guide/idle-lab.md +2 -2
- package/workspace/guide/lifecycle.md +16 -3
- package/workspace/guide/reports-set-up.md +1 -0
- package/workspace/guide/today.md +11 -3
- package/workspace/lib/bg-queue.js +7 -3
- package/workspace/lib/board-premium.js +4 -1
- package/workspace/lib/briefing-premium-agent.js +9 -2
- package/workspace/lib/engine/boards.js +3 -2
- package/workspace/lib/headless.js +19 -4
- package/workspace/lib/idle-lab-engine.js +531 -0
- package/workspace/lib/initiative-chat.js +12 -2
- package/workspace/lib/initiative-cli.js +1 -1
- package/workspace/lib/initiative-premium.js +11 -3
- package/workspace/lib/lab-cli.js +220 -0
- package/workspace/lib/lifecycle-stage.js +17 -3
- package/workspace/lib/one-stage.js +131 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proposals.js +5 -2
- package/workspace/lib/report-chat.js +31 -4
- package/workspace/lib/report-draft.js +1 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-board-premium.js +22 -0
- package/workspace/lib/routes-boards.js +61 -12
- package/workspace/lib/routes-briefing-premium.js +18 -1
- package/workspace/lib/routes-notes.js +1 -1
- package/workspace/lib/routes-your-move.js +64 -0
- package/workspace/lib/status-signal.js +123 -0
- package/workspace/lib/workflow-cli.js +3 -1
- package/workspace/lib/your-move.js +61 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-acts.js +2 -1
- package/workspace/public/pm/bd-cols.js +8 -5
- package/workspace/public/pm/bd-prog-panel.js +3 -2
- package/workspace/public/pm/bd-span.js +2 -1
- package/workspace/public/pm/bd-views.js +8 -6
- package/workspace/public/pm/cozy-home.css +3 -0
- package/workspace/public/pm/cozy-today.css +151 -0
- package/workspace/public/pm/cozy.css +9 -0
- package/workspace/public/pm/good-morning.js +327 -0
- package/workspace/public/pm/grill-words.js +16 -1
- package/workspace/public/pm/home-app.js +187 -18
- package/workspace/public/pm/home-chat.js +6 -2
- package/workspace/public/pm/home-data.js +45 -13
- package/workspace/public/pm/home-day.js +66 -14
- package/workspace/public/pm/home-ini-model.js +1 -1
- package/workspace/public/pm/home-stage-card.js +7 -0
- package/workspace/public/pm/ini-tasks.js +2 -2
- package/workspace/public/pm/ini-view-model.js +10 -3
- package/workspace/public/pm/ini-view.css +9 -0
- package/workspace/public/pm/ini-view.js +19 -5
- package/workspace/public/pm/move-rank.js +155 -0
- package/workspace/public/pm/premium-board.js +7 -4
- package/workspace/public/pm/premium-initiative-head.js +39 -4
- package/workspace/public/pm/premium-report-tab.js +15 -3
- package/workspace/public/pm/report-flow.js +7 -1
- package/workspace/public/pm/stage-chip.js +44 -0
- package/workspace/run-tests.js +1 -0
- package/workspace/server.js +43 -1
- package/workspace/skills/idle-lab-prototype.skill +36 -0
- package/workspace/skills/idle-lab-scan.skill +32 -0
- package/workspace/test-lists/good-morning.json +9 -0
- package/workspace/test-lists/idle-lab-engine.json +9 -0
- package/workspace/test-lists/one-stage.json +5 -0
- package/workspace/test-lists/status-signal.json +5 -0
- package/workspace/test-lists/your-move.json +9 -0
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* costWord(usd) '38¢' · '$1.20' · '' (unknown)
|
|
9
9
|
* whyWord(run) 'before it leaves its stage' · 'when its forecast turned' · 'after its scope or dates moved' · 'in the monthly look'
|
|
10
10
|
* runLine(run, now) "Claude grilled it overnight, when its forecast turned" (no cost; the page adds it quietly)
|
|
11
|
-
* turnWord(to) 'will miss' · 'is at risk' · ''
|
|
11
|
+
* turnWord(to) 'will miss' · 'is at risk' · ''
|
|
12
|
+
* headerFlags(props) the initiative header's grill findings (workspace grill W3, WgW3Move): { n, rows: [{ title, label, day, hot }] } | null.
|
|
13
|
+
* props is a Board row's r.props (lib/proposals.js viewOf); the two best by rank, the folded ones counted too */
|
|
12
14
|
export const ANGLE = Object.freeze({
|
|
13
15
|
premortem: { label: 'Premortem', icon: 'M12 3l9 16H3zM12 10v4M12 17v.5' },
|
|
14
16
|
redteam: { label: 'Red team', icon: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8' },
|
|
@@ -53,3 +55,16 @@ export function runLine(r, now = Date.now()) {
|
|
|
53
55
|
return ['Claude grilled it', whenWord(r.at, now)].filter(Boolean).join(' ') + (whyWord(r) ? ', ' + whyWord(r) : '');
|
|
54
56
|
}
|
|
55
57
|
export const turnWord = (to) => (to === 'will-miss' ? 'will miss' : to === 'at-risk' ? 'is at risk' : '');
|
|
58
|
+
|
|
59
|
+
const MONS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
60
|
+
export function headerFlags(props) {
|
|
61
|
+
const g = ((props && props.groups) || []).find((x) => x.group === 'grill');
|
|
62
|
+
const all = g ? (g.props || []).concat(g.hidden || []) : [];
|
|
63
|
+
if (!all.length) return null;
|
|
64
|
+
const rank = (p) => (p && p.data && p.data.rank) || 5;
|
|
65
|
+
const rows = all.slice().sort((a, b) => rank(a) - rank(b)).slice(0, 2).map((p) => {
|
|
66
|
+
const f = findingOf(p), t = Date.parse(p.at || ''), d = Number.isFinite(t) ? new Date(t) : null;
|
|
67
|
+
return { title: f.title, label: f.label, day: d ? d.getDate() + ' ' + MONS[d.getMonth()] : '', hot: rank(p) <= 2 };
|
|
68
|
+
});
|
|
69
|
+
return { n: all.length, rows };
|
|
70
|
+
}
|
|
@@ -29,7 +29,7 @@ import * as H from './home-data.js';
|
|
|
29
29
|
import { mountChat, avHtml } from './home-chat.js';
|
|
30
30
|
import * as MORE from './home-more.js';
|
|
31
31
|
import * as PP from './cozy-pick.js'; // the one product picker Home, Board and Insights share (batch 12 item 5)
|
|
32
|
-
import { headModel, STEPS } from './premium-initiative-head.js';
|
|
32
|
+
import { headModel, STEPS, gateOf } from './premium-initiative-head.js';
|
|
33
33
|
import * as SC from './home-stage-card.js'; // the initiative's stage card D
|
|
34
34
|
import * as WFM from './ini-wf-model.js'; // D99: the workflow an initiative runs (kind line, usual days)
|
|
35
35
|
import { wfIcon, wfOf } from './wf-model.js';
|
|
@@ -45,6 +45,8 @@ import * as L from './home-list.js';
|
|
|
45
45
|
import * as TD from './home-today.js';
|
|
46
46
|
import * as GW from './grill-words.js'; // Home's one line when Claude grilled something by itself (canvas FcPhone, Home)
|
|
47
47
|
import * as YD from './home-day.js';
|
|
48
|
+
import * as GM from './good-morning.js'; // Good morning is one page: the quarter, the reading column and the time rail (nyt-ui GM2)
|
|
49
|
+
import * as MSG from './ms-suggest.js'; // Your move's ✓ on a program's milestone changes confirms them all (workspace grill W3)
|
|
48
50
|
import * as AD from './cozy-adler.js'; // Adler, Claude's face (batch 16 item 2)
|
|
49
51
|
import * as OKL from './okr-list.js'; // the product page's Goals line (batch 16 item 6, GsProduct)
|
|
50
52
|
import { canDictate, dictate } from './home-dictate.js';
|
|
@@ -363,13 +365,155 @@ function homeListHtml() {
|
|
|
363
365
|
* minutes (the event 'cz:news-for-you') and again when Your day opens; a row opens the item in the Viewer, "N more" the whole list, ✓✓
|
|
364
366
|
* marks all seen. An item leaves once it is opened or marked seen. */
|
|
365
367
|
function dayHtml() { return ''; }
|
|
368
|
+
/** Your move's ranked list (workspace grill W3, pm/move-rank.js): the products Home reads with their Board rows, the programs, and each
|
|
369
|
+
* product's GET /api/your-move answer (overdue verdicts, the first key result with no reading). */
|
|
370
|
+
function rankIn() {
|
|
371
|
+
const products = ((S.M && S.M.products) || []).filter((p) => p.model).map((p) => {
|
|
372
|
+
const I = p.model.initiatives || {};
|
|
373
|
+
return { ws: p.ws || boardWs() || '', name: p.name, board: (p.model.board && p.model.board.id) || '', rows: (I.list && I.list.rows) || p.model.rows || [] };
|
|
374
|
+
});
|
|
375
|
+
return { products, programs: S.D.programs || [], move: S.move || {} };
|
|
376
|
+
}
|
|
377
|
+
/** Each product's GET /api/your-move, read when Your day opens (never on Home's first draw: page speed). */
|
|
378
|
+
async function readMove() {
|
|
379
|
+
const L = ((S.M && S.M.products) || []).filter((p) => p.model).map((p) => p.ws || boardWs() || '');
|
|
380
|
+
const got = await Promise.all(L.map((ws) => pmGet('/api/your-move' + (ws ? '?ws=' + encodeURIComponent(ws) : '')).catch(() => null)));
|
|
381
|
+
const next = {};
|
|
382
|
+
L.forEach((ws, i) => { const r = got[i]; if (r && r.ok) next[ws] = { verdicts: r.verdicts || [], kr: r.kr || null }; });
|
|
383
|
+
const same = JSON.stringify(next) === JSON.stringify(S.move || null);
|
|
384
|
+
S.move = next;
|
|
385
|
+
if (!same) redrawLater();
|
|
386
|
+
}
|
|
387
|
+
/** Redraw Your day after a late read, but never under something passing on it (Adler's dress tip, the pull's "Reading your day again"):
|
|
388
|
+
* it waits until that is gone. */
|
|
389
|
+
const PASSING = '.ad-tip, .cz-pull';
|
|
390
|
+
function redrawLater() {
|
|
391
|
+
const body = VW.body();
|
|
392
|
+
if (!body || !body.querySelector(PASSING)) { redrawDay(); return; }
|
|
393
|
+
const mo = new MutationObserver(() => { if (!body.querySelector(PASSING)) { mo.disconnect(); redrawDay(); } });
|
|
394
|
+
mo.observe(body, { childList: true, subtree: true });
|
|
395
|
+
}
|
|
396
|
+
const moveRow = (key) => { const Y = S.Y && S.Y.rank; return Y ? Y.rows.concat(Y.older).find((r) => r.key === key) || null : null; };
|
|
397
|
+
const insightsHref = (ws) => '/premium-intelligence.html' + (ws ? '?ws=' + encodeURIComponent(ws) : '');
|
|
398
|
+
/** The ✦ of one initiative of any product Home reads, over its page (pm/ini-props.js). */
|
|
399
|
+
async function openProps(ws, board, id) {
|
|
400
|
+
await goIni(id);
|
|
401
|
+
const r = await pmGet('/api/proposals?ws=' + encodeURIComponent(ws || '') + '&board=' + encodeURIComponent(board || '') + '&initiative=' + encodeURIComponent(id) + '&grill=all').catch(() => null);
|
|
402
|
+
if (!r || r.ok === false) { toast((r && r.error) || 'What Claude proposes could not be read.'); return; }
|
|
403
|
+
const row = iniRowOf(id) || {};
|
|
404
|
+
const IP = await import('./ini-props.js');
|
|
405
|
+
IP.open({ id, title: row.title || id, props: r.props }, { where: () => ({ ws, board }), reload: () => load(), toast: (m, undo) => (undo ? toastUndo(m, undo) : toast(m)) });
|
|
406
|
+
}
|
|
407
|
+
/** A press on a ranked row of Your move: mv-open opens what it is about, mv-yes does what its ✓ says, mv-alt picks, decides or drops. */
|
|
408
|
+
async function movePress(b) {
|
|
409
|
+
const r = moveRow(b.dataset.key); if (!r || b.disabled) return;
|
|
410
|
+
const act = b.dataset.yd;
|
|
411
|
+
const prog = r.program ? (S.D.programs || []).find((p) => p.id === r.program) : null;
|
|
412
|
+
if (act === 'mv-open' || act === 'mv-alt' && (r.kind === 'other' || r.kind === 'kr' || r.kind === 'verdict' || r.kind === 'meeting')) {
|
|
413
|
+
if (r.kind === 'verdict' || r.kind === 'kr') { location.href = insightsHref(r.ws); return; }
|
|
414
|
+
if (r.kind === 'meeting') { if (prog) { VW.drill(); goProgram(prog.id); } else if (r.props[0]) { VW.drill(); openProps(r.props[0].ws, r.props[0].board, r.props[0].ini); } return; }
|
|
415
|
+
if (r.kind === 'call') { select(r.call.id); return; }
|
|
416
|
+
VW.drill(); openProps(r.ws, r.board, r.ini); return;
|
|
417
|
+
}
|
|
418
|
+
b.disabled = true;
|
|
419
|
+
const done = (msg, undo) => { if (undo) toastUndo(msg, undo); else toast(msg); load().then(() => readMove()); };
|
|
420
|
+
if (act === 'mv-alt' && r.kind === 'grill') {
|
|
421
|
+
// ✕ Not this: each part is dropped for good (Claude does not propose it again on the same evidence)
|
|
422
|
+
let ok = true;
|
|
423
|
+
for (const x of r.props) for (const sig of x.sigs) { const res = await pmPost('/api/proposals/drop', { ws: x.ws, board: x.board, initiative: x.ini, sig }).catch(() => null); if (!res || !res.ok) ok = false; }
|
|
424
|
+
if (!ok) { b.disabled = false; toast('It could not be dropped. Nothing changed.'); return; }
|
|
425
|
+
done('Dropped. Claude will not raise it again on the same evidence.'); return;
|
|
426
|
+
}
|
|
427
|
+
if (act !== 'mv-yes') { b.disabled = false; return; }
|
|
428
|
+
if (r.kind === 'verdict' && r.verdict) {
|
|
429
|
+
const v = r.verdict;
|
|
430
|
+
const res = await pmPost('/api/insights-records/verdict', { ws: r.ws, board: v.board, ini: v.ini, verdict: v.verdict, learned: v.text, goal: null, drafted: true }).catch(() => null);
|
|
431
|
+
if (!res || res.ok !== true) { b.disabled = false; toast((res && res.error) || 'The verdict could not be saved. Nothing changed.'); return; }
|
|
432
|
+
done('Recorded: ' + v.verdict + '.', async () => { const u = await pmPost('/api/insights-records/verdict-undo', { ws: r.ws, board: v.board, ini: v.ini }).catch(() => null); load().then(() => readMove()); return u || { ok: false }; });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// ✓ keeps every part: the program's milestone changes, then the proposals per initiative (one batch each, one Undo for all)
|
|
436
|
+
const batches = []; let failed = 0;
|
|
437
|
+
if (r.ms && prog) for (const key of r.ms.keys) { const a = await MSG.answer(pmPost, prog, key, 'keep'); if (!a.ok) failed++; }
|
|
438
|
+
for (const x of r.props) {
|
|
439
|
+
const res = await pmPost('/api/proposals/keep', { ws: x.ws, board: x.board, initiative: x.ini, ids: x.ids }).catch(() => null);
|
|
440
|
+
if (res && res.ok) { if (res.batch) batches.push({ ws: x.ws, board: x.board, batch: res.batch }); } else failed++;
|
|
441
|
+
}
|
|
442
|
+
if (failed && !batches.length && !(r.ms && r.ms.keys.length > failed)) { b.disabled = false; toast('It could not be kept. Nothing changed.'); return; }
|
|
443
|
+
const undo = batches.length ? async () => { let ok = true; for (const x of batches) { const u = await pmPost('/api/proposals/undo', x).catch(() => null); if (!u || !u.ok) ok = false; } load().then(() => readMove()); return { ok }; } : null;
|
|
444
|
+
done(failed ? 'Kept, but ' + failed + (failed === 1 ? ' part was' : ' parts were') + ' not saved.' : 'Kept.', undo);
|
|
445
|
+
}
|
|
446
|
+
/** The notes Home's recorder holds (GET /api/notes rows), for Good morning's rail. */
|
|
447
|
+
const noteRows = () => (REC && REC.notes ? REC.notes() || [] : []);
|
|
366
448
|
function dayParts() {
|
|
367
|
-
const Y = YD.dayModel(S.M, S.D, L.reviewModel(S.D, NOW()), NOW(), { delightOk: !!S.joyOk, news: S.news, sources: S.src && S.src.sources, ask: MEN.dayAskOf(S.D), dress: AD.dress() });
|
|
368
|
-
|
|
449
|
+
const Y = YD.dayModel(S.M, S.D, L.reviewModel(S.D, NOW()), NOW(), { delightOk: !!S.joyOk, news: S.news, sources: S.src && S.src.sources, ask: MEN.dayAskOf(S.D), dress: AD.dress(), rank: rankIn() });
|
|
450
|
+
// Good morning (GM2): the goal data (the quarter, the PM's line, the morning snapshot, a goal tag per row) is GM1's, in S.gmGoals once it reads
|
|
451
|
+
const G = GM.gmModel(Y, S.D, NOW(), { quarter: S.gmGoals && S.gmGoals.quarter, tags: S.gmGoals && S.gmGoals.tags, notes: noteRows(), picks: S.notePicks || {}, today: H.todayOf(S.D) });
|
|
452
|
+
S.G = G;
|
|
453
|
+
return { Y, P: YD.gmParts(Y, G, { moment: !!(isPhone() && PHC), now: NOW(), first: !!S.first, html: GM.gmHtml }) };
|
|
454
|
+
}
|
|
455
|
+
/** The quarter under Good morning (GET /api/morning-goals, GM1): goal states, the PM's line or Claude's ✦ line, the move since the
|
|
456
|
+
* morning, and a goal tag per initiative. Read when Your day opens (never on Home's first draw: page speed). No answer, no strip. */
|
|
457
|
+
async function readGoals() {
|
|
458
|
+
const r = await pmGet('/api/morning-goals').catch(() => null);
|
|
459
|
+
const next = GM.fromMorningGoals(r), was = JSON.stringify(S.gmGoals || null);
|
|
460
|
+
S.gmGoals = next;
|
|
461
|
+
// redraw only when the page would change: a redraw closes what is open on the page (a tip, a fold the PM is reading)
|
|
462
|
+
const shows = !!next && (!!GM.quarterOf(next.quarter, 'morning', NOW()) || Object.keys(next.tags).length > 0);
|
|
463
|
+
if (shows && JSON.stringify(next) !== was) redrawLater();
|
|
464
|
+
}
|
|
465
|
+
/** The ✦ link of each loose recording in Good morning's rail: GET /api/notes/initiatives finds the initiative it shares the most words with.
|
|
466
|
+
* A note with no such initiative gets no ✦ (proven only). */
|
|
467
|
+
async function readNotePicks() {
|
|
468
|
+
const L = GM.notesOf(noteRows(), NOW(), {}).filter((n) => n.loose);
|
|
469
|
+
const ws = deskWs(); if (!L.length || !ws) return;
|
|
470
|
+
const got = await Promise.all(L.map((n) => pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id)).catch(() => null)));
|
|
471
|
+
const next = {};
|
|
472
|
+
L.forEach((n, i) => { const r = got[i]; if (r && r.ok && r.pick && r.pick.id) next[n.id] = { id: r.pick.id, title: r.pick.title || r.pick.id }; });
|
|
473
|
+
S.notePicks = next;
|
|
474
|
+
if (Object.keys(next).length) redrawLater();
|
|
475
|
+
}
|
|
476
|
+
/** A press on Good morning's own parts: By product, a quiet "N drafted for you" line, a meeting, Record, a note, All notes, ✦ link. */
|
|
477
|
+
async function gmPress(b) {
|
|
478
|
+
const act = b.dataset.yd, body = b.closest('.yd');
|
|
479
|
+
if (act === 'gm-intel') { location.href = insightsHref(deskWs()); return; }
|
|
480
|
+
if (act === 'gm-fold') {
|
|
481
|
+
const sec = body && body.querySelector('.czs-sec[data-czs-id="' + b.dataset.fold + '"]'); if (!sec) return;
|
|
482
|
+
const f = sec.querySelector('[data-czs-fold]'); if (f && f.getAttribute('aria-expanded') !== 'true') f.click();
|
|
483
|
+
sec.scrollIntoView({ behavior: 'smooth', block: 'start' }); return;
|
|
484
|
+
}
|
|
485
|
+
const meetOf = (id) => H.todayOf(S.D).concat((S.D.phoneDay && S.D.phoneDay.tomorrow && S.D.phoneDay.tomorrow.meetings) || []).find((m) => m && m.id === id) || null;
|
|
486
|
+
if (act === 'gm-meet' || act === 'gm-rec') { const m = meetOf(b.dataset.id); if (m) { VW.drill(); openMeeting(m, act === 'gm-rec' ? { record: true } : {}); } return; }
|
|
487
|
+
if (act === 'gm-note') { VW.drill(); openMeeting({ note: b.dataset.id }); return; }
|
|
488
|
+
if (act === 'gm-notes') { const v = await V(); if (typeof v.openNotes === 'function') { VW.drill(); v.openNotes({ ws: deskWs() }); } return; }
|
|
489
|
+
if (act === 'gm-line' || act === 'gm-line-off') {
|
|
490
|
+
// ✓ keeps Claude's line for the quarter; ✕ puts the ✦ off for the quarter. Both are the PM's own press (page press)
|
|
491
|
+
if (b.disabled) return; b.disabled = true;
|
|
492
|
+
const r = await pmPost('/api/morning-goals/' + (act === 'gm-line' ? 'line' : 'line-off'), act === 'gm-line' ? { line: +b.dataset.n } : {}).catch(() => null);
|
|
493
|
+
if (!r || !r.ok) { b.disabled = false; toast((r && r.error) || 'The line was not kept. Nothing changed.'); return; }
|
|
494
|
+
toast(act === 'gm-line' ? 'Your line for the quarter is ' + b.dataset.n + '.' : 'No line this quarter. I won’t ask again.');
|
|
495
|
+
await readGoals(); redrawDay(); return;
|
|
496
|
+
}
|
|
497
|
+
if (act === 'gm-link') {
|
|
498
|
+
const pk = (S.notePicks || {})[b.dataset.id]; if (!pk || b.disabled) return;
|
|
499
|
+
b.disabled = true;
|
|
500
|
+
const r = await pmPost('/api/notes/link', { id: b.dataset.id, initiative: { id: pk.id, ws: deskWs() || null, title: pk.title } }).catch(() => null);
|
|
501
|
+
if (!r || !r.ok) { b.disabled = false; toast((r && r.error) || 'The note could not be linked. Nothing changed.'); return; }
|
|
502
|
+
toast('Linked to ' + pk.title + '.');
|
|
503
|
+
delete S.notePicks[b.dataset.id];
|
|
504
|
+
if (REC) await REC.refresh();
|
|
505
|
+
redrawDay();
|
|
506
|
+
}
|
|
369
507
|
}
|
|
370
508
|
function drawDayBody(body, P) {
|
|
509
|
+
S.dayP = JSON.stringify([P, S.chap || null]);
|
|
371
510
|
const ph = isPhone() && PHC;
|
|
511
|
+
// the reads that land after the first draw (New for you, Your move, the quarter) redraw the page: an open dress tip stays open
|
|
512
|
+
// (and so does Adler peeking up while a pull reads the day again)
|
|
513
|
+
const tip = body.querySelector('.ad-tip'), pull = body.querySelector(':scope > .cz-pull');
|
|
372
514
|
YD.drawDay(body, P, VW, ph ? (el) => { el.append(PHC.el); PHC.read(true); } : null);
|
|
515
|
+
const at = tip && body.querySelector('[data-ad-dress]'); if (at) at.insertAdjacentElement('afterend', tip);
|
|
516
|
+
if (pull) body.prepend(pull);
|
|
373
517
|
// Your impact's chapter (batch 16 item 7, GmP4Chapter): on a Friday when a stage of one of these initiatives really closed this week
|
|
374
518
|
if (S.chap) body.insertAdjacentHTML('afterbegin', IM.chapterHtml(S.chap, { href: '/premium.html?ini=' + encodeURIComponent(S.chap.c.id) + (S.chap.c.ws ? '&ws=' + encodeURIComponent(S.chap.c.ws) : '') }));
|
|
375
519
|
}
|
|
@@ -414,6 +558,8 @@ function day(on) {
|
|
|
414
558
|
if (b.dataset.hmSel) { if (b.dataset.hmSel === 'n:update' && !rowById('n:update')) openUpdateViewer(); else select(b.dataset.hmSel); return; }
|
|
415
559
|
if (b.dataset.yd === 'fin') { const d = (dayParts().Y.drafts || [])[+b.dataset.i]; if (d && d.fin) { VW.drill(); openFinished(d.fin); } return; }
|
|
416
560
|
if (b.dataset.yd === 'sel') { const c = b.closest('.yd-call'); if (c) select(c.dataset.id); return; }
|
|
561
|
+
if (/^mv-/.test(b.dataset.yd || '')) { movePress(b); return; }
|
|
562
|
+
if (/^gm-/.test(b.dataset.yd || '')) { gmPress(b); return; }
|
|
417
563
|
if (b.dataset.yd === 'retry' || b.dataset.yd === 'prep') { dayAct(b); return; }
|
|
418
564
|
if (b.dataset.yd === 'sync') { syncAgain(b); return; }
|
|
419
565
|
if (b.dataset.yd === 'ask') { S.day = false; S.sel = 'mentions'; drawList(); MEN.openMentions(menCtx(), { sel: b.dataset.id }); return; }
|
|
@@ -422,13 +568,20 @@ function day(on) {
|
|
|
422
568
|
if ('hmReview' in b.dataset) { openReviewVw(); return; }
|
|
423
569
|
};
|
|
424
570
|
} });
|
|
571
|
+
// Good morning is a page: on a wide desk it opens wide (the reading column and the time rail side by side)
|
|
572
|
+
if (!isPhone() && typeof innerWidth === 'number' && innerWidth >= 1280) VW.wide(true);
|
|
425
573
|
readNews();
|
|
574
|
+
readMove();
|
|
575
|
+
readNotePicks();
|
|
576
|
+
readGoals();
|
|
426
577
|
}
|
|
427
578
|
/** Pull to refresh on Your day (AdP2): Home reads again, the place menu reads New for you and the sources again, then the day redraws. */
|
|
428
579
|
async function rereadDay() {
|
|
429
580
|
await load();
|
|
430
581
|
document.dispatchEvent(new CustomEvent('cz:news'));
|
|
431
582
|
await readNews();
|
|
583
|
+
await readMove();
|
|
584
|
+
await readGoals();
|
|
432
585
|
redrawDay();
|
|
433
586
|
}
|
|
434
587
|
/** The sources' last reads (GET /api/sources), passed on by the place menu and by Settings: a failed one shows on Your day (AdP5). */
|
|
@@ -452,7 +605,10 @@ async function syncAgain(b) {
|
|
|
452
605
|
/** Your day drawn again in place (a new read of New for you, a seen item), when it is the item shown. */
|
|
453
606
|
function redrawDay() {
|
|
454
607
|
const c = VW.current(), body = VW.body(); if (!S.day || !c || c.id !== 'day' || !body || !S.M) return;
|
|
455
|
-
const { Y, P } = dayParts(); S.Y = Y;
|
|
608
|
+
const { Y, P } = dayParts(); S.Y = Y;
|
|
609
|
+
// a read that changed nothing draws nothing: a redraw closes what is open on the page (the dress tip, a fold being read)
|
|
610
|
+
if (S.dayP === JSON.stringify([P, S.chap || null]) && body.querySelector(".yd")) return;
|
|
611
|
+
drawDayBody(body, P);
|
|
456
612
|
}
|
|
457
613
|
async function readNews() {
|
|
458
614
|
const r = await pmGet('/api/news-for-you' + wsQ()).catch(() => null);
|
|
@@ -513,7 +669,9 @@ function iniViewOf(id) {
|
|
|
513
669
|
const B = S.D.board || {}, t = S.D.trail && S.D.trail.items && S.D.trail.items[id];
|
|
514
670
|
const need = H.needsOf(S.D).find((r) => r.ini === id);
|
|
515
671
|
const p = programOf(S.D.programs || [], row.ws || (B.board || {}).ws || '', id);
|
|
672
|
+
const IM = S.IM && S.IMid === id ? S.IM : null; // W1: the page model's gate line (the spec ladder), when the page model is read
|
|
516
673
|
return iniView(rowFor({ ...row, trail: t || row.trail || null }), { steps: B.steps || STEPS, today: B.today, usual: WFM.usualOf(S.D.trail, row),
|
|
674
|
+
gate: IM && !(row.wf && row.wf.id && row.wf.id !== 'product-feature') ? gateOf(IM.gateSteps || IM.checks) : null,
|
|
517
675
|
ask: need ? { title: need.name, n: H.needsOf(S.D).filter((r) => r.ini === id).length, propose: (need.row && need.row.propose) || null } : null, program: p ? { id: p.id, title: p.title } : null, jiraReadAt: ((B.jira || {}).readAt) || '',
|
|
518
676
|
// the card (batch 18 item 1): Claude's line while a session works on it, the stakeholders
|
|
519
677
|
claude: (() => { const w = S.D.sessions.find((s) => H.iniOfSession(s) === id && s.shown === 'working'); return w ? 'I’m working on “' + H.nameOf(w) + '”. I’ll say here when it is ready.' : ''; })(),
|
|
@@ -548,7 +706,7 @@ function otherIni(id) {
|
|
|
548
706
|
function headOf(id) {
|
|
549
707
|
const row = iniRowOf(id) || {};
|
|
550
708
|
const M = S.IM && S.IMid === id ? { ...S.IM } : { head: { stage: row.stage }, acts: row.acts || {} };
|
|
551
|
-
Object.assign(M, { wf: row.wf || null, wfStage: row.wfStage || null, wfStages: row.wfStages || null }); // D99: the initiative's own workflow
|
|
709
|
+
Object.assign(M, { wf: row.wf || null, wfStage: row.wfStage || null, wfStages: row.wfStages || null, oneStage: row.oneStage || M.oneStage || null }); // W1: the one stage // D99: the initiative's own workflow
|
|
552
710
|
const t = S.D.trail && S.D.trail.items && S.D.trail.items[id];
|
|
553
711
|
const u = WFM.usualOf(S.D.trail, { ...row, stage: (M.head && M.head.stage) || row.stage }); // "usually N days" within its own workflow
|
|
554
712
|
if (t && t.since) M.stageSince = t.since;
|
|
@@ -564,7 +722,7 @@ function headOf(id) {
|
|
|
564
722
|
* next, ! for a risk, J for Jira. Its tag: You; Working (a plain word), or Stuck (an outlined pill) when the initiative is stalled; Next. */
|
|
565
723
|
const markHtml = (k, role) => (k === 'done' ? `<span class="hm-mk done" aria-hidden="true">${SVG(I.check, 12, 3)}</span>` : k === 'risk' ? '<span class="hm-mk risk" aria-hidden="true">!</span>' : k === 'jira' ? '<span class="hm-mk jira" aria-hidden="true">J</span>'
|
|
566
724
|
: k === 'agent' && role ? avHtml(role, 20, 'hm-mk-av') : `<span class="hm-mk ${esc(k)}" aria-hidden="true"></span>`);
|
|
567
|
-
const TAG = { you: 'You', agent: 'Working', wait: 'Next', jira: 'Jira', risk: 'At risk' };
|
|
725
|
+
const TAG = { you: 'You', agent: 'Working', wait: 'Next', jira: 'Jira', risk: 'At risk', none: 'No signal' };
|
|
568
726
|
const itemHtml = (x, o = {}) => {
|
|
569
727
|
const k = x.kind === 'agent' && !o.role && !o.stalled ? 'wait' : x.kind; // an agent step with no agent at work on it waits: it is next
|
|
570
728
|
const tag = k === 'agent' && o.stalled ? 'Stuck' : TAG[k];
|
|
@@ -595,7 +753,7 @@ function cardHtml(id, o = {}) {
|
|
|
595
753
|
const C = H.productCard(S.D, S.D.now);
|
|
596
754
|
kind = C.kind; title = C.name; turn = C.turn === 'you' ? 'you' : 'agents'; turnLabel = C.turn === 'you' ? 'Your turn' : 'Sent this week';
|
|
597
755
|
stage = C.stage; ckh = [C.title, C.of];
|
|
598
|
-
items = C.items.map((x) => itemHtml({ kind: x.kind === 'ok' ? 'done' : 'risk', label: x.label })).join('') || '<p class="hm-none">No KR or budget is anchored on this board.</p>';
|
|
756
|
+
items = C.items.map((x) => itemHtml({ kind: x.kind === 'ok' ? 'done' : x.kind === 'none' ? 'none' : 'risk', label: x.label })).join('') || '<p class="hm-none">No KR or budget is anchored on this board.</p>';
|
|
599
757
|
then = C.payoff; time = C.time; cta = C.cta; primary = !C.sent;
|
|
600
758
|
const risk = C.items.filter((x) => x.kind === 'risk').length;
|
|
601
759
|
foldSum = C.of + (risk ? ' · ' + risk + ' at risk' : '');
|
|
@@ -916,13 +1074,14 @@ function needTarget(r) {
|
|
|
916
1074
|
function updateTarget() {
|
|
917
1075
|
const U = H.weeklyUpdate(S.D, S.D.now);
|
|
918
1076
|
const flags = U.sections.flatMap(([, , L]) => L).filter((x) => x.flag);
|
|
919
|
-
const text = 'The weekly update is drafted from the
|
|
1077
|
+
const text = (U.draft ? 'The weekly update is drafted from what PMOS read (Jira, the plan\'s dates, the risks you confirmed), not from memory.' : 'Nothing moved and no risk is open this week, so there is no update to send.') + (flags.length ? ' One thing to check before it goes out: ' + flags[0].title + ' looks green, but ' + flags[0].flag.replace(/^Looks green, but /, '').toLowerCase() + ', so I marked it.' : '');
|
|
920
1078
|
const sent = (U.sent || []).find((x) => x.week === U.week.iso);
|
|
921
1079
|
// the product's conversation shows its In: scope chip (AgProduct); To: is for a running role session only (AgChatRunning)
|
|
922
1080
|
return { key: H.UPDATE_KEY, ini: 'product', ws: WS || undefined, title: 'Weekly update', sub: 'Communicator · ' + (sent ? 'sent this week' : 'ready to send'), scope: scopeOf('product'), rich: rich(), role: COMMS, to: null,
|
|
923
1081
|
intro: { role: COMMS, text: sent ? 'This week\'s update went out from your mail on ' + new Date(sent.at).toDateString() + '.' : text,
|
|
924
1082
|
pills: sent ? [{ label: 'Read it', primary: true, go: () => openUpdateViewer() }] : [{ label: 'Send now', primary: true, go: () => sendUpdate() }, { label: 'Edit', go: () => { openUpdateViewer(); chat.focus(); } }] },
|
|
925
|
-
context: '
|
|
1083
|
+
context: (U.unconfirmed.length ? 'Read these first: Claude proposed ' + U.unconfirmed.length + ' risks the PM has not confirmed (they are not in the mail): ' + U.unconfirmed.map((x) => x.title + ', ' + x.why).join('; ') + '.\n' : '')
|
|
1084
|
+
+ 'The weekly update draft for ' + H.productName(S.D) + ':\n' + H.updateText(U, H.productName(S.D)) + '\nRecipients: ' + (U.to.join(', ') || 'none yet') + '. You can change the draft; the PM sends it from their own mail.',
|
|
926
1085
|
placeholder: 'Tell Communicator what to change', also: null };
|
|
927
1086
|
}
|
|
928
1087
|
/** The product's own conversation (its heading's name, HbCProduct): Claude with the product's goals as its context. */
|
|
@@ -1013,7 +1172,7 @@ function productPageHtml(x, ws, vid, name, g) {
|
|
|
1013
1172
|
const U = own ? H.weeklyUpdate(S.D, S.D.now) : null, sent = U && (U.sent || []).some((y) => y.week === U.week.iso);
|
|
1014
1173
|
const R = d ? DB.reportsHtml(d, { open: x.open }) : '';
|
|
1015
1174
|
// redesign R (RgR4Make): "Ready to send" only in a week with news (something at risk or waiting for a decision); a quiet week says so
|
|
1016
|
-
const news = U && U.
|
|
1175
|
+
const news = !!(U && U.draft); // workspace grill W2: something moved or a risk is open
|
|
1017
1176
|
const extra = own ? (R ? '' : row('data-hp="update"', '', 'Weekly update', sent ? 'Sent this week' : news ? 'Ready to send' : 'Nothing new this week · skipped'))
|
|
1018
1177
|
: `<a class="pv-row" href="${esc(ownHome)}" target="_blank" rel="noopener"><span class="pv-rt"><b>Weekly update</b><small>In ${esc(name)}'s own Home, in a new tab</small></span></a>`;
|
|
1019
1178
|
const reps = ((d && d.reports) || []).filter((r) => r.kind !== 'heads-up');
|
|
@@ -1489,15 +1648,16 @@ function openUpdateViewer(o = {}) {
|
|
|
1489
1648
|
const draw = (b) => {
|
|
1490
1649
|
const U = H.weeklyUpdate(S.D, S.D.now);
|
|
1491
1650
|
const sent = (U.sent || []).find((x) => x.week === U.week.iso);
|
|
1492
|
-
|
|
1493
|
-
const words = [ok ? ok + ' on track' : '', risk ? risk + ' at risk' : '', dec ? dec + (dec === 1 ? ' needs' : ' need') + ' a decision' : ''].filter(Boolean);
|
|
1651
|
+
// workspace grill W2: the words come from the sections (H.updateWords); a week with nothing moved and no open risk has no update to send
|
|
1494
1652
|
const flag = U.sections.flatMap((x) => x[2]).find((x) => x.flag);
|
|
1495
|
-
const text = (
|
|
1496
|
-
const sum = VW.czSummary({ line: sent ? 'Sent ' + new Date(sent.at).toDateString().slice(0, 10) : 'Ready to send', tone: sent ? '' : 'you', text,
|
|
1497
|
-
meta: 'Communicator · ' + (U.to.length ? 'to ' + U.to.join(', ') : 'say who gets it under More'), primary: { label: sent ? 'Send again' : 'Send now', act: 'send' } });
|
|
1498
|
-
const it = (x) => `<button type="button" class="czs-it" data-hv-ini="${esc(x.id)}"><span class="w">${esc(x.title)}<span>${esc([x.meta, x.text].filter(Boolean).join(' · '))}</span>${x.flag ? `<span class="flag">${esc(x.flag)}</span>` : ''}</span></button>`;
|
|
1499
|
-
|
|
1500
|
-
|
|
1653
|
+
const text = (U.draft || sent ? H.updateWords(U) : 'Nothing moved and no risk is open this week, so there is no update to send.') + (flag ? ' ' + flag.title + ': ' + flag.flag.toLowerCase() + '.' : '');
|
|
1654
|
+
const sum = VW.czSummary({ line: sent ? 'Sent ' + new Date(sent.at).toDateString().slice(0, 10) : U.draft ? 'Ready to send' : 'Nothing new this week', tone: sent || !U.draft ? '' : 'you', text,
|
|
1655
|
+
meta: 'Communicator · ' + (U.to.length ? 'to ' + U.to.join(', ') : 'say who gets it under More'), primary: U.draft || sent ? { label: sent ? 'Send again' : 'Send now', act: 'send' } : null });
|
|
1656
|
+
const it = (x) => x.key ? `<div class="czs-it"><span class="w">${esc(x.title)}<span>${esc([x.meta, x.text].filter(Boolean).join(' · '))}</span></span></div>` : `<button type="button" class="czs-it" data-hv-ini="${esc(x.id)}"><span class="w">${esc(x.title)}<span>${esc([x.meta, x.text].filter(Boolean).join(' · '))}</span>${x.flag ? `<span class="flag">${esc(x.flag)}</span>` : ''}</span></button>`;
|
|
1657
|
+
// "Before you send": Claude's risks the PM has not confirmed, with one press that confirms them all (each a ✓ Keep on its program's charter)
|
|
1658
|
+
const confirmAll = (L) => { const say = L.length === 1 ? 'Confirm this risk' : 'Confirm all ' + L.length + ' risks'; return `<p><button type="button" class="hv-chip" data-hv-confirm aria-label="${say}" title="${say}">${SVG(I.check, 15, 2.4)}</button></p>`; };
|
|
1659
|
+
const secs = VW.czSections(U.sections.filter(([k, , L]) => L.length || k !== 'none').map(([k, h, L]) => ({ id: k, title: h, count: L.length, open: k !== 'ok' && L.length > 0, html: L.map(it).join('') + (k === 'confirm' && L.length ? confirmAll(L) : '') }))
|
|
1660
|
+
.concat([{ id: 'src', title: 'Sources', count: 'Jira, plan dates, confirmed risks · ' + H.hm(new Date(S.D.now).toISOString()), html: '<p>Drafted by Communicator from what PMOS read: the Jira tickets, the dates of the plan items and the risks you confirmed. An initiative nothing was read for is under Not read, never under On track.</p>' }]), { key: 'weekly' });
|
|
1501
1661
|
const to = edit ? `<div class="hv-to"><span>To</span>${U.to.map((x, i) => `<button type="button" class="hv-chip" data-hv-rm="${i}" aria-label="Remove ${esc(x)}" title="Remove">${esc(x)}</button>`).join('')}<input id="hvTo" placeholder="${U.to.length ? 'Add' : 'Who gets it: a name, a mail or #channel'}" aria-label="Add a recipient"></div>` : '';
|
|
1502
1662
|
const old = past ? `<div class="czs-secs"><div class="czs-body" style="padding-top:12px">${(U.sent || []).length ? U.sent.slice().reverse().map((x) => `<div class="czs-it"><span class="w">Week ${esc(x.week)}<span>${esc(new Date(x.at).toDateString())} · to ${esc((x.to || []).join(', '))}</span></span></div>`).join('') : '<p>No update was sent yet.</p>'}</div></div>` : '';
|
|
1503
1663
|
b.innerHTML = to + sum + old + secs + '<p class="hx-n">Send opens a draft in your own mail app. PMOS sends nothing by itself; it records that you opened the draft.</p><p class="hx-n" role="status" id="hvMsg"></p>';
|
|
@@ -1517,9 +1677,18 @@ function openUpdateViewer(o = {}) {
|
|
|
1517
1677
|
const t = e.target.closest('button'); if (!t) return;
|
|
1518
1678
|
if (t.dataset.hvRm) { const to = H.weeklyUpdate(S.D).to.slice(); to.splice(+t.dataset.hvRm, 1); saveTo(to); return; }
|
|
1519
1679
|
if (t.dataset.hvIni) { goIni(t.dataset.hvIni); return; }
|
|
1680
|
+
if ('hvConfirm' in t.dataset) { confirmRisks(H.weeklyUpdate(S.D, S.D.now).unconfirmed, () => draw(body)); return; }
|
|
1520
1681
|
if (t.dataset.czsAct === 'send' || 'hvSend' in t.dataset) sendUpdate();
|
|
1521
1682
|
};
|
|
1522
1683
|
}
|
|
1684
|
+
/** ✓ on every risk Claude proposed and the PM has not confirmed (the "Mark … at risk" ✦ of the programs): each is a Keep on its program's charter
|
|
1685
|
+
* (POST /api/programs/suggestion), so its initiatives read At risk on every surface. */
|
|
1686
|
+
async function confirmRisks(L, after) {
|
|
1687
|
+
let ok = 0; const bad = [];
|
|
1688
|
+
for (const x of L || []) { const r = await pmPost('/api/programs/suggestion', { program: x.program, key: x.key, answer: 'keep' }).catch(() => null); if (r && r.ok) ok++; else bad.push((r && r.error) || 'not saved'); }
|
|
1689
|
+
toast(ok && !bad.length ? (ok === 1 ? 'The risk is confirmed.' : 'All ' + ok + ' risks are confirmed.') : ok ? ok + ' of ' + L.length + ' risks are confirmed. ' + bad[0] + '.' : 'No risk was confirmed: ' + (bad[0] || 'nothing to confirm') + '.');
|
|
1690
|
+
if (ok) { await load(); if (after) after(); }
|
|
1691
|
+
}
|
|
1523
1692
|
/** This page's address for one view (the Viewer's Open in a new tab): the workspace and board of this page, and the view's own keys. */
|
|
1524
1693
|
const selfUrl = (o) => { const q = new URLSearchParams(); if (WS) q.set('ws', WS); if (BOARD) q.set('board', BOARD); for (const [k, v] of Object.entries(o)) q.set(k, v); return location.pathname + '?' + q.toString(); };
|
|
1525
1694
|
const deskWs = () => (S.D.desk && S.D.desk.ws) || ((S.D.board && S.D.board.board) || {}).ws || WS || undefined;
|
|
@@ -36,9 +36,10 @@ import { adler } from './cozy-adler.js';
|
|
|
36
36
|
import { snapOf, turnEnded } from './chat-turn-end.js'; // a finished turn re-reads what the Viewer shows (batch 25 item 3)
|
|
37
37
|
import { nextPromptUi } from './next-prompt-ui.js'; // the suggested next prompt and the Sharper card above the box (batch 26 item 2)
|
|
38
38
|
/* The views a chat opens go in the Viewer: on a page with no dialog the old viewer's views draw in the cozy card (premium-viewer.js). */
|
|
39
|
+
let REPORT_TAB = false; // a report tab was opened from a chat on this page: only then is premium-report-tab.js read at send
|
|
39
40
|
const views = {
|
|
40
41
|
ctx: () => import('./premium-vw-ctx.js'), file: () => import('./premium-vw-file.js'), attach: () => import('./premium-attach.js'),
|
|
41
|
-
block: (b, o) => (b.kind === 'report' ? import('./premium-report-tab.js').then((M) => M.openReportTab(b, o)) : import('./premium-chart-tab.js').then((M) => M.openChartTab(b))),
|
|
42
|
+
block: (b, o) => (b.kind === 'report' ? (REPORT_TAB = true, import('./premium-report-tab.js').then((M) => M.openReportTab(b, o))) : import('./premium-chart-tab.js').then((M) => M.openChartTab(b))),
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
const SVG = (d, w = 16, sw = 2) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
@@ -477,7 +478,10 @@ export function mountChat(el, host) {
|
|
|
477
478
|
C.pend.push(p); C.files = []; C.jump = true; draw();
|
|
478
479
|
if (t.key) {
|
|
479
480
|
const files = sentFiles.map((f) => f.saved);
|
|
480
|
-
|
|
481
|
+
// the set-up report beside this chat goes with the message, so feedback in any words reaches its draft (Wawan, 2026-11-04)
|
|
482
|
+
const report = REPORT_TAB ? await import('./premium-report-tab.js').then((M) => M.reportBeside(t.key)).catch(() => null) : null;
|
|
483
|
+
const extra = { ...(t.context ? { context: t.context } : {}), ...(report ? { report } : {}) };
|
|
484
|
+
r = await host.pmPost('/api/briefing-premium/ask' + (t.ws ? '?ws=' + encodeURIComponent(t.ws) : ''), files.length ? { key: t.key, text, files, ...extra } : { key: t.key, text, ...extra }).catch(() => null);
|
|
481
485
|
} else r = await host.pmPost('/api/session/say', { id: t.sid, text }).catch(() => null);
|
|
482
486
|
C.sending = false;
|
|
483
487
|
if (!r || r.ok !== true) {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* iniLayout(D, id) one initiative as a layout of Home: its card's groups (Needs you, Sessions, Stakeholders, Meetings, Routines, Files,
|
|
7
7
|
* Finished sessions) and the Elsewhere line
|
|
8
8
|
* productCard(D) the product's card (every anchor from D.board.anchorRows when sent): "This week", the KRs and the budget on track or at risk, Then, the next routine, the CTA
|
|
9
|
-
* weeklyUpdate(D) the weekly update the Viewer shows and Send turns into a mail draft:
|
|
9
|
+
* weeklyUpdate(D) the weekly update the Viewer shows and Send turns into a mail draft: risks to confirm, At risk, Needs a decision, On track,
|
|
10
|
+
* Not read (workspace grill W2); updateNews(D) says whether there is one to draft this week
|
|
10
11
|
* streamOf(conv, o) the chat's stream: day dividers, "New since you left", "N earlier messages", messages, Worked-for folds
|
|
11
12
|
* meetLineOf(P) the meeting line: the current or next meeting, +N for the rest of the day
|
|
12
13
|
* roleOf(s, roles) an agent's role name and initial for a session ("Claude" when no skill names one)
|
|
@@ -107,7 +108,7 @@ export function needsOf(D) {
|
|
|
107
108
|
// the product's weekly update: drafted from the board each week, it waits on the PM until it is sent (a sent record for this week)
|
|
108
109
|
if (boardRows(D).length) {
|
|
109
110
|
const P = (D.desk && D.desk.product) || {}, wk = weekOf(D.now || Date.now());
|
|
110
|
-
if (!(P.sent || []).some((x) => x.week === wk.iso)) {
|
|
111
|
+
if (!(P.sent || []).some((x) => x.week === wk.iso) && updateNews(D, D.now || Date.now()).draft) {
|
|
111
112
|
const rt = nextRoutine(D, D.now || Date.now(), /comm|update|weekly|status|stakeholder/i);
|
|
112
113
|
rows.push({ id: 'n:update', kind: 'update', key: UPDATE_KEY, ini: 'product', name: 'Weekly update is ready to send', hint: 'Communicator · ' + (rt ? 'goes out ' + rt.when : 'send it when it reads right'), state: 'needs', icon: 'doc', initialRole: 'Co' });
|
|
113
114
|
}
|
|
@@ -144,6 +145,9 @@ export const OPEN = (r) => !(r.acts && (r.acts.archived || r.acts.life === 'done
|
|
|
144
145
|
/* ── the product ── */
|
|
145
146
|
const statusOf = (r) => String(r.status || '').toLowerCase();
|
|
146
147
|
const risky = (r) => r.late || /risk|late|stalled|blocked/.test(statusOf(r));
|
|
148
|
+
/* workspace grill W2: on track only from a read; a row with nothing read (status 'no signal', or no status at all) is never counted as on track */
|
|
149
|
+
const onTrack = (r) => !risky(r) && (statusOf(r) === 'on track' || statusOf(r) === 'done');
|
|
150
|
+
const noSignal = (r) => !risky(r) && !onTrack(r) && !/hold/.test(statusOf(r));
|
|
147
151
|
const anchorOf = (r) => (r.kr ? r.kr : /^health:/.test(String(r.anchor || '')) ? String(r.anchor) : r.anchor ? String(r.anchor) : 'No anchor');
|
|
148
152
|
export function productName(D) {
|
|
149
153
|
const b = D.board && D.board.board;
|
|
@@ -159,10 +163,12 @@ export function productCard(D, now = Date.now()) {
|
|
|
159
163
|
for (const r of rows) { const a = anchorOf(r); if (!by.has(a)) by.set(a, []); by.get(a).push(r); }
|
|
160
164
|
const items = [...by.entries()].filter(([a]) => a !== 'No anchor').map(([a, L]) => {
|
|
161
165
|
const bad = L.filter(risky);
|
|
162
|
-
|
|
166
|
+
// an anchor is on track only when a read says so for one of its initiatives and none is late or at risk; else it has no signal
|
|
167
|
+
const read = L.some(onTrack);
|
|
168
|
+
const lead = (bad[0] || L.find(onTrack) || L[0]);
|
|
163
169
|
const health = /^health:/.test(a);
|
|
164
|
-
const words = bad.length ? (bad[0].late ? 'running late' : /stalled/.test(statusOf(bad[0])) ? 'stalled' : statusOf(bad[0]) || 'at risk') : health ? 'within budget' : 'on track';
|
|
165
|
-
return { kind: bad.length ? 'risk' : 'ok', label: (health ? a : a + ' · ' + lead.title) + ' ' + (health && !bad.length ? '· within budget' : words), anchor: a, ids: L.map((x) => x.id) };
|
|
170
|
+
const words = bad.length ? (bad[0].late ? 'running late' : /stalled/.test(statusOf(bad[0])) ? 'stalled' : statusOf(bad[0]) || 'at risk') : !read ? 'no signal' : health ? 'within budget' : 'on track';
|
|
171
|
+
return { kind: bad.length ? 'risk' : read ? 'ok' : 'none', label: (health ? a : a + ' · ' + lead.title) + ' ' + (health && !bad.length && read ? '· within budget' : words), anchor: a, ids: L.map((x) => x.id) };
|
|
166
172
|
});
|
|
167
173
|
const krs = items.filter((x) => !/^health:/.test(x.anchor)).length, budgets = items.length - krs;
|
|
168
174
|
const ok = items.filter((x) => x.kind === 'ok').length;
|
|
@@ -186,9 +192,22 @@ export function weekOf(now = Date.now()) {
|
|
|
186
192
|
const d = new Date(now); const day = (d.getDay() + 6) % 7; d.setDate(d.getDate() - day);
|
|
187
193
|
return { iso: d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()), label: MON[d.getMonth()] + ' ' + d.getDate() };
|
|
188
194
|
}
|
|
189
|
-
/**
|
|
195
|
+
/** What the week holds (workspace grill W2): the rows that moved (a stage entered this week), the rows at risk or late, Claude's risks the PM
|
|
196
|
+
* has not confirmed, and the open rows nothing was read for. The update is drafted only when something moved or a risk is open. */
|
|
197
|
+
export function updateNews(D, now = Date.now()) {
|
|
198
|
+
const rows = boardRows(D).filter(OPEN).filter((r) => !/hold/.test(statusOf(r)));
|
|
199
|
+
const trail = (D.trail && D.trail.items) || {};
|
|
200
|
+
const moved = rows.filter((r) => trail[r.id] && trail[r.id].days != null && trail[r.id].days < 7);
|
|
201
|
+
const risk = rows.filter(risky);
|
|
202
|
+
const unconfirmed = (D.board && Array.isArray(D.board.openRisks) ? D.board.openRisks : []).filter((x) => x && x.key);
|
|
203
|
+
return { moved, risk, unconfirmed, unread: rows.filter(noSignal), draft: moved.length > 0 || risk.length > 0 || unconfirmed.length > 0 };
|
|
204
|
+
}
|
|
205
|
+
/** The weekly update from the board: first the risks Claude found that the PM has not confirmed, then At risk, Needs a decision, On track (only
|
|
206
|
+
* what a read says) and Not read (the initiatives nothing was read for). `draft` is false in a week where nothing moved and no risk is open:
|
|
207
|
+
* then there is no update to send. */
|
|
190
208
|
export function weeklyUpdate(D, now = Date.now()) {
|
|
191
209
|
const rows = boardRows(D).filter(OPEN);
|
|
210
|
+
const N = updateNews(D, now);
|
|
192
211
|
const trail = (D.trail && D.trail.items) || {}, usual = (D.trail && D.trail.usual) || {};
|
|
193
212
|
const needIds = new Set(needsOf(D).filter((r) => r.row && r.row.kind === 'decision').map((r) => r.ini));
|
|
194
213
|
const line = (r) => {
|
|
@@ -196,20 +215,33 @@ export function weeklyUpdate(D, now = Date.now()) {
|
|
|
196
215
|
const days = t && t.days != null ? 'In ' + r.stage + ' for ' + plural(t.days, 'day', 'days') : 'In ' + (r.stage || 'no stage');
|
|
197
216
|
const over = t && u && u.days != null && t.days > u.days ? ', ' + plural(t.days - u.days, 'day', 'days') + ' over its usual time' : '';
|
|
198
217
|
const fin = r.finish ? '. Planned finish ' + r.finish : '';
|
|
199
|
-
|
|
218
|
+
const why = r.signal && r.signal.why ? '. ' + r.signal.why : '';
|
|
219
|
+
return days + over + fin + why + '.';
|
|
200
220
|
};
|
|
201
221
|
const item = (r) => ({ id: r.id, title: r.title, meta: [r.stage, anchorOf(r)].filter(Boolean).join(' · '), text: line(r), flag: r.late && !/late/.test(statusOf(r)) ? 'Looks green, but it is past its planned finish' : '' });
|
|
202
222
|
const decide = rows.filter((r) => needIds.has(r.id) || /stalled|hold/.test(statusOf(r)));
|
|
203
223
|
const risk = rows.filter((r) => !decide.includes(r) && risky(r));
|
|
204
|
-
const ok = rows.filter((r) => !decide.includes(r) && !risk.includes(r));
|
|
224
|
+
const ok = rows.filter((r) => !decide.includes(r) && !risk.includes(r) && onTrack(r));
|
|
225
|
+
const none = rows.filter((r) => !decide.includes(r) && !risk.includes(r) && !ok.includes(r));
|
|
226
|
+
const conf = N.unconfirmed.map((x) => ({ id: (x.inis && x.inis[0]) || '', key: x.key, program: x.program, title: x.title, meta: x.programTitle || '', text: (x.why || '') + (x.src && x.src.label ? ' (' + x.src.label + ')' : ''), flag: '', risk: true }));
|
|
205
227
|
const P = (D.desk && D.desk.product) || {};
|
|
206
|
-
|
|
228
|
+
const sections = [];
|
|
229
|
+
if (conf.length) sections.push(['confirm', 'Before you send · ' + plural(conf.length, 'risk you have not confirmed', 'risks you have not confirmed'), conf]);
|
|
230
|
+
sections.push(['risk', 'At risk', risk.map(item)], ['decide', 'Needs a decision', decide.map(item)], ['ok', 'On track', ok.map(item)]);
|
|
231
|
+
if (none.length) sections.push(['none', 'Not read', none.map((r) => ({ ...item(r), text: 'Nothing read says how it is going. ' + line(r) }))]);
|
|
232
|
+
return { week: weekOf(now), to: P.recipients || [], sent: P.sent || [], draft: N.draft, unconfirmed: N.unconfirmed, sections };
|
|
233
|
+
}
|
|
234
|
+
const COUNT_WORDS = { confirm: ['risk to confirm', 'risks to confirm'], risk: ['at risk', 'at risk'], decide: ['needs a decision', 'need a decision'], ok: ['on track', 'on track'], none: ['not read', 'not read'] };
|
|
235
|
+
/** The update's summary in words, from its sections: "2 at risk, 3 on track and 4 not read." */
|
|
236
|
+
export function updateWords(U) {
|
|
237
|
+
const w = U.sections.filter(([, , L]) => L.length).map(([k, , L]) => L.length + ' ' + (L.length === 1 ? COUNT_WORDS[k][0] : COUNT_WORDS[k][1]));
|
|
238
|
+
return w.length ? w.join(', ').replace(/, ([^,]*)$/, ' and $1') + '.' : 'No initiative is open this week.';
|
|
207
239
|
}
|
|
208
|
-
/** The update as mail text, for the mailto draft. */
|
|
240
|
+
/** The update as mail text, for the mailto draft. The risks the PM has not confirmed are not mailed: they are the PM's to check first. */
|
|
209
241
|
export function updateText(U, name) {
|
|
210
242
|
const out = [name + ' · week of ' + U.week.label, ''];
|
|
211
|
-
for (const [, h, L] of U.sections) { if (!L.length) continue; out.push(h.toUpperCase()); for (const x of L) out.push('- ' + x.title + ' (' + x.meta + '): ' + x.text + (x.flag ? ' ' + x.flag + '.' : '')); out.push(''); }
|
|
212
|
-
out.push('Drafted in PMOS from the
|
|
243
|
+
for (const [k, h, L] of U.sections) { if (!L.length || k === 'confirm') continue; out.push(h.toUpperCase()); for (const x of L) out.push('- ' + x.title + (x.meta ? ' (' + x.meta + ')' : '') + ': ' + x.text + (x.flag ? ' ' + x.flag + '.' : '')); out.push(''); }
|
|
244
|
+
out.push('Drafted in PMOS from what it read: Jira, the plan\'s dates and the risks you confirmed.');
|
|
213
245
|
return out.join('\n');
|
|
214
246
|
}
|
|
215
247
|
|
|
@@ -252,7 +284,7 @@ function productRow(D) {
|
|
|
252
284
|
const P = productCard(D);
|
|
253
285
|
const risk = P.items.filter((x) => x.kind === 'risk').length;
|
|
254
286
|
const kids = needsOf(D).filter((r) => r.kind === 'update');
|
|
255
|
-
const upd = P.sent ? 'update sent' : 'update ready';
|
|
287
|
+
const upd = P.sent ? 'update sent' : updateNews(D, D.now || Date.now()).draft ? 'update ready' : 'nothing new this week';
|
|
256
288
|
return { id: 'p:product', kind: 'product', name: P.name, hint: 'Product · ' + upd + (risk ? ', ' + risk + ' at risk' : ''), state: P.sent ? 'quiet' : 'needs', icon: 'product',
|
|
257
289
|
kids, chip: P.sent ? null : { text: 'your turn', loud: true } };
|
|
258
290
|
}
|