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
|
@@ -90,6 +90,8 @@ const rowsLib = require('./initiative-rows.js');
|
|
|
90
90
|
const writer = require('./initiative-writer.js');
|
|
91
91
|
const itemSpans = require('./item-spans.js');
|
|
92
92
|
const jiraStatus = require('./jira-status.js');
|
|
93
|
+
const SS = require('./status-signal.js');
|
|
94
|
+
const jiraOf = (root, id) => { try { return jiraStatus.readSnapshot(root, id); } catch { return null; } };
|
|
93
95
|
const routesInit = require('./routes-initiatives.js');
|
|
94
96
|
const jiraTickets = require('./jira-tickets.js');
|
|
95
97
|
const { repoScript } = require('./repo-script.js');
|
|
@@ -101,6 +103,7 @@ const TS = require('./task-store.js');
|
|
|
101
103
|
const holdSpans = require('./hold-spans.js');
|
|
102
104
|
const ledgers = require('./plan-ledgers.js');
|
|
103
105
|
const intake = require(repoScript('intake.js'));
|
|
106
|
+
const OS = require('./one-stage.js');
|
|
104
107
|
|
|
105
108
|
const LEDGER = 'boards.jsonl';
|
|
106
109
|
const SELF_REFUSAL = 'PMOS keeps its derived ladder: its stages come from scripts/workflow-state.js, so a move or a workflow edit is refused here';
|
|
@@ -302,14 +305,11 @@ function frontmatter(root, rel) {
|
|
|
302
305
|
return out;
|
|
303
306
|
}
|
|
304
307
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (healthLevel === 'at-risk') return 'at risk';
|
|
311
|
-
return 'on track';
|
|
312
|
-
}
|
|
308
|
+
/* workspace grill W2: the status says only what PMOS knows (lib/status-signal.js decides it; this keeps the inputs). */
|
|
309
|
+
const statusIn = (rec, health, end, day, jira) => ({ accepted: !!rec.accepted, stopped: rec.stopped ? { state: rec.stopped.state } : null,
|
|
310
|
+
reasons: health && Array.isArray(health.reasons) ? health.reasons.map((x) => ({ level: x.level, code: x.code, text: x.text, source: x.source })) : [],
|
|
311
|
+
end: end || null, day, jira: jira ? { fetchedAt: jira.fetchedAt, tickets: [...jira.tickets.values()].map((t) => ({ key: t.key, status: t.status, category: t.category, sprint: t.sprint, flagged: !!t.flagged })) } : null });
|
|
312
|
+
function statusOf(rec, health, end, day, jira) { return SS.statusOf(statusIn(rec, health, end, day, jira)).status; }
|
|
313
313
|
|
|
314
314
|
/** The board-independent part of every work row of a repository, cached by the read signature. */
|
|
315
315
|
/* A READ PASS (ctx.boardsPass, a Map): a page that asks several of these routes for one answer (GET /api/board-premium asks ten)
|
|
@@ -333,6 +333,7 @@ function baseRowsRead(w) {
|
|
|
333
333
|
// every lifecycle line, not only the latest: a hold is a gap on the timeline (feedback batch 9 item 8)
|
|
334
334
|
let lcFile = null;
|
|
335
335
|
try { lcFile = path.join(ledgers.runsDir(w.root), 'lifecycle.jsonl'); const st = fs.statSync(lcFile); sig.lifecycle = st.size + ':' + st.mtimeMs; } catch { sig.lifecycle = 'none'; }
|
|
336
|
+
try { const jd = jiraTickets.jiraDir(w.root); sig.jira = fs.readdirSync(jd).filter((n) => n.endsWith('.md')).map((n) => { const st = fs.statSync(path.join(jd, n)); return n + ':' + st.mtimeMs; }).join(','); } catch { sig.jira = 'none'; }
|
|
336
337
|
const cacheKey = JSON.stringify(sig) + day;
|
|
337
338
|
const hit = rowCache.get(w.root);
|
|
338
339
|
if (hit && hit.key === cacheKey) return { ok: true, rows: hit.rows };
|
|
@@ -365,7 +366,8 @@ function baseRowsRead(w) {
|
|
|
365
366
|
id, title: r.title, lane: r.lane || null,
|
|
366
367
|
owner: fm.owner ? String(fm.owner) : null,
|
|
367
368
|
anchor: r.anchor || null, rank: Number.isInteger(r.rank) ? r.rank : null, dormant: !!r.dormant,
|
|
368
|
-
status: statusOf(rec, r.health
|
|
369
|
+
status: statusOf(rec, r.health, end, day, jiraOf(w.root, id)),
|
|
370
|
+
statusIn: statusIn(rec, r.health, end, day, jiraOf(w.root, id)),
|
|
369
371
|
health: r.health ? r.health.level : null,
|
|
370
372
|
start: span ? span.start || null : null, end,
|
|
371
373
|
progress: r.progress && r.progress.total ? Math.round((r.progress.done / r.progress.total) * 100) / 100 : null, // a fraction 0..1, as the PRD row reads
|
|
@@ -377,6 +379,7 @@ function baseRowsRead(w) {
|
|
|
377
379
|
hasSpec, record: relRec || null, domainFolder: dm ? dm[1] : null,
|
|
378
380
|
recordEpic: jiraTickets.anchorOf(fm),
|
|
379
381
|
holds: holds.get(id) || [], workSpans: work.map((x) => ({ start: x.start, due: x.due, stage: x.stage || null })),
|
|
382
|
+
accepted: !!rec.accepted, root: w.root, // W1: what lib/one-stage.js reads besides the row (the sign-off, the Jira snapshot's repository)
|
|
380
383
|
});
|
|
381
384
|
}
|
|
382
385
|
rowCache.set(w.root, { key: cacheKey, rows });
|
|
@@ -499,7 +502,33 @@ function closedOf(row, wx) {
|
|
|
499
502
|
// the last stage, not counting the ones after it that only end work
|
|
500
503
|
return wf.length > 1 && wf.slice(i + 1).every((x) => END_RE.test(String(x && x.name || '')));
|
|
501
504
|
}
|
|
505
|
+
/** W1: the one stage of a row (lib/one-stage.js): the PM's word, then Jira, then dated plan items, then the files (base.reading).
|
|
506
|
+
* A Board move is the PM's word while it is newer than the last Jira read. → the stageOf answer, with `moveDropped` (a mapped move older than the Jira read). */
|
|
507
|
+
function oneStageOf(board, base, folded, epics) {
|
|
508
|
+
const k = B.key(board.id, base.id);
|
|
509
|
+
const movedName = folded && folded.stages ? folded.stages.get(k) : null;
|
|
510
|
+
let move = null;
|
|
511
|
+
if (movedName && !(board && board.self)) {
|
|
512
|
+
const last = [...((folded && folded.moves) || [])].reverse().find((m) => m.board === board.id && m.id === base.id);
|
|
513
|
+
const pf = WF.resolve(libOf(folded), null);
|
|
514
|
+
const st = WF.stageByRef(pf, movedName) || mergedStage(pf, movedName, null);
|
|
515
|
+
if (st) move = { key: st.key, at: last ? last.at : null };
|
|
516
|
+
}
|
|
517
|
+
const jira = base.root ? OS.factsOf(base.root, base.id, epics) : null;
|
|
518
|
+
const one = OS.stageOf({ files: base.reading, life: base.lifecycle ? { ...(base.stopped || {}), state: base.lifecycle } : null, move, jira, plan: { dated: (base.workSpans || []).length }, accepted: !!base.accepted });
|
|
519
|
+
const under = one.under || one;
|
|
520
|
+
// a move to a board stage the eight steps do not name (Execute, Doing) stays the board's: only a mapped move older than Jira's read gives way
|
|
521
|
+
return { ...one, moveDropped: !!(move && under.setBy !== 'pm') };
|
|
522
|
+
}
|
|
502
523
|
function boardRow(board, base, folded, lib) {
|
|
524
|
+
const ep = epicsOf(base, folded);
|
|
525
|
+
const one = oneStageOf(board, base, folded, ep.epics);
|
|
526
|
+
// the board's stage name follows the one stage: a move the one stage did not keep (an older move, Jira read since) no longer places it
|
|
527
|
+
if (one.moveDropped && folded && folded.stages && folded.stages.has(B.key(board.id, base.id))) {
|
|
528
|
+
const own = folded.iniWf && folded.iniWf.get(B.key(board.id, base.id));
|
|
529
|
+
if (!own) { const st = new Map(folded.stages); st.delete(B.key(board.id, base.id)); folded = Object.assign(Object.create(Object.getPrototypeOf(folded)), folded, { stages: st }); }
|
|
530
|
+
}
|
|
531
|
+
if (one.setBy !== 'files' || !folded || !folded.stages || !folded.stages.has(B.key(board.id, base.id)) || one.moveDropped) base = { ...base, reading: one.step };
|
|
503
532
|
const s = B.stageOf(board, base.id, base.reading, folded);
|
|
504
533
|
const ts = taskSpanOf(folded, base.id); // the bar covers the breakdown items and the tasks
|
|
505
534
|
const wf = B.workflowOf(board, base.id, folded);
|
|
@@ -513,15 +542,28 @@ function boardRow(board, base, folded, lib) {
|
|
|
513
542
|
// item 13); stageSpans: where the dated work of each stage runs, so a skipped stage between two of them is marked » (XgapAGrill)
|
|
514
543
|
holds: base.holds || [], ...gapFields((base.workSpans || []).concat(taskSpansOf(folded, base.id))),
|
|
515
544
|
template: B.templateOf(board, base.id, folded), workflow: wf.map((x) => ({ name: x.name, cat: x.cat })),
|
|
516
|
-
...
|
|
545
|
+
...ep,
|
|
546
|
+
// W1: the one stage every surface draws (public/pm/stage-chip.js), and what set it
|
|
547
|
+
oneStage: { stage: one.stage, step: one.step, label: one.label, setBy: one.setBy, source: one.source, readAt: one.readAt },
|
|
517
548
|
};
|
|
518
549
|
const wx = workflowRowOf(board, base, folded, lib || libOf(folded), s.stage);
|
|
519
550
|
Object.assign(row, wfRowFields(wx));
|
|
551
|
+
// a board whose columns merge or leave out a step ("Discover" holds Triage) shows the step its column names: the chip says the same
|
|
552
|
+
if (!['shipped', 'on-hold', 'cancelled'].includes(one.stage) && row.wfStage && OS.STEPS.includes(row.wfStage.key) && row.wfStage.key !== one.step && !wx.own) {
|
|
553
|
+
row.oneStage = { ...row.oneStage, stage: row.wfStage.key, step: row.wfStage.key, label: OS.labelOf(row.wfStage.key) };
|
|
554
|
+
}
|
|
520
555
|
if (wx.own) { row.stage = (wx.stages.find((x) => x.key === wx.stage) || {}).name || row.stage; row.cat = null; row.stageSource = 'move'; }
|
|
521
556
|
// batch 26 item 5: an initiative in its workflow's terminal stage is finished, so it is never late or at risk. Every surface (the Board's
|
|
522
557
|
// tone, dot, late line and edge tabs, Home, Kanban, the program rollups) reads the status this leaves and row.closed.
|
|
558
|
+
// workspace grill W2: the status from what PMOS read, a passed date, or a risk the PM confirmed on a program milestone; else No signal.
|
|
559
|
+
// W1's one stage (row.oneStage, lib/one-stage.js stageOf) is read when it is there: a stage the PM set to shipped is done, a Jira-set stage a read
|
|
560
|
+
if (base.statusIn) {
|
|
561
|
+
const sig = SS.statusOf({ ...base.statusIn, stage: row.oneStage || null, confirmed: (folded && folded.risks && folded.risks.get(base.id)) || null });
|
|
562
|
+
row.status = sig.status; row.signal = { from: sig.from, why: sig.why, source: sig.source, readAt: sig.readAt };
|
|
563
|
+
}
|
|
523
564
|
row.closed = closedOf(row, wx);
|
|
524
|
-
|
|
565
|
+
// a finished initiative is never late, at risk or without a signal: its last stage is the PM's own move (batch 26 item 5)
|
|
566
|
+
if (row.closed && ['late', 'at risk', 'no signal'].includes(row.status)) { row.status = 'on track'; row.signal = { from: 'pm', why: 'In its last stage', source: 'the workflow', readAt: null }; }
|
|
525
567
|
const w = [];
|
|
526
568
|
if (s.source === 'move') {
|
|
527
569
|
const last = [...folded.moves].reverse().find((m) => m.board === board.id && m.id === base.id);
|
|
@@ -539,10 +581,17 @@ function frameRead(ctx, w) {
|
|
|
539
581
|
const boards = boardsOf(ctx, w, folded);
|
|
540
582
|
const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null;
|
|
541
583
|
folded.wfLib = WS.library(d); // D99: the workflow library rides on the fold, so every boardRow call reads one copy
|
|
584
|
+
folded.risks = risksOf(ctx, w); // workspace grill W2: the risks the PM confirmed on a program milestone
|
|
542
585
|
return { w, folded, boards };
|
|
543
586
|
}
|
|
544
587
|
/** A fresh fold with the workflow library on it (frameFor's, without the boards). Inside a read pass, the pass's own fold. */
|
|
545
|
-
function foldOf(ctx, w) { if (ctx && ctx.boardsPass instanceof Map) return frameFor(ctx, w).folded; const f = B.fold(readLedger(ctx, w)); const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null; f.wfLib = WS.library(d); return f; }
|
|
588
|
+
function foldOf(ctx, w) { if (ctx && ctx.boardsPass instanceof Map) return frameFor(ctx, w).folded; const f = B.fold(readLedger(ctx, w)); const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null; f.wfLib = WS.library(d); f.risks = risksOf(ctx, w); return f; }
|
|
589
|
+
/** The initiatives of workspace w that a program milestone the PM kept "at risk" waits on (lib/status-signal.js confirmedRisks). Every
|
|
590
|
+
* program of every workspace is read once per read pass; a program ledger that cannot be read confirms nothing. */
|
|
591
|
+
function risksOf(ctx, w) {
|
|
592
|
+
if (!ctx || !ctx.workspaceList) return new Map();
|
|
593
|
+
try { return SS.confirmedRisks(inPass(ctx, 'programs', () => require('./routes-programs.js').all(ctx)), w.id); } catch { return new Map(); }
|
|
594
|
+
}
|
|
546
595
|
const libOf = (folded) => (folded && folded.wfLib) || WS.library(null);
|
|
547
596
|
/** A workflow by id for workspace w: its own library first, then the home workspace's, then any other open workspace's (not archived there).
|
|
548
597
|
* → { wf, ws: null } | { wf, ws: <the workspace it came from> } | null */
|
|
@@ -83,6 +83,23 @@ function iniInfoOf(ctx, key, wsId) {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** The set-up report open beside this chat, for lib/report-chat.js wrap: { id, name, draft } | null. Wawan, 2026-11-04: feedback in the chat,
|
|
87
|
+
* in any words, is about the draft beside it. The page names the report (pm/premium-report-tab.js reportBeside); it counts only while it is set
|
|
88
|
+
* up, read in the chat's own workspace, then the others. */
|
|
89
|
+
function reportInfoOf(ctx, wsId, rid) {
|
|
90
|
+
const id = String(rid || ''); if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id)) return null;
|
|
91
|
+
const RS = require('./report-setups.js');
|
|
92
|
+
const list = ctx.workspaceList ? ctx.workspaceList() : [];
|
|
93
|
+
const first = wsId && ctx.getWs ? ctx.getWs(wsId) : null;
|
|
94
|
+
for (const w of [first, ...list.filter((x) => !first || x.id !== first.id)].filter((x) => x && x.root)) {
|
|
95
|
+
let st; try { st = RS.stateOf(w.root); } catch { continue; }
|
|
96
|
+
const s = st.setups.get(id); if (!s) continue;
|
|
97
|
+
const last = (s.drafted || []).filter((d) => d && d.path).slice(-1)[0];
|
|
98
|
+
return { id, name: s.name || id, draft: last ? last.path : null };
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
async function post(req, res, url, ctx) {
|
|
87
104
|
const act = url.pathname.slice('/api/briefing-premium/'.length);
|
|
88
105
|
const got = await ctx.readJson(req);
|
|
@@ -125,7 +142,7 @@ async function post(req, res, url, ctx) {
|
|
|
125
142
|
if (!att.ok) { ctx.json(res, { ok: false, error: att.error }, att.status); return true; }
|
|
126
143
|
// batch 20 item 13: the PM can start a grill any time by asking for it in an initiative's chat; it also runs by itself
|
|
127
144
|
const g2 = grillAsk(ctx, b.key, b.text, wsId);
|
|
128
|
-
const a = (ctx.chatSessions ? Ag.chat : Ag.ask)({ runtimeDir, key: b.key, text: Files.withFiles(g2 ? g2.text : b.text, att.paths), context: b.context, sessions: ctx.chatSessions, model: /^work:/.test(String(b.key || '')) ? WP.peekModel() : /^board:/.test(String(b.key || '')) ? BoardP.peekModel() : /^ins:/.test(String(b.key || '')) ? InsP.peekModel() : /^(init|doc):/.test(String(b.key || '')) ? InitP.peekModel() : sk || (/^src:/.test(String(b.key || '')) && !(b.key === 'src:this-page' && url.searchParams.get('ws'))) ? Prod.peekModel() : BP.peekModel(root), spawnSession: ctx.spawnSession, wsId, iniInfo: () => iniInfoOf(ctx, b.key, wsId) });
|
|
145
|
+
const a = (ctx.chatSessions ? Ag.chat : Ag.ask)({ runtimeDir, key: b.key, text: Files.withFiles(g2 ? g2.text : b.text, att.paths), context: b.context, sessions: ctx.chatSessions, reportInfo: () => reportInfoOf(ctx, wsId, b.report), model: /^work:/.test(String(b.key || '')) ? WP.peekModel() : /^board:/.test(String(b.key || '')) ? BoardP.peekModel() : /^ins:/.test(String(b.key || '')) ? InsP.peekModel() : /^(init|doc):/.test(String(b.key || '')) ? InitP.peekModel() : sk || (/^src:/.test(String(b.key || '')) && !(b.key === 'src:this-page' && url.searchParams.get('ws'))) ? Prod.peekModel() : BP.peekModel(root), spawnSession: ctx.spawnSession, wsId, iniInfo: () => iniInfoOf(ctx, b.key, wsId) });
|
|
129
146
|
ctx.json(res, a.body, a.status);
|
|
130
147
|
return true;
|
|
131
148
|
}
|
|
@@ -373,7 +373,7 @@ function openInitiativesOf(w) {
|
|
|
373
373
|
if (rec && (rec.stopped || rec.accepted)) return;
|
|
374
374
|
const isTop = !dormant && top < PICKER_TOP;
|
|
375
375
|
if (isTop) top++;
|
|
376
|
-
ranked.push({ id: r.id, title: r.title, anchor: r.anchor, intent: '', stage: lifecycle.labelOf(rec), dormant, top: isTop });
|
|
376
|
+
ranked.push({ id: r.id, title: r.title, anchor: r.anchor, intent: '', stage: lifecycle.labelOf(rec, w.root), dormant, top: isTop });
|
|
377
377
|
};
|
|
378
378
|
for (const r of rm.rows) add(r, false);
|
|
379
379
|
for (const r of rm.dormant) add(r, true);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-your-move.js — what Your move reads from a product beside its Board rows (workspace grill W3, frame WgW3Move).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/your-move?ws=<id> → { ok, ws, today, verdicts: [lib/your-move.js verdict + board], kr: lib/your-move.js krOf | null }
|
|
5
|
+
*
|
|
6
|
+
* verdicts: the outcome checks past their date with no verdict (lib/outcomes.js scan, less Intelligence's verdicts in
|
|
7
|
+
* lib/insights-records.js), each with Claude's draft from the board's Jira tickets and the board it is on (the ✓ records it there through
|
|
8
|
+
* POST /api/insights-records/verdict). kr: the first key result of the OKR file that waits on a source (lib/okr-goals.js, the kr-metrics records
|
|
9
|
+
* as its readers). Read only: nothing is written. A part that cannot be read is left out, never guessed. */
|
|
10
|
+
const YM = require('./your-move.js');
|
|
11
|
+
|
|
12
|
+
const localDay = (ms = Date.now()) => { const d = new Date(ms); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
|
|
13
|
+
|
|
14
|
+
/** The board items of every board of the workspace, and the board each initiative is on. */
|
|
15
|
+
function itemsOf(ctx, w) {
|
|
16
|
+
const RB = require('./routes-boards.js'), TLR = require('./routes-task-links.js');
|
|
17
|
+
const items = [], boardOf = new Map();
|
|
18
|
+
let fr = null;
|
|
19
|
+
try { fr = RB.frameFor(ctx, w); } catch { fr = null; }
|
|
20
|
+
for (const b of (fr && fr.boards) || []) {
|
|
21
|
+
let f = null;
|
|
22
|
+
try { f = TLR.frame(ctx, w, b); } catch { f = null; }
|
|
23
|
+
if (!f) continue;
|
|
24
|
+
for (const r of f.rows || []) if (r && r.id && !boardOf.has(r.id)) boardOf.set(r.id, b.id);
|
|
25
|
+
items.push(...(f.items || []));
|
|
26
|
+
}
|
|
27
|
+
return { items, boardOf };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function verdicts(ctx, w, today) {
|
|
31
|
+
const S = require('./outcomes.js').scan(w.root, { today });
|
|
32
|
+
if (!S.due.length) return [];
|
|
33
|
+
const have = new Set();
|
|
34
|
+
try { const V = require('./insights-records.js').read(w.root).verdicts || {}; for (const b of Object.values(V)) for (const id of Object.keys(b || {})) have.add(id); } catch { /* none recorded */ }
|
|
35
|
+
const { items, boardOf } = itemsOf(ctx, w);
|
|
36
|
+
return YM.verdictsOf({ due: S.due, verdicts: have, items, today }).map((v) => ({ ...v, board: boardOf.get(v.ini) || null }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function kr(ctx, w, today) {
|
|
40
|
+
const OG = require('./okr-goals.js');
|
|
41
|
+
const okr = OG.read(w.root);
|
|
42
|
+
if (!okr.ok) return null;
|
|
43
|
+
let recs = [];
|
|
44
|
+
try { recs = require(require('./repo-script.js').repoScript('records/index.js')).list(require('./records-dir.js').recordsDirFor(ctx, w.id), 'kr-metrics').filter((k) => k.retired !== true); } catch { recs = []; }
|
|
45
|
+
const goals = recs.map((k) => ({ id: k.rid, title: k.title || '', kr: k.kr || null, readings: Array.isArray(k.readings) ? k.readings.length : 0, recipe: k.recipe || null }));
|
|
46
|
+
return YM.krOf(OG.build({ okr, goals, initiatives: [], records: {}, today }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
prefixes: ['/api/your-move'],
|
|
51
|
+
handle: async (req, res, url, ctx) => {
|
|
52
|
+
if (url.pathname !== '/api/your-move') return false;
|
|
53
|
+
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
54
|
+
let w = null;
|
|
55
|
+
try { w = ctx.getWs(url.searchParams.get('ws') || null); } catch { w = null; }
|
|
56
|
+
if (!w || !w.root) { ctx.json(res, { ok: false, error: 'no such workspace' }, 404); return true; }
|
|
57
|
+
const today = localDay();
|
|
58
|
+
let V = [], K = null;
|
|
59
|
+
try { V = verdicts(ctx, w, today); } catch { V = []; }
|
|
60
|
+
try { K = kr(ctx, w, today); } catch { K = null; }
|
|
61
|
+
ctx.json(res, { ok: true, ws: w.id, today, verdicts: V, kr: K });
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* status-signal.js — an initiative's status says only what PMOS knows (workspace grill W2, "Status says what it knows"; canvas In
|
|
3
|
+
* development, frame WgW2Status). THE ONE place a row's status is decided. Every surface (the Board's rows and header, Home's product row and
|
|
4
|
+
* card, the weekly update, the program rollups) reads the status this decides; none decides one of its own.
|
|
5
|
+
*
|
|
6
|
+
* statusOf(x) → { status, from, why, source, readAt }
|
|
7
|
+
* wordsOf(c) → '3 late · 1 at risk · 5 no signal' the pill's words from THE count (scripts/boards.js counts, whose
|
|
8
|
+
* byStatus holds 'no signal' apart: it is never counted as on track); ''
|
|
9
|
+
* when nothing is open
|
|
10
|
+
* confirmedRisks(programs, ws) → Map initiative → { why, program, milestone, name } the PM's ✓ on a "Mark … at risk" ✦
|
|
11
|
+
* openRisks(programs, ws) → [{ key, program, title, milestone, name, why, src, inis }] the same ✦ before the PM's ✓
|
|
12
|
+
* READ_DAYS · STATUSES
|
|
13
|
+
*
|
|
14
|
+
* THE RULES (the agreed W2 frame):
|
|
15
|
+
* done · cancelled · on hold the PM's word on the record (accepted, closed, cancelled, on-hold in lifecycle.jsonl); a stage the PM set to
|
|
16
|
+
* shipped (W1's stageOf, setBy 'pm') reads on track: it stays open for Monitor and Evaluate
|
|
17
|
+
* late only from a date that passed: the planned finish of the dated plan items, or an overdue milestone
|
|
18
|
+
* at risk only from a read (an overdue plan item, an idle initiative under way, a passed check-by date, a failed factory item, an
|
|
19
|
+
* open Jira ticket past its due date or its sprint's end, a ticket flagged blocked) or from a risk the PM confirmed (✓ on Claude's "Mark … at risk")
|
|
20
|
+
* on track only from a read: a Jira reading of the initiative's tickets in the last READ_DAYS with none past its due date, or dated
|
|
21
|
+
* plan items whose finish is still ahead
|
|
22
|
+
* no signal everything else: grey, and never counted as on track anywhere
|
|
23
|
+
* A health reason the spec ladder gives (a PM step owed: files, not a read) and an `unknown` reason (a source PMOS could not read) say
|
|
24
|
+
* nothing about status. A stale Jira reading (older than READ_DAYS) is not a read.
|
|
25
|
+
*
|
|
26
|
+
* x = { accepted, stopped: { state }, reasons: [{ level, code, text, source }] (lib/health.js), end ('YYYY-MM-DD', the planned finish),
|
|
27
|
+
* day (today, 'YYYY-MM-DD'), now (ms), jira: { fetchedAt, tickets: Map|[] of { key, status, category, due } } | null,
|
|
28
|
+
* stage: W1's { stage, setBy, source, readAt } | null, confirmed: { why, name } | null }
|
|
29
|
+
* Pure: no I/O, no clock beyond x.now.
|
|
30
|
+
*/
|
|
31
|
+
const STATUSES = Object.freeze(['on track', 'at risk', 'late', 'no signal', 'on hold', 'done', 'cancelled']);
|
|
32
|
+
const READ_DAYS = 14;
|
|
33
|
+
const DAY = 864e5;
|
|
34
|
+
const DATE_CODES = new Set(['milestone-overdue']); // a passed date: late
|
|
35
|
+
const NOT_READS = new Set(['pm-step-owed']); // the spec ladder (files): not a read
|
|
36
|
+
const DONE_CAT = /^done$/i;
|
|
37
|
+
|
|
38
|
+
const ticketsOf = (j) => (!j || !j.tickets ? [] : typeof j.tickets.values === 'function' && !Array.isArray(j.tickets) ? [...j.tickets.values()] : Array.isArray(j.tickets) ? j.tickets : []);
|
|
39
|
+
const doneTicket = (t) => DONE_CAT.test(String(t.category || '')) || /^(done|closed|resolved|won't do)$/i.test(String(t.status || ''));
|
|
40
|
+
/** When an open ticket was meant to be done: its own due date, else the end of the sprint it sits in (a closed sprint, or the active one). */
|
|
41
|
+
const dueOf = (t) => {
|
|
42
|
+
const sp = t.sprint && (t.sprint.state === 'active' || t.sprint.state === 'closed') ? t.sprint.end : null;
|
|
43
|
+
const m = /^\d{4}-\d{2}-\d{2}/.exec(String(t.due || t.duedate || sp || ''));
|
|
44
|
+
return m ? m[0] : null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The Jira reading, when it is a read: fresh, with tickets. → { at, open, late: [ticket] } | null */
|
|
48
|
+
function jiraRead(j, now) {
|
|
49
|
+
if (!j || !j.fetchedAt) return null;
|
|
50
|
+
const at = Date.parse(j.fetchedAt);
|
|
51
|
+
if (!Number.isFinite(at) || now - at > READ_DAYS * DAY) return null;
|
|
52
|
+
const T = ticketsOf(j);
|
|
53
|
+
if (!T.length) return null;
|
|
54
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
55
|
+
const open = T.filter((t) => !doneTicket(t));
|
|
56
|
+
return { at: j.fetchedAt, n: T.length, open: open.length, late: open.filter((t) => { const d = dueOf(t); return d && d < day; }), blocked: open.filter((t) => t.flagged) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function statusOf(x = {}) {
|
|
60
|
+
const now = typeof x.now === 'number' ? x.now : Date.now();
|
|
61
|
+
const day = x.day || new Date(now).toISOString().slice(0, 10);
|
|
62
|
+
const out = (status, from, why, source, readAt) => ({ status, from, why: why || '', source: source || '', readAt: readAt || null });
|
|
63
|
+
const st = x.stopped && x.stopped.state;
|
|
64
|
+
const S = x.stage || null;
|
|
65
|
+
// the PM's word
|
|
66
|
+
if (x.accepted || st === 'closed') return out('done', 'pm', '', 'the record');
|
|
67
|
+
if (st === 'cancelled') return out('cancelled', 'pm', '', 'lifecycle.jsonl');
|
|
68
|
+
if (st === 'on-hold') return out('on hold', 'pm', '', 'lifecycle.jsonl');
|
|
69
|
+
// shipped by the PM's word: never late or at risk, but still open on the Board (Monitor and Evaluate follow), so on track, not done
|
|
70
|
+
if (S && S.setBy === 'pm' && S.stage === 'shipped') return out('on track', 'pm', 'Shipped', S.source || 'lifecycle.jsonl', S.readAt);
|
|
71
|
+
const reasons = (Array.isArray(x.reasons) ? x.reasons : []).filter((r) => r && (r.level === 'off' || r.level === 'at-risk') && !NOT_READS.has(r.code));
|
|
72
|
+
// a passed date
|
|
73
|
+
if (x.end && x.end < day) return out('late', 'date', 'Planned finish ' + x.end + ' passed', 'planning/runs/plan-items.jsonl');
|
|
74
|
+
const dated = reasons.find((r) => DATE_CODES.has(r.code));
|
|
75
|
+
if (dated) return out('late', 'date', dated.text, dated.source);
|
|
76
|
+
// a risk the PM confirmed
|
|
77
|
+
if (x.confirmed) return out('at risk', 'confirmed', x.confirmed.why || 'You marked it at risk', x.confirmed.name ? 'Program milestone ' + x.confirmed.name : 'the program charter');
|
|
78
|
+
// a read
|
|
79
|
+
if (reasons.length) return out('at risk', 'read', reasons[0].text, reasons[0].source);
|
|
80
|
+
const J = jiraRead(x.jira, now) || (S && S.setBy === 'jira' && S.readAt && now - Date.parse(S.readAt) <= READ_DAYS * DAY ? { at: S.readAt, n: 0, open: 0, late: [], stage: S } : null);
|
|
81
|
+
if (J && J.late.length) return out('at risk', 'read', J.late.length === 1 ? (J.late[0].key || 'A ticket') + ' is past its due date' : J.late.length + ' tickets are past their due dates', 'Jira', J.at);
|
|
82
|
+
if (J && J.blocked && J.blocked.length) return out('at risk', 'read', (J.blocked[0].key || 'A ticket') + ' is blocked in Jira' + (J.blocked.length > 1 ? ', and ' + (J.blocked.length - 1) + ' more' : ''), 'Jira', J.at);
|
|
83
|
+
if (J) return out('on track', 'read', J.n ? (J.n - J.open) + ' of ' + J.n + ' tickets done' : (J.stage.source || 'Jira'), 'Jira', J.at);
|
|
84
|
+
if (x.end && x.end >= day) return out('on track', 'read', 'Plan items on dates, finish ' + x.end, 'planning/runs/plan-items.jsonl');
|
|
85
|
+
return out('no signal', null, 'Nothing read says how it is going', '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function wordsOf(c) {
|
|
89
|
+
if (!c || !c.open) return '';
|
|
90
|
+
const p = [c.late && c.late + ' late', c.risk && c.risk + ' at risk', c.on && c.on + ' on track', c.none && c.none + ' no signal', c.hold && c.hold + ' on hold'].filter(Boolean);
|
|
91
|
+
return p.join(' · ');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── the PM-confirmed risks: a program milestone the PM kept "at risk", on the initiatives it waits on ── */
|
|
95
|
+
const memberOf = (p, ws, ini) => (p.members || []).some((m) => m && m.initiative === ini && (!ws || !m.ws || m.ws === ws));
|
|
96
|
+
function confirmedRisks(programs, ws) {
|
|
97
|
+
const out = new Map();
|
|
98
|
+
for (const p of programs || []) {
|
|
99
|
+
if (!p || p.archived) continue;
|
|
100
|
+
for (const m of p.milestones || []) {
|
|
101
|
+
if (!m || !m.risk || m.done || m.fixed) continue;
|
|
102
|
+
for (const ini of m.waits || []) if (memberOf(p, ws, ini) && !out.has(ini)) out.set(ini, { why: String(m.risk), program: p.id, milestone: m.id, name: m.name || '' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Claude's "Mark … at risk" ✦ the PM has not answered, for the initiatives of workspace `ws`. */
|
|
108
|
+
function openRisks(programs, ws) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const p of programs || []) {
|
|
111
|
+
if (!p || p.archived) continue;
|
|
112
|
+
for (const i of (p.review && p.review.open) || []) {
|
|
113
|
+
if (!i || i.op !== 'risk') continue;
|
|
114
|
+
const m = (p.milestones || []).find((x) => x.id === i.milestone) || {};
|
|
115
|
+
const inis = (m.waits || []).filter((ini) => memberOf(p, ws, ini));
|
|
116
|
+
if (ws && !inis.length && !(p.members || []).some((x) => x && x.ws === ws)) continue;
|
|
117
|
+
out.push({ key: i.key, program: p.id, programTitle: p.title || p.id, title: 'Mark ' + (m.name || 'a milestone') + ' at risk', milestone: i.milestone, name: m.name || '', why: i.why, src: i.src || null, at: i.at || '', inis });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { STATUSES, READ_DAYS, statusOf, wordsOf, confirmedRisks, openRisks, jiraRead };
|
|
@@ -233,11 +233,13 @@ function ofLines(r) {
|
|
|
233
233
|
const row = r.row, st = row.wfStage || {};
|
|
234
234
|
const out = [`${row.title} (${row.id}) runs ${row.wf.name} (${row.wf.id}) on the board ${r.board.name}.`];
|
|
235
235
|
out.push(row.ended ? `It ended: ${row.ended.as === 'wontdo' ? "Won't do" : 'Archived'}.` : `Stage ${st.n} of ${st.of}: ${st.name} [${st.key}]`);
|
|
236
|
+
const one = row.oneStage; // W1: the one stage the pages show, and what set it
|
|
237
|
+
if (one && one.label) out.push(`The pages show: ${one.label} · set by ${one.source}${one.readAt ? ' · read ' + one.readAt : ''}`);
|
|
236
238
|
(row.wfStages || []).forEach((s, i) => out.push(` ${MARK[s.status] || ' '} ${String(i + 1).padStart(2)}. ${s.milestone ? '◆ ' : ''}${s.name} [${s.key}]${s.status === 'current' ? ' · current' : s.status === 'skipped' ? ' · skipped' : ''}`));
|
|
237
239
|
out.push(`Move it: pmos workflow move ${row.id} --stage <key>. Moving the initiative changes no task; a task's group (pmos task set --stage) is its place in the breakdown, not its state.`);
|
|
238
240
|
return out;
|
|
239
241
|
}
|
|
240
|
-
const ofJson = (r) => ({ initiative: { id: r.row.id, title: r.row.title }, board: { id: r.board.id, name: r.board.name }, wf: r.row.wf, stage: r.row.wfStage, stages: r.row.wfStages, ended: r.row.ended });
|
|
242
|
+
const ofJson = (r) => ({ initiative: { id: r.row.id, title: r.row.title }, board: { id: r.board.id, name: r.board.name }, wf: r.row.wf, stage: r.row.wfStage, stages: r.row.wfStages, ended: r.row.ended, oneStage: r.row.oneStage || null });
|
|
241
243
|
|
|
242
244
|
function onInitiative(verb, { pos, flags, json, say, fail, root, stateDir, actor, others }) {
|
|
243
245
|
const id = pos[0];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* your-move.js — what Your move needs from a product that its Board rows do not carry (workspace grill W3, frame WgW3Move; brief
|
|
3
|
+
* /mnt/project-files/workspace-grill/brief-W1-W4.md). Pure: lib/routes-your-move.js reads the files and passes them in.
|
|
4
|
+
*
|
|
5
|
+
* verdictsOf({ due, verdicts, items, today }) → [verdict] the outcome checks past their date with no verdict yet, oldest first
|
|
6
|
+
* krOf(okrFile) → { waiting, first: { token, title, need } } | null the first key result with no reading
|
|
7
|
+
*
|
|
8
|
+
* A VERDICT ROW. An outcome check is due (lib/outcomes.js scan: `due` = a metric, a check date on or before today, no disposition in
|
|
9
|
+
* outcome_checks.jsonl) and Intelligence holds no verdict for it (lib/insights-records.js). Claude drafts Keep, Iterate or Stop only from what
|
|
10
|
+
* it read, and the PM's ✓ records it (POST /api/insights-records/verdict, drafted: true). What it reads today is the initiative's Jira tickets on
|
|
11
|
+
* the board (each item carries the time Jira was read):
|
|
12
|
+
* - a ticket of type Bug still open → Iterate, naming the bugs
|
|
13
|
+
* - tickets read and no Bug open → Keep, and it says the metric itself was not read (the mood is never happier than the data)
|
|
14
|
+
* - no ticket read → no draft: "I could not read enough to judge", and ✎ opens the full verdict on Intelligence
|
|
15
|
+
* Stop is never drafted: it is the PM's call. Each draft carries its sources.
|
|
16
|
+
*
|
|
17
|
+
* A KEY RESULT ROW. lib/okr-goals.js build() lists the OKR file's key results with what each needs when nothing reads it. The row names the
|
|
18
|
+
* first one that waits on a source and how many wait; a file where every key result has a reading draws nothing (zero is silence). */
|
|
19
|
+
|
|
20
|
+
const DAY = 864e5;
|
|
21
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
22
|
+
const dn = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? Math.round(Date.UTC(+m[1], +m[2] - 1, +m[3]) / DAY) : null; };
|
|
23
|
+
const md = (iso, today) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); if (!m) return ''; const y = String(today || '').slice(0, 4); return +m[3] + ' ' + MON[+m[2] - 1] + (y && m[1] !== y ? ' ' + m[1] : ''); };
|
|
24
|
+
const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
|
|
25
|
+
const list = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
|
|
26
|
+
const OPEN = (s) => !/^(done|closed|resolved|released|won't do|archived)$/i.test(String(s || ''));
|
|
27
|
+
|
|
28
|
+
/** The drafted verdict for one initiative from its Jira tickets on the board, or null when nothing was read. */
|
|
29
|
+
function draftOf(ini, items, today) {
|
|
30
|
+
const tk = (items || []).filter((x) => x && x.src === 'jira' && x.ini === ini && x.kind !== 'Epic' && x.readAt);
|
|
31
|
+
if (!tk.length) return null;
|
|
32
|
+
const at = tk.map((x) => String(x.readAt).slice(0, 10)).sort().pop();
|
|
33
|
+
const src = [{ label: 'Jira, read ' + md(at, today), ref: tk.map((x) => x.key).slice(0, 6).join(', ') }];
|
|
34
|
+
const bugs = tk.filter((x) => x.kind === 'Bug' && OPEN(x.status));
|
|
35
|
+
if (bugs.length) {
|
|
36
|
+
const keys = bugs.map((x) => x.key);
|
|
37
|
+
return { verdict: 'Iterate', text: plural(bugs.length, 'bug is', 'bugs are') + ' still open in Jira: ' + list(keys.slice(0, 3)) + (keys.length > 3 ? ' and ' + (keys.length - 3) + ' more' : '') + '.', sources: src };
|
|
38
|
+
}
|
|
39
|
+
return { verdict: 'Keep', text: 'No bug is open among its ' + plural(tk.length, 'Jira ticket', 'Jira tickets') + ' (read ' + md(at, today) + '). I could not read the metric itself.', sources: src };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The checks past their date with no verdict: [{ ini, title, metric, horizon, days, draft }] oldest first. PURE.
|
|
43
|
+
* due: lib/outcomes.js scan().due; verdicts: Set of initiative ids Intelligence holds a verdict for; items: the board's work items. */
|
|
44
|
+
function verdictsOf({ due, verdicts, items, today }) {
|
|
45
|
+
const t = dn(today), have = verdicts instanceof Set ? verdicts : new Set(verdicts || []);
|
|
46
|
+
return (due || []).filter((it) => it && it.id && it.horizon && !have.has(it.id)).map((it) => ({
|
|
47
|
+
ini: it.id, title: it.title || it.id, metric: it.metric || '', horizon: it.horizon, days: t != null && dn(it.horizon) != null ? Math.max(0, t - dn(it.horizon)) : 0,
|
|
48
|
+
draft: draftOf(it.id, items, today),
|
|
49
|
+
})).sort((a, b) => b.days - a.days || a.ini.localeCompare(b.ini));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The first key result that waits on a source, and how many wait. PURE. okrFile: lib/okr-goals.js build(). */
|
|
53
|
+
function krOf(F) {
|
|
54
|
+
if (!F || !Array.isArray(F.objectives)) return null;
|
|
55
|
+
const wait = F.objectives.flatMap((o) => o.krs || []).filter((k) => k && !k.read && k.need && !k.proposed);
|
|
56
|
+
if (!wait.length) return null;
|
|
57
|
+
const k = wait[0];
|
|
58
|
+
return { waiting: wait.length, total: F.total || wait.length, read: F.read || 0, first: { token: k.token, title: k.title, need: k.need, goal: k.goal || null } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { verdictsOf, krOf, draftOf };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.527.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.527.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -26,6 +26,7 @@ import * as Asks from './bd-asks.js';
|
|
|
26
26
|
import * as WF from './bd-wf.js';
|
|
27
27
|
import * as Pins from './bd-pin-card.js';
|
|
28
28
|
import { holdSpans, holdWords, holdRange, md } from './bd-span.js';
|
|
29
|
+
import { stageName } from './stage-chip.js'; // W1: the one stage
|
|
29
30
|
|
|
30
31
|
const SV = (d) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${d}"/></svg>`;
|
|
31
32
|
const I = {
|
|
@@ -174,7 +175,7 @@ function findList() {
|
|
|
174
175
|
const h = findHits(findQ), all = [...h.ini.map((r) => ({ r })), ...h.tk];
|
|
175
176
|
findAt = Math.max(0, Math.min(findAt, all.length - 1));
|
|
176
177
|
let i = -1;
|
|
177
|
-
const ini = h.ini.map((r) => { i++; const s = V.statusLine(r); return `<button type="button" class="bd-mi${i === findAt ? ' hl' : ''}" data-findini="${esc(r.key)}"><i class="cz-dot ${s.tone}"></i><span class="tx"><span>${mark(r.title, findQ)}</span><span class="bd-amut">${esc([(r
|
|
178
|
+
const ini = h.ini.map((r) => { i++; const s = V.statusLine(r); return `<button type="button" class="bd-mi${i === findAt ? ' hl' : ''}" data-findini="${esc(r.key)}"><i class="cz-dot ${s.tone}"></i><span class="tx"><span>${mark(r.title, findQ)}</span><span class="bd-amut">${esc([stageName(r) || r.stage, s.text].filter(Boolean).join(' · '))}</span></span>${i === findAt ? '<span class="k">Enter</span>' : ''}</button>`; }).join('');
|
|
178
179
|
const tk = h.tk.map(({ k, r }) => { i++; return `<button type="button" class="bd-mi${i === findAt ? ' hl' : ''}" data-findtk="${esc(k.key)}" data-ini="${esc(r.key)}"><span class="bd-pill">${mark(k.key, findQ)}</span><span class="tx"><span>${mark(k.title, findQ)}</span><span class="bd-amut">${esc([k.blocked ? 'Blocked' : k.status, k.assignee].filter(Boolean).join(' · '))}</span></span>${i === findAt ? '<span class="k">Enter</span>' : ''}</button>`; }).join('');
|
|
179
180
|
return (ini ? '<p class="bd-mlbl">Initiatives</p>' + ini : '') + (tk ? '<p class="bd-mlbl">Work-items in Jira</p>' + tk : '') + (findQ.trim() && !ini && !tk ? `<p class="bd-amut bd-mnote">Nothing on this Board matches “${esc(findQ.trim())}”.</p>` : '');
|
|
180
181
|
}
|
|
@@ -13,6 +13,7 @@ import { esc, store } from './premium-shell.js';
|
|
|
13
13
|
import { endStage } from './bd-pins.js';
|
|
14
14
|
import { stageOfRow, wfOf, wfIcon, catOfRow, catLabel, CAT_ORDER } from './bd-wf.js';
|
|
15
15
|
import { offTone, md, dy } from './bd-span.js';
|
|
16
|
+
import { stageName as oneStageName } from './stage-chip.js';
|
|
16
17
|
|
|
17
18
|
/** Every column in its place, left to right: key, title, width in px. Initiative takes what the others leave of its 250 px. */
|
|
18
19
|
export const COLS = [
|
|
@@ -73,15 +74,17 @@ export function nextMs(r, now = new Date()) {
|
|
|
73
74
|
// a closed initiative (batch 26 item 5) is never late: its next milestone keeps its date and drops the days late
|
|
74
75
|
return r.closed ? { ...x, late: false, over: 0 } : { ...x, late: over > 0, over: Math.max(0, over) };
|
|
75
76
|
}
|
|
76
|
-
const STATUS = { late: 'Late', risk: 'At risk', q: 'On hold' };
|
|
77
|
+
const STATUS = { late: 'Late', risk: 'At risk', q: 'On hold', none: 'No signal' };
|
|
78
|
+
/* workspace grill W2: a row with nothing read says No signal, grey; on track stays empty (zero is silence) */
|
|
79
|
+
const stTone = (r, now) => offTone(r, now) || (r.status === 'no signal' ? 'none' : null);
|
|
77
80
|
const nextText = (r) => { const t = r.track || {}; if (typeof t.next === 'string') return t.next; if (t.next && t.next.text) return t.next.text; const l = (t.line || [])[0]; return l ? l.text || '' : ''; };
|
|
78
|
-
const stageName = (r) =>
|
|
81
|
+
const stageName = (r) => oneStageName(r) || ((s) => (s && s.name) || '')(stageOfRow(r)); // W1: the one stage
|
|
79
82
|
|
|
80
83
|
/** One cell's markup (never a <b>: the name is the row's only one). o: { now, avatar(name) } */
|
|
81
84
|
function cell(r, k, o) {
|
|
82
85
|
const now = o.now;
|
|
83
86
|
if (k === 'stage') return `<span class="tg-c">${esc(stageName(r))}</span>`;
|
|
84
|
-
if (k === 'status') { const t =
|
|
87
|
+
if (k === 'status') { const t = stTone(r, now); return t ? `<span class="tg-c tg-cst ${t === 'none' ? 'q' : t}"><i class="cz-dot ${t === 'none' ? 'q' : t}" aria-hidden="true"></i>${STATUS[t]}</span>` : '<span class="tg-c"></span>'; }
|
|
85
88
|
if (k === 'start') { const d = day(r.start); return `<span class="tg-c">${d ? esc(dy(d, now)) : ''}</span>`; }
|
|
86
89
|
if (k === 'ms') {
|
|
87
90
|
const m = nextMs(r, now);
|
|
@@ -99,7 +102,7 @@ function cell(r, k, o) {
|
|
|
99
102
|
function words(r, keys, now) {
|
|
100
103
|
return keys.map((k) => {
|
|
101
104
|
if (k === 'stage') return stageName(r);
|
|
102
|
-
if (k === 'status') { const t =
|
|
105
|
+
if (k === 'status') { const t = stTone(r, now); return t ? STATUS[t] : ''; }
|
|
103
106
|
if (k === 'start') return day(r.start) ? 'starts ' + md(day(r.start)) : '';
|
|
104
107
|
if (k === 'ms') { const m = nextMs(r, now); return m ? 'next ' + m.name + ' ' + md(m.d) + (m.late ? ', ' + m.over + (m.over === 1 ? ' day' : ' days') + ' late' : '') : ''; }
|
|
105
108
|
if (k === 'wf') return wfOf(r).name;
|
|
@@ -111,7 +114,7 @@ function words(r, keys, now) {
|
|
|
111
114
|
}).filter(Boolean).join(', ');
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
const tone = (r, now) => ({ late: 0, risk: 1, q: 2 })[
|
|
117
|
+
const tone = (r, now) => ({ late: 0, risk: 1, q: 2, none: 3 })[stTone(r, now)] ?? 4;
|
|
115
118
|
const str = (v) => String(v || '~');
|
|
116
119
|
const SORTS = {
|
|
117
120
|
title: (a, b) => String(a.title).localeCompare(String(b.title)),
|
|
@@ -39,6 +39,7 @@ import { openCharter as charterForm, renamed as charterRenamed } from './program
|
|
|
39
39
|
import * as MSG from './ms-suggest.js';
|
|
40
40
|
import * as MD from './metric-drafts.js'; // the goal Claude drafts when the charter has none (batch 15 item 1, canvas Iq9Program)
|
|
41
41
|
import * as DB from './dash-view.js'; // the page model's parts: the reports and the proposals (batch 17 item 1)
|
|
42
|
+
import { stageName } from './stage-chip.js'; // W1: the one stage
|
|
42
43
|
import * as PV from './prog-page.js'; // the one program page: the top and four tabs (batch 21 item 2, canvas RdProgL–RdProgP)
|
|
43
44
|
|
|
44
45
|
const SV = (d, w) => `<svg viewBox="0 0 24 24" width="${w || 18}" height="${w || 18}" aria-hidden="true"><path d="${d}"/></svg>`;
|
|
@@ -225,7 +226,7 @@ const TREE_TASKS = 3; // an initiative shows its first 3 open tasks in the tre
|
|
|
225
226
|
function iniNode(p, st, i, tasks) {
|
|
226
227
|
const k = 'i:' + p.id + ':' + i.id, r = i.row, t = r.track || {}, here = rowsOf(i.ws) && rowsOf(i.ws).here;
|
|
227
228
|
const name = here ? `<button type="button" class="x pp-open" data-pp="ini" data-ini="${esc(i.id)}" aria-haspopup="dialog">${esc(i.title)}</button>` : `<a class="x pp-open" href="${esc(boardHref(i.ws, i.id))}" title="Opens on its product's board">${esc(i.title)}</a>`;
|
|
228
|
-
const stg = t.label || (STEPS[i.at] && STEPS[i.at].label) || i.stage;
|
|
229
|
+
const stg = stageName(r) || t.label || (STEPS[i.at] && STEPS[i.at].label) || i.stage; // W1: the one stage
|
|
229
230
|
let kids = '';
|
|
230
231
|
if (!fold.has(k)) {
|
|
231
232
|
const you = youOf(i);
|
|
@@ -448,7 +449,7 @@ function textOf(p) {
|
|
|
448
449
|
const st = programState(p, rowsOf), s = summaryOf(p, st);
|
|
449
450
|
const lines = [p.title + (p.goal ? ' · ' + p.goal : ''), [s.line, s.meta].filter(Boolean).join(' · ')];
|
|
450
451
|
for (const m of st.milestones) lines.push('Milestone: ' + m.name + ' · ' + m.day + (m.done ? ' · done' : ''));
|
|
451
|
-
for (const i of st.inis) lines.push('- ' + i.title + ' · ' + ((i.row.track && i.row.track.label) || i.stage || ''));
|
|
452
|
+
for (const i of st.inis) lines.push('- ' + i.title + ' · ' + (stageName(i.row) || (i.row.track && i.row.track.label) || i.stage || ''));
|
|
452
453
|
return lines.join('\n');
|
|
453
454
|
}
|
|
454
455
|
const titleOf = (p) => p.title + (p.goal ? ' · ' + p.goal : '');
|
|
@@ -47,6 +47,7 @@ import { esc } from './premium-shell.js';
|
|
|
47
47
|
import { mergeClose } from './program-model.js';
|
|
48
48
|
import { stagesOfRow } from './bd-wf.js';
|
|
49
49
|
import { shownMs, endStage } from './bd-pins.js';
|
|
50
|
+
import { stageName } from './stage-chip.js'; // W1: the one stage
|
|
50
51
|
|
|
51
52
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
52
53
|
const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
@@ -566,7 +567,7 @@ function scaled(rows, M, o) {
|
|
|
566
567
|
}
|
|
567
568
|
const head = `<div class="tg-r tg-h"><div class="tg-n">${C ? C.head(back) : `<b>Initiative</b>${back}`}</div><div class="tg-p">${ticks}${pill}</div></div>`;
|
|
568
569
|
// one line per initiative (PgTimeline): its status dot and its name; the owner and the step stay in the row's title for a pointer and a reader
|
|
569
|
-
const one = (r, cls) => { const s = o.status(r), sub = [r.owner, r
|
|
570
|
+
const one = (r, cls) => { const s = o.status(r), sub = [r.owner, stageName(r), s.text].filter(Boolean).join(' · ');
|
|
570
571
|
// the dot keeps its place while it is silent, so every name starts at the same line (XcalmA1Desk)
|
|
571
572
|
const dot = dotHtml(r, now) || '<i class="tg-nodot" aria-hidden="true"></i>', nm = `<b>${r.inProgram && o.memberIcon ? o.memberIcon(r.inProgram) : ''}${esc(r.title)}</b>`;
|
|
572
573
|
// with columns, the Status cell carries the dot and the name has none (say it once)
|