create-pmos 0.145.0 → 0.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-sessions-panel-test.js +126 -0
- package/workspace/guide/briefing-cards.md +2 -2
- package/workspace/guide/briefing.md +19 -1
- package/workspace/guide/console-calendar.md +18 -3
- package/workspace/guide/console3-sessions.md +35 -0
- package/workspace/guide/data-freshness.md +7 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/lib/briefing-real.js +264 -0
- package/workspace/lib/calendar-layers.js +5 -2
- package/workspace/lib/freshness.js +4 -1
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/routes-briefing.js +25 -5
- package/workspace/lib/routes-calendar-views.js +24 -2
- package/workspace/lib/routes-runs.js +12 -4
- package/workspace/lib/tracker-cli.js +68 -0
- package/workspace/lib/tracker-read.js +40 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-model.js +12 -1
- package/workspace/public/pm/c3-brief-needs.js +16 -4
- package/workspace/public/pm/c3-brief-pulse.js +35 -7
- package/workspace/public/pm/c3-calendar.js +162 -25
- package/workspace/public/pm/c3-case.js +7 -2
- package/workspace/public/pm/c3-demo-cal4.js +3 -3
- package/workspace/public/pm/c3-dock.js +2 -1
- package/workspace/public/pm/c3-session-term.js +44 -0
- package/workspace/public/pm/c3-sheet.js +3 -2
- package/workspace/public/pm/c3-shell.js +2 -1
- package/workspace/public/pm/c3-terminal-data.js +1 -1
- package/workspace/public/pm/c3-terminal-icons.js +1 -1
- package/workspace/public/pm/console3.css +24 -0
- package/workspace/public/pm/run-card.js +4 -1
- package/workspace/public/pm/sessions-panel.js +40 -4
- package/workspace/skills/tracker-refresh.skill +2 -1
- package/workspace/test-files.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.147.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T02:42:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T02:42:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T02:42:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-sessions-panel-test.js — the console3 Sessions panel (Agent surfaces): ending a session and opening one (initiative sessions-panel-end-and-open).
|
|
4
|
+
*
|
|
5
|
+
* Starts a real server with its own state directory. Before it starts, a session record is written whose process does not exist and
|
|
6
|
+
* whose file still says it runs: the server restores it as exited, and GET /api/runs must say it ended (it read "starting · 4 d" before).
|
|
7
|
+
* Then one live shell is started, and the page checks: the live row reads the server's state, not "starting"; Open the session draws the
|
|
8
|
+
* session's real terminal in a sheet on /c3.html and the address does not change; End asks first, Keep it keeps it, End ends it.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const { WebSocket } = require('ws');
|
|
14
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
15
|
+
const T = require('./test-run').configure({ name: 'c3-sessions-panel' });
|
|
16
|
+
|
|
17
|
+
const BASE = T.BASE;
|
|
18
|
+
const results = [];
|
|
19
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
|
|
20
|
+
let srv = null; let browser = null;
|
|
21
|
+
const finish = async (code) => {
|
|
22
|
+
if (browser) await browser.close().catch(() => {});
|
|
23
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
24
|
+
T.killTmux();
|
|
25
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
26
|
+
console.log(`\n${results.length - bad}/${results.length} c3-sessions-panel assertions passed`);
|
|
27
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
28
|
+
};
|
|
29
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
30
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
async function until(fn, ms = 8000) { const end = Date.now() + ms; while (Date.now() < end) { const v = await fn(); if (v) return v; await sleep(150); } return null; }
|
|
32
|
+
async function waitFor(page, fn, arg, ms = 15000) { try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; } }
|
|
33
|
+
const get = (p) => fetch(BASE + p).then((r) => r.json());
|
|
34
|
+
|
|
35
|
+
function startShell(name) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const ws = new WebSocket(`ws://${T.HOST}:${T.PORT}`); const inbox = [];
|
|
38
|
+
ws.on('message', (raw) => { try { inbox.push(JSON.parse(raw)); } catch { /* ignore */ } });
|
|
39
|
+
ws.on('error', () => resolve(null));
|
|
40
|
+
ws.on('open', async () => {
|
|
41
|
+
ws.send(JSON.stringify({ t: 'new', agent: 'shell', name }));
|
|
42
|
+
const m = await until(() => inbox.find((x) => x.t === 'created' || x.t === 'error'), 8000);
|
|
43
|
+
ws.close(); resolve(m && m.t === 'created' ? m.id : null);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
(async () => {
|
|
49
|
+
const exe = findBrowser();
|
|
50
|
+
if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-sessions-panel suite cannot run'); return finish(1); }
|
|
51
|
+
let chromium;
|
|
52
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-sessions-panel suite cannot run'); return finish(1); }
|
|
53
|
+
|
|
54
|
+
/* a record left by a server that stopped: the file says it runs, its process is gone (no tmux session by that name) */
|
|
55
|
+
const sessDir = path.join(T.STATE_DIR, 'sessions');
|
|
56
|
+
fs.mkdirSync(sessDir, { recursive: true });
|
|
57
|
+
const created = new Date(Date.now() - 4 * 864e5).toISOString();
|
|
58
|
+
fs.writeFileSync(path.join(sessDir, 'sess-900.json'), JSON.stringify({ id: 'sess-900', name: 'stale claude', agent: 'shell', state: 'working', created, tmux: 'pmos-gone-900', origin: 'manual', cwd: path.resolve(__dirname, '..'), root: path.resolve(__dirname, '..') }));
|
|
59
|
+
fs.writeFileSync(path.join(sessDir, 'sess-900.scrollback'), '');
|
|
60
|
+
|
|
61
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
62
|
+
await T.waitForServer(BASE);
|
|
63
|
+
|
|
64
|
+
/* ── A: /api/runs believes the server, not a stale file ── */
|
|
65
|
+
const r0 = await get('/api/runs');
|
|
66
|
+
const inGroup = (r, g, id) => ((r.groups && r.groups[g]) || []).some((s) => s.id === id);
|
|
67
|
+
ok('A1 a session whose process is gone is not listed as started by me', r0.ok && !inGroup(r0, 'startedByMe', 'sess-900'), JSON.stringify(r0.groups && r0.groups.startedByMe));
|
|
68
|
+
ok('A2 it is listed in Past', inGroup(r0, 'past', 'sess-900'));
|
|
69
|
+
|
|
70
|
+
const id = await startShell('panel shell');
|
|
71
|
+
ok('A3 a live shell starts', !!id);
|
|
72
|
+
const r1 = await until(async () => { const r = await get('/api/runs'); return inGroup(r, 'startedByMe', id) ? r : null; });
|
|
73
|
+
const row = r1 && r1.groups.startedByMe.find((s) => s.id === id);
|
|
74
|
+
ok('A4 the live shell is started by me and carries the server state', !!row && typeof row.label === 'string' && row.label.length > 0, JSON.stringify(row));
|
|
75
|
+
|
|
76
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
77
|
+
const page = await (await browser.newContext({ viewport: { width: 1440, height: 1000 } })).newPage();
|
|
78
|
+
const errors = [];
|
|
79
|
+
page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
|
|
80
|
+
|
|
81
|
+
/* ── B: the panel ── */
|
|
82
|
+
await page.goto(BASE + '/c3.html');
|
|
83
|
+
ok('B1 the header has the Sessions button', await waitFor(page, () => !!document.querySelector('[data-c3pg="agent-button"]'), null, 45000));
|
|
84
|
+
await page.click('[data-c3pg="agent-button"]');
|
|
85
|
+
const sel = `[data-c3t="mine"] [data-run="${id}"]`;
|
|
86
|
+
ok('B2 the live shell has a row under Started by me', await waitFor(page, (s) => !!document.querySelector(s), sel));
|
|
87
|
+
const sub = await page.evaluate((s) => (document.querySelector(s + ' .chips') || {}).textContent || '', sel);
|
|
88
|
+
ok('B3 the row does not say starting', !!sub && !/^starting/.test(sub.trim()), sub);
|
|
89
|
+
await page.click(sel + ' .nb');
|
|
90
|
+
ok('B3b a plain session draws no empty "Starting" card', await page.evaluate((s) => !document.querySelector(s + ' [data-run-host]'), sel));
|
|
91
|
+
ok('B4 the open row has End the session', await waitFor(page, (i) => !!document.querySelector(`[data-end-session="${i}"]`), id));
|
|
92
|
+
|
|
93
|
+
/* ── C: Open the session stays on this page ── */
|
|
94
|
+
const url0 = page.url();
|
|
95
|
+
await page.click(`[data-blank-panel="${id}"] [data-open-session]`);
|
|
96
|
+
ok('C1 the session opens in a terminal sheet', await waitFor(page, (i) => !!document.querySelector(`[data-c3-session-term="${i}"] .xterm`), id));
|
|
97
|
+
ok('C2 the page stays on console3', page.url() === url0, page.url());
|
|
98
|
+
await sleep(800);
|
|
99
|
+
await page.click(`[data-c3-session-term="${id}"] .xterm`);
|
|
100
|
+
await page.keyboard.type('echo c3-typed-$((40+2))\n');
|
|
101
|
+
ok('C2b what is typed in the sheet reaches the shell', !!(await until(async () => /c3-typed-42/.test((await get('/api/sessions/transcript?id=' + id + '&max=4000')).text || ''), 8000)));
|
|
102
|
+
await page.keyboard.press('Escape');
|
|
103
|
+
await sleep(300);
|
|
104
|
+
ok('C3 Esc in the terminal goes to the shell, not the sheet', await page.evaluate((i) => !!document.querySelector(`[data-c3-session-term="${i}"]`), id));
|
|
105
|
+
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
106
|
+
ok('C4 the close button closes the terminal sheet', await waitFor(page, () => !document.querySelector('[data-c3-session-term]')));
|
|
107
|
+
ok('C5 closing the sheet leaves the session running', await until(async () => ((await get('/api/sessions')).sessions || []).some((s) => s.id === id && s.state !== 'exited')));
|
|
108
|
+
|
|
109
|
+
/* ── D: End asks first, then ends ── */
|
|
110
|
+
await page.click('[data-c3pg="agent-button"]');
|
|
111
|
+
ok('D0 the panel opens again', await waitFor(page, (s) => !!document.querySelector(s), sel));
|
|
112
|
+
if (!(await page.isVisible(`[data-end-session="${id}"]`).catch(() => false))) await page.click(sel + ' .nb');
|
|
113
|
+
await page.click(`[data-end-session="${id}"]`);
|
|
114
|
+
ok('D1 End asks first', await waitFor(page, (i) => !!document.querySelector(`[data-end-confirm="${i}"]`), id));
|
|
115
|
+
await page.click(`[data-end-confirm="${id}"] button:not([data-end-yes])`);
|
|
116
|
+
ok('D2 Keep it keeps the session', await waitFor(page, (i) => !document.querySelector(`[data-end-confirm="${i}"]`), id) && ((await get('/api/sessions')).sessions || []).some((s) => s.id === id));
|
|
117
|
+
await page.click(`[data-end-session="${id}"]`);
|
|
118
|
+
await page.click(`[data-end-yes="${id}"]`);
|
|
119
|
+
const gone = async () => !((await get('/api/sessions')).sessions || []).some((s) => s.id === id);
|
|
120
|
+
if (!(await until(gone, 6000)) && await page.$(`[data-end-session="${id}"]`)) { await page.click(`[data-end-session="${id}"]`); await page.click(`[data-end-yes="${id}"]`); } // "asked it to end": the second press removes it
|
|
121
|
+
ok('D3 End ends the session', !!(await until(gone, 8000)));
|
|
122
|
+
ok('D4 its row leaves Started by me', await waitFor(page, (s) => !document.querySelector(s), sel));
|
|
123
|
+
|
|
124
|
+
ok('E1 the page threw no error', errors.length === 0, errors.join(' | '));
|
|
125
|
+
return finish();
|
|
126
|
+
})().catch((e) => { console.error(e); finish(1); });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Briefing: cards, time of day, Needs you and Portfolio pulse"
|
|
4
4
|
description: "How the Briefing cards keep one height and flip in place, how to confirm an act on a large screen, how the day part follows the clock, how a Portfolio pulse row opens inline, and how overdue items reach Needs you."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Briefing: cards, time of day, Needs you and Portfolio pulse
|
|
@@ -17,7 +17,7 @@ The tabs show counts: Briefing shows how many things need you, Work shows its in
|
|
|
17
17
|
|
|
18
18
|
### Kinds of card
|
|
19
19
|
|
|
20
|
-
The label on a card says what kind of thing happened, for example **Deadline moved**, **Promise out of date**, **Blocked**, **Conflict**, **Risk**, **Waiting on someone** or **Report due**. A win keeps the label **Big win** or **Small win**. The colour of the card shows how serious it is: red is critical, amber is a warning, green is good news, and the accent colour is neutral.
|
|
20
|
+
The label on a card says what kind of thing happened, for example **Deadline moved**, **Promise out of date**, **Blocked**, **Conflict**, **Risk**, **Waiting on someone** or **Report due**. A win keeps the label **Big win** or **Small win**. A card from your own data has the label **Overdue**, **Due today**, **Blocked**, **Worth asking** or **For you today** (see The Briefing: today's brief). The colour of the card shows how serious it is: red is critical, amber is a warning, green is good news, and the accent colour is neutral.
|
|
21
21
|
|
|
22
22
|
Each kind has a short list of what its card must say. For example, a **Deadline moved** card says what moved, by how much, by when you must decide and the one move. When the card does not know a part, the back of the card says so under **Not known yet**. PMOS does not guess a missing part.
|
|
23
23
|
|
|
@@ -17,6 +17,16 @@ The Briefing shows only what PMOS already holds. When PMOS holds nothing for a s
|
|
|
17
17
|
|
|
18
18
|
The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
|
|
19
19
|
|
|
20
|
+
The cards come from two places:
|
|
21
|
+
|
|
22
|
+
- **Your own data.** PMOS makes a card from each of these records:
|
|
23
|
+
- A Jira ticket or a plan (WBS) item assigned to you that is overdue, due today or blocked. "Blocked" means that the item is flagged, or that its status says blocked or on hold. The tickets come from the last My work read.
|
|
24
|
+
- An item in your Work list that is overdue or due today. An item that you parked, put on Waiting or moved to Later does not get a card.
|
|
25
|
+
- An item of today's morning brief that is worth asking, and an item of today's brief that is for you.
|
|
26
|
+
- **The signal engine.** It reads the initiative records of the repository, for example a spec written after the build.
|
|
27
|
+
|
|
28
|
+
Overdue cards come first, the most late first. The signal engine's cards come next. Then come the blocked, due today and brief cards. PMOS adds at most 5 cards from your own data. A card shows only the values that its record holds. When the record has a link, for example a Jira ticket, the case of the card has a link to it. The link opens in a new tab. When no record gives a card, the deck says that nothing needs you.
|
|
29
|
+
|
|
20
30
|
Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none. The part of the day follows the clock. There is no button to change it. When the clock moves to a new part, the line under the greeting and the order of the cards change after you stop using the page for a minute.
|
|
21
31
|
|
|
22
32
|
## Today
|
|
@@ -41,6 +51,14 @@ Needs you lists what only you can do. Its header says how many decisions, how ma
|
|
|
41
51
|
3. Items outside PMOS that wait for you to confirm: talk to someone, meet, call, ask.
|
|
42
52
|
4. Decisions that only you can make.
|
|
43
53
|
|
|
54
|
+
The decisions come from these records:
|
|
55
|
+
|
|
56
|
+
- A Jira ticket or a plan item assigned to you that is overdue, blocked, due today, or marked as your decision. Its row has the ticket key, and its case has a link to the ticket.
|
|
57
|
+
- A signal engine card that has a suggested move, or is Critical.
|
|
58
|
+
- An item of today's morning brief that is worth asking. The brief was not sure enough to act, so ask first.
|
|
59
|
+
|
|
60
|
+
An item that is in your own items above does not show again as a decision. **Done** and **Later** on a ticket row change the item in your Work list. They do not change Jira.
|
|
61
|
+
|
|
44
62
|
Press a row to open it in place. A row of your own item has three actions. Each one writes to the item itself, the same record the Work page shows.
|
|
45
63
|
|
|
46
64
|
- **Done** marks the item done. For an item outside PMOS you can first type one line about what happened. The line is optional.
|
|
@@ -51,7 +69,7 @@ PMOS can see evidence that an outside item is done. For example, a meeting with
|
|
|
51
69
|
|
|
52
70
|
## Portfolio pulse
|
|
53
71
|
|
|
54
|
-
Portfolio pulse says how many initiatives need attention, then shows one bar for the status of the initiatives and one row for each initiative. **All initiatives** at the bottom opens the Initiatives page. Press a row to open it in place. It shows why the initiative has that status,
|
|
72
|
+
Portfolio pulse says how many initiatives need attention, then shows one bar for the status of the initiatives and one row for each initiative. An initiative shows when it has a promised date, or when PMOS holds a read of its Jira epic. A row from a Jira epic shows "3 of 8 done" and a bar. "Done" counts the tickets under the epic that are done in Jira. A row with blocked tickets says **Blocked**. The Jira read is the last Jira status check; the opened row says its date. When no initiative has a promised date or a Jira read, the pulse says so. **All initiatives** at the bottom opens the Initiatives page. Press a row to open it in place. It shows why the initiative has that status, the promised date against the forecast, and the Jira progress when PMOS holds it. The page does not change.
|
|
55
73
|
|
|
56
74
|
To go to the initiative, press **Open initiative** inside the opened row.
|
|
57
75
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: the Calendar tab of the console page"
|
|
4
|
-
description: "The Calendar tab of the console page (/c3.html): Calendar and Notes, the five zooms, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
4
|
+
description: "The Calendar tab of the console page (/c3.html): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -47,12 +47,27 @@ The chips under the zoom bar are the layers. Each chip shows the number of items
|
|
|
47
47
|
|
|
48
48
|
All layers and Free time are on at the start. Tidy-up is off.
|
|
49
49
|
|
|
50
|
+
On Month, Quarter and Year there is one more chip, **Routine**. It shows the number of routine items in the range: report drafts, reports that go out daily or weekly, recurring meetings, stand-ups, 1:1s and due items on track. These items are counted, not listed. Press **Routine** to show every item as a marker, and press it again to go back to the highlights.
|
|
51
|
+
|
|
52
|
+
## Highlights
|
|
53
|
+
|
|
54
|
+
Every zoom starts with a short summary above the grid, for example "October highlights". It names what matters in the range:
|
|
55
|
+
|
|
56
|
+
- the releases, and the milestones that are on track
|
|
57
|
+
- the key meetings, on Day, Week and Month only. A meeting is key when it is not routine. Month names the two biggest.
|
|
58
|
+
- the reports that go out
|
|
59
|
+
- **Watch:** the milestones and releases that are late or at risk, and the due items that are late
|
|
60
|
+
|
|
61
|
+
Press a name to open the item in the detail column. When nothing significant falls in the range, the summary says so.
|
|
62
|
+
|
|
50
63
|
## The grid
|
|
51
64
|
|
|
52
65
|
- **Day** shows the timeline of the day. The markers of the day are in a strip above it.
|
|
53
66
|
- **Week** shows a timeline for each day. Each day name has the hours of meetings of that day and a small bar. The bar has three parts: time tied to a promise or a decision, routine time, and the rest of a 6 hour day. The markers of each day are in a strip under the day name. Press a day name to open that day.
|
|
54
|
-
- **Month** shows a cell for each day. A cell shows the
|
|
55
|
-
- **Quarter** shows a
|
|
67
|
+
- **Month** shows a cell for each day. A cell shows the highlight of the day in one sentence, for example "SSO general release ships." Under it, a quiet line counts the rest, for example "1 due · 2 routine". Press that line or the day number to open the day. A line under the grid says what is counted and not named. With **Routine** on, a cell shows the number of meetings and up to three markers instead, and "+2" opens the day with the other markers.
|
|
68
|
+
- **Quarter** shows a column for each month. **Year** shows a column for each quarter, with a button for each of its three months. Each column shows its highlight and a quiet line, for example "24 meetings · 3 due · 9 routine". Quarter and Year are stricter than Month: they name only milestones, releases, reports that go out and late items. Press a column's title to open that month or quarter. With **Routine** on, a column shows its markers instead.
|
|
69
|
+
|
|
70
|
+
Titles wrap onto more lines. They are never cut.
|
|
56
71
|
|
|
57
72
|
A marker has a symbol and a short title. A red edge means late or at risk of missing. An amber edge means at risk.
|
|
58
73
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Sessions in console3: open, read and end your sessions"
|
|
4
|
+
description: "How the Sessions panel in console3 groups your sessions, how to open a session's terminal without leaving console3, and how to end a session."
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Sessions in console3: open, read and end your sessions
|
|
9
|
+
|
|
10
|
+
Press **Sessions** in the console3 header to open the panel. It shows your sessions in four groups: **On a card**, **Started by me**, **Past (7 days)** and **Automatic**.
|
|
11
|
+
|
|
12
|
+
## Read a row
|
|
13
|
+
|
|
14
|
+
Each row under **Started by me** shows the state of the session, how long ago it started, and its tree. The state comes from the workspace server, for example `working` or `quiet`.
|
|
15
|
+
|
|
16
|
+
If the server finds that the process of a session stopped (for example, after a restart), the session moves to **Past**.
|
|
17
|
+
|
|
18
|
+
## Open a session
|
|
19
|
+
|
|
20
|
+
1. Press the row to open it.
|
|
21
|
+
2. Press **Open the session**.
|
|
22
|
+
|
|
23
|
+
The terminal of the session opens in a panel on the same page. You can type in it. When the terminal has focus, Esc and Tab go to the terminal. To close the panel, press its close button. The session continues to run.
|
|
24
|
+
|
|
25
|
+
The **Open the session** link in the sessions dock opens the same panel.
|
|
26
|
+
|
|
27
|
+
## End a session
|
|
28
|
+
|
|
29
|
+
1. Press the row to open it.
|
|
30
|
+
2. Press **End the session**.
|
|
31
|
+
3. Press **End** to confirm, or press **Keep it** to cancel.
|
|
32
|
+
|
|
33
|
+
When you end a session, its process stops and the session is removed. Any pending change stays a proposal. If the process does not stop immediately, press **End** again to remove it.
|
|
34
|
+
|
|
35
|
+
A session that is on a card has the same **End the session** control below its card.
|
|
@@ -35,6 +35,13 @@ The calendar-sync skill reads today and the next 14 days, read-only, and saves t
|
|
|
35
35
|
|
|
36
36
|
The events are kept in your own runtime folder on this machine, never in a repository.
|
|
37
37
|
|
|
38
|
+
The tracker-refresh skill reads tracker items from files already in your repository. It moves an item only when a file shows a new status, so a read can change nothing. Its last step records that the read finished, with this command:
|
|
39
|
+
|
|
40
|
+
- `pmos tracker done [--moved N] [--left N] [--not-found N]` records the time of the read for this repository and the counts of what it found. It moves no item.
|
|
41
|
+
- `pmos tracker status` says when tracker items were last read for this repository.
|
|
42
|
+
|
|
43
|
+
The record is kept in your own runtime folder on this machine, never in a repository. Tracker items show the time of this record, or the time the schedule last started the skill if that is later.
|
|
44
|
+
|
|
38
45
|
## Where you see it
|
|
39
46
|
|
|
40
47
|
Each page shows a short line under its title that says when each source was last read, for example **Initiatives read 08:02 · Calendar read 07:15 · Jira status read 06:31**. The line never says that the data is up to date, because the agents read these sources on a schedule and not live.
|
package/workspace/guide/index.md
CHANGED
|
@@ -69,4 +69,5 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
69
69
|
- [Integrated work](integrated-work.md): how `pmos verify-work` turns a merged commit into Done, and what "Done · your call · not merged" means.
|
|
70
70
|
- [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
|
|
71
71
|
- [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit and share one as a draft, and the Drafts tab.
|
|
72
|
+
- [Sessions in console3](console3-sessions.md): the Sessions panel, how to open the terminal of a session on the same page, and how to end a session.
|
|
72
73
|
- [Settings in console3](settings.md): how to open Settings, what each section holds, the scope tags, the environment notes, and where the settings are kept.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* lib/briefing-real.js — the Briefing's cards, "Needs you" rows and pulse progress from the PM's own data (initiative briefing-real-data).
|
|
3
|
+
*
|
|
4
|
+
* build({ brief, myWork, itemLines, today }) → { lead, tail, needs }
|
|
5
|
+
* lead the overdue cards (most days late first); the route puts them before the engine's cards
|
|
6
|
+
* tail the blocked, due today, worth asking and brief priority cards, after the engine's cards
|
|
7
|
+
* lead + tail hold at most MAX_CARDS cards together (R4)
|
|
8
|
+
* needs "Needs you" rows: overdue, blocked, decision and due today assigned items, then the brief's worth asking items (R6)
|
|
9
|
+
* merge(engineCards, built) → cards lead, then the engine's cards unchanged, then tail
|
|
10
|
+
* engineNeeds(cards) → rows the engine's cards that carry a move or are Critical, as needs rows ({ card })
|
|
11
|
+
* jiraProgress(snapshot) → { done, total, blocked, epics, as_of } | null one initiative's Jira epic progress (R7)
|
|
12
|
+
* readSources(ctx, now, today) → { brief, myWork, itemLines } what the route reads; a source that fails gives nothing
|
|
13
|
+
*
|
|
14
|
+
* The sources: today's morning brief (<runtime>/briefs/<date>.json: `priorities` and `worth_asking`), GET /api/my-work items (Jira
|
|
15
|
+
* tickets and WBS items assigned to the PM), and the PM's Work ledger (<runtime>/pm-items/items.jsonl). A card or row is made only from
|
|
16
|
+
* a record, and a field is set only when the record holds its value. The ledger wins for an assigned item it holds: a done or dropped
|
|
17
|
+
* copy gives no card, and its due date, parking and waiting are the PM's. An assigned row carries its item id, so Done and Later work
|
|
18
|
+
* through the pm-items routes (they take a jira:, wbs: or brief: id on its first press).
|
|
19
|
+
*
|
|
20
|
+
* PURE except readSources. No network, no write.
|
|
21
|
+
*/
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const pmItems = require('./pm-items.js');
|
|
24
|
+
|
|
25
|
+
const MAX_CARDS = 5;
|
|
26
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
27
|
+
const BLOCKED_RE = /block|flag|imped|on hold/i;
|
|
28
|
+
const SOURCE_WORD = { calendar: 'Calendar', wispr: 'Wispr Flow', mail: 'Mail', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Drive', figma: 'Figma', website: 'Website', derived: 'PMOS' };
|
|
29
|
+
const EFFORT_WORD = { minutes: 'minutes', 'an-hour': 'an hour', 'half-day': 'half a day', day: 'a day' };
|
|
30
|
+
const COLOR = { bad: 'var(--destructive)', warn: 'var(--warning)', acc: 'var(--acc)', ink: 'var(--foreground)' };
|
|
31
|
+
|
|
32
|
+
const dayNo = (s) => Math.round(Date.parse(s + 'T00:00:00Z') / 864e5);
|
|
33
|
+
const isDate = (s) => DATE_RE.test(String(s || ''));
|
|
34
|
+
const clip = (s, n) => { const t = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1).trimEnd() + '…' : t; };
|
|
35
|
+
const httpsUrl = (v) => { const s = String(v || ''); try { return new URL(s).protocol === 'https:' && s.length <= 500 ? s : null; } catch { return null; } };
|
|
36
|
+
const daysWord = (n) => (n === 1 ? '1 day' : n + ' days');
|
|
37
|
+
const lateText = (n) => (n > 0 ? daysWord(n) + ' overdue' : 'due today');
|
|
38
|
+
|
|
39
|
+
/** Blocked: the item carries `blocked`, or its status says blocked, flagged, impediment or on hold. */
|
|
40
|
+
function blockedOf(x) {
|
|
41
|
+
if (x.blocked) return x.blocked && typeof x.blocked === 'object' && x.blocked.reason ? clip(x.blocked.reason, 120) : 'Blocked';
|
|
42
|
+
if (x.status && BLOCKED_RE.test(String(x.status))) return 'Status: ' + clip(x.status, 60);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An open item's place in time: days late (0 is due today), or null when it has no due date or is not due yet. */
|
|
47
|
+
function lateBy(due, today) {
|
|
48
|
+
if (!isDate(due)) return null;
|
|
49
|
+
const n = dayNo(today) - dayNo(String(due).slice(0, 10));
|
|
50
|
+
return n >= 0 ? n : null;
|
|
51
|
+
}
|
|
52
|
+
/** The PM set it aside: parked, waiting on someone, moved to Later, or not before a later date. */
|
|
53
|
+
const setAside = (i, today) => !!(i.parked || i.waiting_on || i.list_override === 'later' || (isDate(i.defer) && i.defer > today));
|
|
54
|
+
|
|
55
|
+
/* ── the assigned items (GET /api/my-work), with the ledger's copy when it holds one ───────────────────────────── */
|
|
56
|
+
function assignedOf(myWork, ledger, held, today) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const x of Array.isArray(myWork) ? myWork : []) {
|
|
59
|
+
if (!x || !x.id || !x.title || x.state === 'done' || x.category === 'done') continue;
|
|
60
|
+
let a = x;
|
|
61
|
+
if (held.has(x.id)) {
|
|
62
|
+
const L = ledger.get(x.id);
|
|
63
|
+
if (!L || L.done) continue; // dropped or done in Work: the PM's answer stands
|
|
64
|
+
if (setAside(L, today)) continue;
|
|
65
|
+
a = { ...x, due: L.due !== undefined ? L.due : x.due };
|
|
66
|
+
}
|
|
67
|
+
const late = lateBy(a.due, today);
|
|
68
|
+
const blocked = blockedOf(a);
|
|
69
|
+
const decide = a.kind === 'decide' && (a.state === 'brief' || a.state === 'yours');
|
|
70
|
+
if (late === null && !blocked && !decide) continue;
|
|
71
|
+
out.push({ a, late, blocked, decide });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assignedLabel(a) {
|
|
77
|
+
return a.source === 'wbs' ? 'WBS ' + a.key + (a.repo ? ' in ' + a.repo : '') : 'Jira ' + a.key;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assignedCard(e, today) {
|
|
81
|
+
const { a, late, blocked } = e;
|
|
82
|
+
const jira = a.source !== 'wbs';
|
|
83
|
+
const reason = late > 0 ? 'overdue' : blocked ? 'blocked' : 'due';
|
|
84
|
+
const kpis = [];
|
|
85
|
+
if (late > 0) kpis.push([late + ' d', 'overdue', COLOR.bad]);
|
|
86
|
+
else if (late === 0) kpis.push(['today', 'due', COLOR.warn]);
|
|
87
|
+
if (blocked) kpis.push(['blocked', 'in ' + (jira ? 'Jira' : 'the plan'), COLOR.warn]);
|
|
88
|
+
if (a.wsjf && /^P[1-4]$/.test(String(a.wsjf.tier))) kpis.push([a.wsjf.tier, 'WSJF tier', COLOR.acc]);
|
|
89
|
+
if (a.blocks > 0) kpis.push([String(a.blocks), a.blocks === 1 ? 'item waits on it' : 'items wait on it', COLOR.warn]);
|
|
90
|
+
const facts = [[jira ? 'inbox' : 'repo', assignedLabel(a) + (a.status ? ' · ' + clip(a.status, 40) : '')]];
|
|
91
|
+
if (isDate(a.due)) facts.push(['clock', 'Due ' + String(a.due).slice(0, 10)]);
|
|
92
|
+
if (blocked) facts.push(['alert', blocked]);
|
|
93
|
+
else if (a.from) facts.push(['user', 'From ' + clip(a.from, 60)]);
|
|
94
|
+
const sug = a.suggest ? clip(a.suggest, 120) : null;
|
|
95
|
+
return {
|
|
96
|
+
key: 'real:' + a.id, origin: jira ? 'jira' : 'wbs', itemId: a.id,
|
|
97
|
+
tag: reason === 'due' ? 'Just in' : 'Critical', tone: reason === 'overdue' ? 'critical' : 'warning',
|
|
98
|
+
kind: reason === 'overdue' ? 'Overdue' : reason === 'blocked' ? 'Blocked' : 'Due today',
|
|
99
|
+
headline: clip(a.title, 140), sub: assignedLabel(a) + ' · assigned to you',
|
|
100
|
+
hero: { kind: 'big-number' }, kpis: kpis.slice(0, 3), factsX: facts.slice(0, 3), facts: [],
|
|
101
|
+
deadline: late !== null ? { days: -late, source: jira ? 'jira' : 'wbs', text: lateText(late) } : null,
|
|
102
|
+
why: { text: reason === 'overdue' ? 'Assigned to you and past its due date.' : reason === 'blocked' ? 'Assigned to you and blocked: ' + blocked + '.' : 'Assigned to you and due today.' },
|
|
103
|
+
...(sug ? { move: { suggested: { id: 'suggest', label: sug }, options: [{ id: 'suggest', label: sug }] } } : {}),
|
|
104
|
+
...(httpsUrl(a.url) ? { url: httpsUrl(a.url), url_label: 'Open ' + a.key + ' in Jira' } : {}),
|
|
105
|
+
_late: late, _rank: reason === 'overdue' ? 0 : reason === 'blocked' ? 1 : 2,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function workCard(i, late) {
|
|
110
|
+
const kpis = [late > 0 ? [late + ' d', 'overdue', COLOR.bad] : ['today', 'due', COLOR.warn]];
|
|
111
|
+
if (/^P[1-3]$/.test(String(i.priority || ''))) kpis.push([i.priority, 'priority', COLOR.acc]);
|
|
112
|
+
const facts = [['repo', 'In your Work list'], ['clock', 'Due ' + i.due]];
|
|
113
|
+
if (i.person) facts.push(['user', clip(i.person, 60)]);
|
|
114
|
+
return {
|
|
115
|
+
key: 'real:work:' + i.id, origin: 'work', itemId: i.id,
|
|
116
|
+
tag: late > 0 ? 'Critical' : 'Just in', tone: late > 0 ? 'critical' : 'warning', kind: late > 0 ? 'Overdue' : 'Due today',
|
|
117
|
+
headline: clip(i.title || '(untitled)', 140), sub: 'Your Work item',
|
|
118
|
+
hero: { kind: 'big-number' }, kpis, factsX: facts, facts: [],
|
|
119
|
+
deadline: { days: -late, source: 'work', text: lateText(late) },
|
|
120
|
+
why: { text: late > 0 ? 'Your own Work item, past its due date.' : 'Your own Work item, due today.' },
|
|
121
|
+
_late: late, _rank: late > 0 ? 0 : 2,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function briefCard(x, ask) {
|
|
126
|
+
const src = SOURCE_WORD[x.source] || null;
|
|
127
|
+
const kpis = [];
|
|
128
|
+
if (typeof x.confidence === 'number') kpis.push([Math.round(x.confidence * 100) + '%', 'confidence', ask ? COLOR.warn : COLOR.acc]);
|
|
129
|
+
if (EFFORT_WORD[x.effort]) kpis.push([EFFORT_WORD[x.effort], 'effort', COLOR.ink]);
|
|
130
|
+
const facts = [];
|
|
131
|
+
if (src) facts.push(['inbox', 'From ' + src]);
|
|
132
|
+
if (x.who) facts.push(['user', clip(x.who, 60)]);
|
|
133
|
+
if (x.ask) facts.push(['reply', clip(x.ask, 120)]);
|
|
134
|
+
else if (x.evidence) facts.push(['info', clip(x.evidence, 120)]);
|
|
135
|
+
const todo = x.todo ? clip(x.todo, 120) : null;
|
|
136
|
+
return {
|
|
137
|
+
key: 'real:brief:' + x.id, origin: 'brief', ...(ask ? {} : { itemId: 'brief:' + x.id }),
|
|
138
|
+
tag: 'Just in', kind: ask ? 'Worth asking' : 'For you today', signal: ask ? 'question' : 'suggest',
|
|
139
|
+
headline: clip(x.title, 140), sub: x.why_now ? clip(x.why_now, 240) : null,
|
|
140
|
+
hero: { kind: 'big-number' }, kpis: kpis.slice(0, 3), factsX: facts.slice(0, 3), facts: [],
|
|
141
|
+
deadline: null,
|
|
142
|
+
why: { text: ask ? 'In today\'s morning brief, below the confidence bar: worth asking before acting.' : 'In today\'s morning brief, for you.' },
|
|
143
|
+
...(todo ? { move: { suggested: { id: 'todo', label: todo }, options: [{ id: 'todo', label: todo }] } } : {}),
|
|
144
|
+
...(httpsUrl(x.link) ? { url: httpsUrl(x.link), url_label: 'Open the source' } : {}),
|
|
145
|
+
_late: null, _rank: ask ? 3 : 4,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── the needs rows (the shape public/pm/c3-brief.js needRow takes) ──────────────────────────────────────────── */
|
|
150
|
+
function assignedNeed(e) {
|
|
151
|
+
const { a, late, blocked, decide } = e;
|
|
152
|
+
const jira = a.source !== 'wbs';
|
|
153
|
+
const kind = late > 0 ? 'Overdue' : blocked ? 'Blocked' : decide ? 'Decide' : 'Due today';
|
|
154
|
+
const facts = [{ icon: jira ? 'inbox' : 'repo', text: assignedLabel(a) + (a.status ? ' · ' + clip(a.status, 40) : ''), source: jira ? 'Jira' : 'WBS' }];
|
|
155
|
+
if (isDate(a.due)) facts.push({ icon: 'clock', text: 'Due ' + String(a.due).slice(0, 10), source: jira ? 'Jira' : 'WBS' });
|
|
156
|
+
if (blocked) facts.push({ icon: 'alert', text: blocked, source: jira ? 'Jira' : 'WBS', tone: 'warn' });
|
|
157
|
+
if (a.ask) facts.push({ icon: 'reply', text: clip(a.ask, 160), source: a.from ? clip(a.from, 40) : (jira ? 'Jira' : 'WBS') });
|
|
158
|
+
return {
|
|
159
|
+
key: 'mw:' + a.id, itemId: a.id, origin: jira ? 'jira' : 'wbs', kind, headline: clip(a.title, 140),
|
|
160
|
+
icon: late > 0 || blocked ? 'alert' : decide ? 'scale' : 'clock', color: late > 0 ? COLOR.bad : blocked || late === 0 ? COLOR.warn : null,
|
|
161
|
+
time: late === null ? null : late > 0 ? 'overdue' : 'due today', dueText: late === null ? null : lateText(late),
|
|
162
|
+
src: jira ? a.key : 'WBS', p: a.wsjf && /^P[1-3]$/.test(String(a.wsjf.tier)) ? a.wsjf.tier : null,
|
|
163
|
+
why: late > 0 ? 'Assigned to you and past its due date.' : blocked ? 'Assigned to you and blocked.' : decide ? 'Assigned to you as a decision.' : 'Assigned to you and due today.',
|
|
164
|
+
facts, ...(httpsUrl(a.url) ? { url: httpsUrl(a.url), url_label: 'Open ' + a.key + ' in Jira' } : {}),
|
|
165
|
+
_rank: late > 0 ? 0 : blocked ? 1 : decide ? 2 : 3, _late: late,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function askNeed(x) {
|
|
170
|
+
const src = SOURCE_WORD[x.source] || null;
|
|
171
|
+
const facts = [];
|
|
172
|
+
if (x.evidence) facts.push({ icon: 'info', text: clip(x.evidence, 160), source: src || 'brief' });
|
|
173
|
+
if (x.ask) facts.push({ icon: 'reply', text: clip(x.ask, 160), source: x.who ? clip(x.who, 40) : (src || 'brief') });
|
|
174
|
+
if (typeof x.confidence === 'number') facts.push({ icon: 'search', text: 'Confidence ' + Math.round(x.confidence * 100) + '%: below the bar, so ask before acting', source: 'brief', tone: 'warn' });
|
|
175
|
+
return {
|
|
176
|
+
key: 'ask:' + x.id, origin: 'brief', kind: 'Worth asking', headline: clip(x.title, 140), icon: 'search', color: COLOR.acc,
|
|
177
|
+
time: null, src: src || 'Brief', why: x.why_now ? clip(x.why_now, 240) : null, facts,
|
|
178
|
+
...(httpsUrl(x.link) ? { url: httpsUrl(x.link), url_label: 'Open the source' } : {}),
|
|
179
|
+
_rank: 4, _late: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const strip = (o) => { const { _late, _rank, ...rest } = o; return rest; };
|
|
184
|
+
const byRank = (a, b) => a._rank - b._rank || (b._late || 0) - (a._late || 0);
|
|
185
|
+
|
|
186
|
+
function build({ brief, myWork, itemLines, today } = {}) {
|
|
187
|
+
if (!isDate(today)) return { lead: [], tail: [], needs: [] };
|
|
188
|
+
const lines = Array.isArray(itemLines) ? itemLines : [];
|
|
189
|
+
const folded = pmItems.fold(lines);
|
|
190
|
+
const ledger = new Map(folded.map((i) => [i.id, i]));
|
|
191
|
+
const held = new Set(lines.filter((l) => l && l.id).map((l) => l.id));
|
|
192
|
+
const assigned = assignedOf(myWork, ledger, held, today);
|
|
193
|
+
const assignedIds = new Set((Array.isArray(myWork) ? myWork : []).map((x) => x && x.id).filter(Boolean));
|
|
194
|
+
|
|
195
|
+
const cards = [];
|
|
196
|
+
for (const e of assigned) if (e.late !== null || e.blocked) cards.push(assignedCard(e, today));
|
|
197
|
+
// the PM's own Work items (and ledger copies whose source no longer lists them); an assigned id was handled above
|
|
198
|
+
for (const i of folded) {
|
|
199
|
+
if (!i || i.done || assignedIds.has(i.id) || setAside(i, today)) continue;
|
|
200
|
+
const late = lateBy(i.due, today);
|
|
201
|
+
if (late !== null) cards.push(workCard(i, late));
|
|
202
|
+
}
|
|
203
|
+
const briefToday = brief && brief.date === today ? brief : null;
|
|
204
|
+
const fromBrief = (x) => x && x.id && x.title && !(held.has('brief:' + x.id) && (!ledger.get('brief:' + x.id) || ledger.get('brief:' + x.id).done));
|
|
205
|
+
const asks = briefToday && Array.isArray(briefToday.worth_asking) ? briefToday.worth_asking.filter(fromBrief) : [];
|
|
206
|
+
const mine = briefToday && Array.isArray(briefToday.priorities) ? briefToday.priorities.filter((x) => fromBrief(x) && x.owner === 'pm') : [];
|
|
207
|
+
for (const x of asks) cards.push(briefCard(x, true));
|
|
208
|
+
for (const x of mine) cards.push(briefCard(x, false));
|
|
209
|
+
cards.sort(byRank);
|
|
210
|
+
const kept = cards.slice(0, MAX_CARDS);
|
|
211
|
+
|
|
212
|
+
// Needs you: the assigned items the own half does not show (it shows the ledger's own items due by today), then the asks
|
|
213
|
+
const ownShown = (id) => { const L = ledger.get(id); return !!(L && !L.done && lateBy(L.due, today) !== null); };
|
|
214
|
+
const needs = [...assigned.filter((e) => !ownShown(e.a.id)).map(assignedNeed), ...asks.map(askNeed)].sort(byRank);
|
|
215
|
+
|
|
216
|
+
return { lead: kept.filter((c) => c._rank === 0).map(strip), tail: kept.filter((c) => c._rank !== 0).map(strip), needs: needs.map(strip) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The engine's cards that carry a move or are Critical, as needs rows; the page shapes each from its card. Real cards (origin) are left out. */
|
|
220
|
+
function engineNeeds(cards) {
|
|
221
|
+
return (Array.isArray(cards) ? cards : []).filter((c) => c && !c.origin && c.headline && (c.move || c.tag === 'Critical'))
|
|
222
|
+
.map((c) => ({ key: 'card:' + (c.key || c.headline), kind: 'decision', headline: String(c.headline), tag: c.tag, card: c }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The deck: the overdue cards, the engine's cards unchanged, then the rest. */
|
|
226
|
+
function merge(engineCards, built) {
|
|
227
|
+
const b = built || {};
|
|
228
|
+
return [...(b.lead || []), ...(Array.isArray(engineCards) ? engineCards : []), ...(b.tail || [])];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Needs you, in order: overdue and blocked assigned items, the engine's decisions, then decision, due today and worth asking rows. */
|
|
232
|
+
function needsOf(built, engineCards) {
|
|
233
|
+
const rows = (built && built.needs) || [];
|
|
234
|
+
const head = rows.filter((n) => n.kind === 'Overdue' || n.kind === 'Blocked');
|
|
235
|
+
const rest = rows.filter((n) => !(n.kind === 'Overdue' || n.kind === 'Blocked'));
|
|
236
|
+
return [...head, ...engineNeeds(engineCards), ...rest];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ── R7 · Jira epic progress, from one status snapshot (lib/jira-status.js readSnapshot) ──────────────────────── */
|
|
240
|
+
function jiraProgress(snap) {
|
|
241
|
+
if (!snap || !snap.tickets || typeof snap.tickets.values !== 'function') return null;
|
|
242
|
+
const all = [...snap.tickets.values()].filter((r) => r && r.key);
|
|
243
|
+
const parents = new Set(all.map((r) => r.parent).filter(Boolean));
|
|
244
|
+
const isEpic = (r) => /^epic$/i.test(String(r.type || '')) || parents.has(r.key);
|
|
245
|
+
const epics = all.filter(isEpic).map((r) => r.key);
|
|
246
|
+
const kids = all.filter((r) => !isEpic(r));
|
|
247
|
+
if (!kids.length) return null;
|
|
248
|
+
const done = kids.filter((r) => r.category === 'done').length;
|
|
249
|
+
const blocked = kids.filter((r) => r.category !== 'done' && r.flagged).length;
|
|
250
|
+
return { done, total: kids.length, blocked, epics, as_of: snap.fetchedAt || null };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── the reads (the route's side) ─────────────────────────────────────────────────────────────────────────── */
|
|
254
|
+
async function readSources(ctx, now, today) {
|
|
255
|
+
const out = { brief: null, myWork: [], itemLines: [] };
|
|
256
|
+
const dir = ctx && ctx.runtimeDir;
|
|
257
|
+
if (!dir) return out;
|
|
258
|
+
try { out.brief = JSON.parse(require('fs').readFileSync(path.join(dir, 'briefs', today + '.json'), 'utf8')); } catch { out.brief = null; }
|
|
259
|
+
try { out.itemLines = pmItems.readLines(path.join(dir, 'pm-items', 'items.jsonl')); } catch { out.itemLines = []; }
|
|
260
|
+
try { const mw = await require('./routes-my-work.js').listForAsync(ctx, now.getTime()); out.myWork = mw && mw.ok ? mw.items : []; } catch { out.myWork = []; }
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { MAX_CARDS, build, merge, needsOf, engineNeeds, jiraProgress, readSources, blockedOf };
|