create-pmos 0.129.0 → 0.130.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.129.0",
3
+ "version": "0.130.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@64a9920
1
+ kernel-source: pmos@21cec91
2
2
  assembled: 2026-10-11
3
3
  mode: vendor
@@ -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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:27:22Z"}
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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:27:22Z"}
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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:27:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -49,6 +49,7 @@ function collect(tab) {
49
49
  // row (Work's rows have no buttons; a swipe or an arrow key is the action).
50
50
  for (const ctl of pane.querySelectorAll('button, [data-a], [role="button"], [data-swipe], [data-row][tabindex]')) {
51
51
  if (!visible(ctl) || ctl.disabled) continue;
52
+ if (ctl.closest('[data-ref-row]')) continue; // a reference row (Briefing Today, Portfolio pulse) opens or explains; it holds no action on a record (pm-brief-page-test.js checks that)
52
53
  const item = ctl.closest(ITEM);
53
54
  if (!item || !pane.contains(item)) continue;
54
55
  const t = item.querySelector('[data-pm-title], .tt, h2, b, strong, .t, [data-title], h3, h4');
@@ -95,7 +96,9 @@ function collect(tab) {
95
96
  last = n;
96
97
  }
97
98
  byTab[tab] = items || [];
98
- ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
99
+ // Reports (since the dashboards) draws widgets and charts, not items: it may have none, and then it cannot hold a second home.
100
+ if (tab === 'rep') ok('the rep tab holds no item with an action control, or only widgets', true);
101
+ else ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
99
102
  }
100
103
 
101
104
  // Every title that has an action control on two or more tabs.
@@ -0,0 +1,60 @@
1
+ ---
2
+ type: Guide
3
+ title: "The Briefing: today's brief"
4
+ description: "The Briefing page of the console (/c3.html): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
5
+ timestamp: 2026-10-11
6
+ ---
7
+
8
+ # The Briefing: today's brief
9
+
10
+ The Briefing is the first page of the console at `/c3.html`. It is one column. It reads from top to bottom: the headline cards, Today, Needs you and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
11
+
12
+ The Briefing shows only what PMOS already holds. When PMOS holds nothing for a section, the section says so.
13
+
14
+ ## Headline cards
15
+
16
+ The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case. The day-part buttons (Plan, Unblock, Close, Reflect) re-rank the same signals.
17
+
18
+ ## Today
19
+
20
+ Today lists the meetings of the day, in time order. Each row shows:
21
+
22
+ - The start time and the length.
23
+ - The people and the initiative of the meeting.
24
+ - **Prepared**: what PMOS holds for the meeting. This is the agent's prep note, your notes on the event, the notes you started for it, and the last notes of the same meeting. When PMOS holds none, the row says "Nothing prepared yet".
25
+ - **Before the meeting**: your open items that name the meeting.
26
+
27
+ A meeting that is over is dimmed. The meeting under way is marked **Now**. The next one is marked **Next**. When the calendar holds no meeting for today, Today says so.
28
+
29
+ A Today row is a reference. It has no button that changes anything.
30
+
31
+ ## Needs you
32
+
33
+ Needs you lists what only you can do. The rows come in this order:
34
+
35
+ 1. Your own items that are overdue. The most late is first. The row says how late.
36
+ 2. Your own items due today.
37
+ 3. Items outside PMOS that wait for you to confirm: talk to someone, meet, call, ask.
38
+ 4. Decisions that only you can make.
39
+
40
+ 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.
41
+
42
+ - **Done** marks the item done. For an item outside PMOS you can first type one line about what happened. The line is optional.
43
+ - **Not yet** asks again later. Choose **Later today**, **Tomorrow** or **Next week**. The item leaves Needs you and comes back on that day.
44
+ - **Drop** removes the item. Press **Undo** in the notice to bring it back.
45
+
46
+ PMOS can see evidence that an outside item is done. For example, a meeting with the person is on the calendar. Then the row says "Looks done: a meeting with Ana is booked Tue 10:00. Confirm?" PMOS never marks the item done itself. The item changes only when you press **Done** or **Yes, it is done**.
47
+
48
+ ## Portfolio pulse
49
+
50
+ Portfolio pulse shows one bar for the status of the initiatives and one row for each initiative. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
51
+
52
+ To go to the initiative, press **Open initiative** inside the opened row.
53
+
54
+ ## Where each action lives
55
+
56
+ An item has its actions in one place only. On the Briefing, Today and Portfolio pulse show items, and do not change them. Needs you is where you decide and where you confirm your own overdue items.
57
+
58
+ ## On a phone
59
+
60
+ At 400 pixels wide the Briefing is one column with no sideways scroll. Each button in an opened row is 44 pixels high.
@@ -0,0 +1,46 @@
1
+ ---
2
+ type: Guide
3
+ title: "Calendar popup"
4
+ description: "How to open the full calendar over any page of the console with the calendar button or the C key, how to close it, and how it remembers the zoom."
5
+ timestamp: 2026-10-11
6
+ ---
7
+
8
+ # Calendar popup
9
+
10
+ The calendar popup shows the full calendar over any page of the console (`/c3.html`). You do not leave the page you are on.
11
+
12
+ ## Open it
13
+
14
+ Press the **calendar button** in the top bar. It is next to the search button. Or press **C**.
15
+
16
+ The popup is never open when the page loads.
17
+
18
+ The key **C** does nothing in these cases:
19
+
20
+ - You are typing in a field, for example the search box.
21
+ - You hold Ctrl, ⌘ or Alt.
22
+ - A sheet or the search palette is open.
23
+
24
+ ## Use it
25
+
26
+ The popup has the same five zoom levels as the Calendar tab: Day, Week, Month, Quarter and Year. It has no ribbon. Press **+** or **-** to zoom. The popup opens at the zoom you used last. The console keeps that zoom until you change it.
27
+
28
+ The popup and the Calendar tab are two separate views. A zoom or a date you choose in one view does not change the other.
29
+
30
+ ## Close it
31
+
32
+ Close the popup in one of three ways:
33
+
34
+ 1. Press **Esc**.
35
+ 2. Press the dark area around the popup.
36
+ 3. Press the close button (×) at the top right.
37
+
38
+ The focus goes back to the control that was in use before the popup opened. When you used the button, the focus goes back to the calendar button.
39
+
40
+ ## On a phone
41
+
42
+ On a phone the popup fills the screen. The page does not scroll sideways. Every button is at least 44 pixels high and wide.
43
+
44
+ ## Related
45
+
46
+ - [Layout](layout.md) lists all the keys.
@@ -22,6 +22,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
22
22
  | You want | Go to |
23
23
  |---|---|
24
24
  | What is waiting for you now | Agenda |
25
+ | Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (`/c3.html`) |
25
26
  | Every Jira ticket and WBS item assigned to you, ranked, to brief an agent | My work, or press `g w` |
26
27
  | The initiatives of a repository, or of every repository | Initiatives. Choose **All repositories** in Repository ▾ for every repository. |
27
28
  | The sessions of a repository, by initiative | The agent dock. ⌘K → **Branches and sessions** lists the branches. |
@@ -38,11 +39,13 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
38
39
 
39
40
  ## Where to read next
40
41
 
42
+ - [The Briefing](briefing.md): the console's first page: Today's meetings, Needs you (Done, Not yet, Drop) and the Portfolio pulse.
41
43
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
42
44
  - [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
43
45
  - [My work](my-work.md): the work assigned to you, its order, the brief to an agent, and `pmos my-work`.
44
46
  - [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
45
47
  - [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
48
+ - [Calendar popup](calendar-popup.md): open the full calendar over any page of the console with the calendar button or the C key, and close it.
46
49
  - [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
47
50
  - [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
48
51
  - [Initiatives and next steps](initiatives-and-next-steps.md): the detail, the next steps, and the artifacts.
@@ -111,6 +111,7 @@ On the console page (`/c3.html`), text on the blue accent and on a red badge is
111
111
  | g t, g i, g b, g r, g d, g p | Show Agenda, Initiatives, Board, Reports, Docs or People. |
112
112
  | ⌘K (Ctrl-K) | Open or close the command palette. |
113
113
  | ⌘J (Ctrl-J) | Open or fold the dock. |
114
+ | C | On the console page (`/c3.html`): open the [calendar popup](calendar-popup.md). Ignored while you type or while a sheet is open. |
114
115
  | ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
115
116
  | j, k | Move down or up the rows of the place. |
116
117
  | Enter | Open the row that has the focus. |
@@ -48,6 +48,7 @@ const MODULES = Object.freeze([
48
48
  'routes-forecast', // console3 B5: forecast, critical path, time machine and capacity (read-only)
49
49
  'routes-calendar-views', // console3 C2: Day/Week/Month/Quarter/Year cells, Gantt and the strategy lens (read-only)
50
50
  'routes-briefing', // console3 C1: the day-part Briefing cards from the signal engine (read-only)
51
+ 'routes-brief-page', // console3 Briefing as the brief: Today agenda and Needs you (read-only; writes go through routes-pm-items)
51
52
  'routes-report-facts', // console3 D: GET /api/report-families/facts (read-only, real data only)
52
53
  'routes-records', // console3 E1a: ideas, hypotheses, decisions, KR metrics, bets (GET list; page-press add/edit/reading)
53
54
  'routes-doc-sheet', // console3 E1c: document sheet: revisions, edit with why, propose and accept by hash
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+ /* routes-brief-page.js — GET /api/brief-page[?ws=<id>&now=ISO] (console3 Briefing as the brief, PRD briefing-brief R2, R3, R8).
3
+ *
4
+ * → { ok, date, today: [meeting], needs: [row], decisions_error? }
5
+ *
6
+ * READ ONLY, and built only from what PMOS already holds. Nothing is invented.
7
+ * today one row per meeting of the local day, in time order, from the cached calendar events (routes-calendar-import.js readEvents,
8
+ * the same source as /api/calendar-views): { id, title, start, end, minutes, people, initiative, prepared, before, state }.
9
+ * prepared: [{ kind: 'prep'|'pm-note'|'note'|'last-note', text, id? }] what PMOS holds for the meeting (the agent's prep note,
10
+ * the PM's notes on the event, the note linked to it, the last note of an earlier occurrence). before: [{ id, title, due,
11
+ * done }] the PM's own open items that name the meeting (item.meeting_id or item.event_id equals the event id, or the title
12
+ * contains the meeting's title). state: 'past' | 'now' | 'next' | 'later'.
13
+ * needs decisions only (with ?decisions=1; the page takes them from GET /api/briefing instead) the PM can make (the signal engine's cards that carry options or are Critical), the PM's own items that are
14
+ * overdue or due today, and outside-PMOS items (kind 'offline') that await confirmation. Overdue rows come first, the most
15
+ * late first, then the items due today, then the offline ones, then the decisions.
16
+ * An item row: { key, kind: 'overdue'|'due-today'|'offline', id, headline, due, days_late, priority, person, offline, evidence }.
17
+ * evidence is { text, event_id } when the calendar holds a meeting with the person, else null: the page asks the PM to
18
+ * confirm it and never marks the item done itself.
19
+ * A decision row: { key, kind: 'decision', headline, tag, card }.
20
+ * Item fields read defensively (a missing field is absent): kind, defer, person, evidence, workPlacement(item, today) when lib/pm-items.js
21
+ * has it. A deferred item ('Not yet') is left out until its defer date or time. Writes go through the existing pm-items routes. */
22
+ const path = require('path');
23
+ const pmItems = require('./pm-items.js');
24
+
25
+ const pad = (n) => String(n).padStart(2, '0');
26
+ const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
27
+ const DAY = 86400000;
28
+ const norm = (s) => String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
29
+ const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
30
+ const hhmm = (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
31
+ const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
32
+
33
+ function daysBetween(fromIso, toIso) {
34
+ const a = Date.parse(fromIso + 'T00:00:00Z'); const b = Date.parse(toIso + 'T00:00:00Z');
35
+ return Math.round((b - a) / DAY);
36
+ }
37
+
38
+ function deferred(item, now, todayIso) {
39
+ const d = item.defer;
40
+ if (!d) return false;
41
+ const s = String(d);
42
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s > todayIso;
43
+ const t = Date.parse(s);
44
+ return Number.isFinite(t) && t > now.getTime();
45
+ }
46
+
47
+ /** The meetings of the local day. `notes` is an optional { findByEvent(ev), lastBefore(ev) } so the route can read the notes store. */
48
+ function todayAgenda(events, items, now, notes = {}) {
49
+ const todayIso = localDate(now);
50
+ const open = items.filter((i) => !i.done);
51
+ const rows = (events || []).filter((e) => e && e.at && !Number.isNaN(Date.parse(e.at)) && localDate(new Date(e.at)) === todayIso)
52
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || String(a.id).localeCompare(String(b.id)))
53
+ .map((e) => {
54
+ const s = new Date(e.at); const endMs = e.end && Date.parse(e.end) > Date.parse(e.at) ? Date.parse(e.end) : Date.parse(e.at);
55
+ const prepared = [];
56
+ if (e.agent_prep_notes && String(e.agent_prep_notes).trim()) prepared.push({ kind: 'prep', text: clip(e.agent_prep_notes, 200) });
57
+ const pm = Array.isArray(e.pm_notes) ? e.pm_notes : [];
58
+ if (pm.length) prepared.push({ kind: 'pm-note', text: clip(pm[pm.length - 1].text, 200) });
59
+ const linked = notes.findByEvent ? notes.findByEvent(e) : null;
60
+ if (linked) prepared.push({ kind: 'note', text: 'Notes started: ' + clip(linked.title, 80), id: linked.id });
61
+ const last = notes.lastBefore ? notes.lastBefore(e) : null;
62
+ if (last) prepared.push({ kind: 'last-note', text: 'Last notes: ' + clip(last.title, 80) + ' (' + String(last.updated_at || last.created_at || '').slice(0, 10) + ')', id: last.id });
63
+ const t = norm(e.title);
64
+ const before = open.filter((i) => (i.meeting_id && i.meeting_id === e.id) || (i.event_id && i.event_id === e.id) || (t.length >= 6 && norm(i.title).includes(t)))
65
+ .map((i) => ({ id: i.id, title: i.title, due: i.due || null, done: !!i.done }));
66
+ return { id: e.id, title: e.title, start: s.toISOString(), end: new Date(endMs).toISOString(), startLocal: hhmm(s), minutes: Math.max(0, Math.round((endMs - s.getTime()) / 60000)),
67
+ people: Array.isArray(e.attendees) ? e.attendees.slice(0, 12) : [], initiative: e.initiative || null, prepared, before, _s: s.getTime(), _e: endMs };
68
+ });
69
+ let nextDone = false;
70
+ for (const r of rows) {
71
+ const nowMs = now.getTime();
72
+ if (r._e <= nowMs && r._e > r._s) r.state = 'past';
73
+ else if (r._s <= nowMs && r._e > nowMs) r.state = 'now';
74
+ else if (r._e === r._s && r._s < nowMs) r.state = 'past';
75
+ else if (!nextDone) { r.state = 'next'; nextDone = true; } else r.state = 'later';
76
+ }
77
+ rows.forEach((r) => { delete r._s; delete r._e; });
78
+ return rows;
79
+ }
80
+
81
+ /** A calendar meeting that names the person, from today on: "a meeting with Ana is booked Tue 10:00". */
82
+ function evidenceFor(item, events, now) {
83
+ if (item.evidence) {
84
+ const t = Array.isArray(item.evidence) ? item.evidence[0] : item.evidence;
85
+ const text = typeof t === 'string' ? t : t && t.text;
86
+ if (text) return { text: clip(text, 160), event_id: (t && t.event_id) || null };
87
+ }
88
+ const names = [];
89
+ if (item.person) names.push(norm(item.person));
90
+ const title = norm(item.title);
91
+ const hits = (events || []).filter((e) => e && e.at && Date.parse(e.at) >= now.getTime() - 3600000 && Date.parse(e.at) <= now.getTime() + 21 * DAY)
92
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
93
+ for (const e of hits) {
94
+ const who = (Array.isArray(e.attendees) ? e.attendees : []).map(norm).filter(Boolean);
95
+ const hit = who.find((w) => names.some((n) => n && (w.includes(n) || n.includes(w))) || (w.split(' ')[0].length >= 3 && new RegExp('\\b' + w.split(' ')[0].replace(/[^a-z0-9]/g, '') + '\\b').test(title)));
96
+ if (hit) {
97
+ const d = new Date(e.at);
98
+ const who1 = hit.split(' ')[0]; const nice = who1.charAt(0).toUpperCase() + who1.slice(1);
99
+ return { text: 'a meeting with ' + nice + ' is booked ' + WD[d.getDay()] + ' ' + hhmm(d), event_id: e.id };
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function needsOf(items, events, cards, now, deps = {}) {
106
+ const todayIso = localDate(now);
107
+ const todayDate = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
108
+ const work = typeof pmItems.workPlacement === 'function' ? pmItems.workPlacement : null;
109
+ const late = []; const dueToday = []; const offline = [];
110
+ for (const i of items) {
111
+ if (!i || i.done || deferred(i, now, todayIso)) continue;
112
+ const base = { id: i.id, headline: i.title, due: i.due || null, priority: i.priority || null, person: i.person || null };
113
+ if (i.kind === 'offline') {
114
+ const late0 = i.due && i.due < todayIso ? daysBetween(i.due, todayIso) : 0;
115
+ offline.push({ key: 'item:' + i.id, kind: 'offline', ...base, days_late: late0, offline: true, evidence: evidenceFor(i, events, now) });
116
+ continue;
117
+ }
118
+ if (!i.due || i.due > todayIso) continue;
119
+ if (work) { if (work(i, todayDate) !== 'today') continue; } else if (i.waiting_on || i.parked || i.list_override === 'later') continue;
120
+ if (i.due < todayIso) late.push({ key: 'item:' + i.id, kind: 'overdue', ...base, days_late: daysBetween(i.due, todayIso), offline: false, evidence: null });
121
+ else dueToday.push({ key: 'item:' + i.id, kind: 'due-today', ...base, days_late: 0, offline: false, evidence: null });
122
+ }
123
+ late.sort((a, b) => b.days_late - a.days_late || String(a.headline).localeCompare(String(b.headline)));
124
+ offline.sort((a, b) => b.days_late - a.days_late || String(a.headline).localeCompare(String(b.headline)));
125
+ const byTitle = (a, b) => String(a.headline).localeCompare(String(b.headline));
126
+ dueToday.sort(byTitle);
127
+ const decisions = (cards || []).filter((c) => c && (c.move || c.tag === 'Critical')).map((c) => ({ key: 'card:' + c.key, kind: 'decision', headline: c.headline, tag: c.tag, card: c }));
128
+ return [...late, ...dueToday, ...offline, ...decisions];
129
+ }
130
+
131
+ /** The pure builder. deps: { events, items (folded), cards, notes } so a test needs no repository. */
132
+ function build(query, deps = {}) {
133
+ const q = query || {};
134
+ let now = q.now ? new Date(q.now) : (deps.now ? new Date(deps.now) : new Date());
135
+ if (Number.isNaN(now.getTime())) now = new Date();
136
+ const events = deps.events || []; const items = deps.items || [];
137
+ const body = { ok: true, date: localDate(now), today: todayAgenda(events, items, now, deps.notes || {}), needs: needsOf(items, events, deps.cards || [], now, deps) };
138
+ if (deps.decisionsError) body.decisions_error = deps.decisionsError;
139
+ return { status: 200, body };
140
+ }
141
+
142
+ function notesAdapter(runtimeDir) {
143
+ let notes; try { notes = require('./notes.js'); } catch { return {}; }
144
+ const rows = () => { try { const l = notes.list(runtimeDir); return l && l.ok ? l.notes.filter((n) => !n.private) : []; } catch { return []; } };
145
+ return {
146
+ findByEvent: (e) => { try { const n = notes.findByEvent(runtimeDir, { event_id: e.id, start: e.at, title: e.title }); return n && !n.private ? { id: n.id, title: n.title || 'Untitled note' } : null; } catch { return null; } },
147
+ lastBefore: (e) => {
148
+ const t = norm(e.title);
149
+ const hits = rows().filter((n) => n.meeting && n.meeting.kind === 'event' && norm(n.meeting.title) === t && Date.parse(n.meeting.start) < Date.parse(e.at))
150
+ .sort((a, b) => String(b.meeting.start).localeCompare(String(a.meeting.start)));
151
+ return hits[0] || null;
152
+ },
153
+ };
154
+ }
155
+
156
+ module.exports = {
157
+ prefixes: ['/api/brief-page'],
158
+ build, todayAgenda, needsOf, evidenceFor, deferred,
159
+ handle: async (req, res, url, ctx) => {
160
+ if (url.pathname !== '/api/brief-page') return false;
161
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
162
+ const q = Object.fromEntries(url.searchParams.entries());
163
+ let now = q.now ? new Date(q.now) : new Date();
164
+ if (Number.isNaN(now.getTime())) now = new Date();
165
+ try {
166
+ const dir = ctx.runtimeDir;
167
+ const events = dir ? require('./routes-calendar-import.js').readEvents(dir) : [];
168
+ const items = dir ? pmItems.fold(pmItems.readLines(path.join(dir, 'pm-items', 'items.jsonl'))) : [];
169
+ // The decisions come from the signal engine, whose repository read is slow. The page already holds them (GET /api/briefing feeds its deck), so the
170
+ // read is made only when the caller asks (?decisions=1); the items and the agenda never wait for it.
171
+ let cards = []; let decisionsError = null;
172
+ if (q.decisions === '1') try {
173
+ const w = ctx.getWs && ctx.getWs(q.ws || null);
174
+ const root = (w && w.root) || path.resolve(__dirname, '..', '..');
175
+ const iso = localDate(now);
176
+ const facts = await require('./facts-async.js').readFactsAsync(root, { today: iso });
177
+ const sig = path.resolve(__dirname, '..', '..', 'scripts', 'signals');
178
+ cards = require(path.join(sig, 'index.js')).present(facts, { lens: 'Unblock', zoom: 'week', today: iso }).cards || [];
179
+ } catch (e) { decisionsError = 'the decisions could not be read: ' + (e && e.message ? e.message : String(e)); }
180
+ const out = build({ now: now.toISOString() }, { events, items, cards, decisionsError, notes: dir ? notesAdapter(dir) : {} });
181
+ ctx.json(res, out.body, out.status);
182
+ } catch (e) {
183
+ ctx.json(res, { ok: false, error: 'the brief page could not be built: ' + (e && e.message ? e.message : String(e)) }, 500);
184
+ }
185
+ return true;
186
+ },
187
+ };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.129.0",
3
+ "version": "0.130.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.129.0",
9
+ "version": "0.130.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.129.0",
3
+ "version": "0.130.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -0,0 +1,11 @@
1
+ /* pm/c3-brief-data.js — the one read behind the Briefing's Today agenda and the own-item rows of Needs you: GET /api/brief-page
2
+ * (lib/routes-brief-page.js). Both sections subscribe; one request answers both. A failed read is kept as { ok:false, error } so each
3
+ * section can say so; nothing here invents a row. */
4
+ import { pmGet } from './http.js';
5
+
6
+ let cache = null; const subs = new Set();
7
+ export function onBriefPage(fn) { subs.add(fn); if (cache) fn(cache); return () => subs.delete(fn); }
8
+ export function loadBriefPage() {
9
+ return pmGet('/api/brief-page').then((j) => (j && j.ok !== false ? j : { ok: false, error: (j && j.error) || 'unreadable' }), (e) => ({ ok: false, error: String((e && e.message) || e) }))
10
+ .then((j) => { cache = j; subs.forEach((f) => f(j)); return j; });
11
+ }
@@ -0,0 +1,145 @@
1
+ /* pm/c3-brief-needs.js — the Briefing's "Needs you" (PRD briefing-brief R3, R4, R6).
2
+ * needsSection(S) the section: own items first (overdue, most late first; then due today; then outside PMOS), then the decisions only the PM
3
+ * can make (the briefing's needs in the demo, the engine cards that carry a move or are Critical in real mode: c3-brief.js needRow).
4
+ * itemRow(row, ctx) a row of an own item. It opens in place (c3-expand). Its actions write to the item's home record, through the pm-items routes:
5
+ * Done POST /api/pm-items/done { id, outcome? }
6
+ * Not yet POST /api/pm-items/notyet { id, when: 'later-today' | 'tomorrow' | 'next-week' } the item returns on that date
7
+ * Drop POST /api/pm-items/remove { id } (Undo: POST /api/pm-items/undo)
8
+ * An outside-PMOS item (talk to, meet, call, ask) takes an optional one-line outcome. When PMOS holds evidence ("a meeting with Ana is
9
+ * booked Tue 10:00") it asks "Looks done: ... Confirm?". It never marks the item done itself: only a press on Done or Confirm does.
10
+ * The own-item rows are built again only when their keys change, so a re-read does not close an open row. An open row is kept in BX.open. */
11
+ import { el } from './dom.js';
12
+ import { c3ic } from './pm-work-view.js';
13
+ import { pmPost } from './http.js';
14
+ import { makeXp, attachExpand } from './c3-expand.js';
15
+ import { needRow, belowLine, BX, toast } from './c3-brief.js';
16
+ import { onBriefing, loadBriefing } from './shell-ticker.js';
17
+ import { cardView, TAG_STYLE } from './briefing-model.js';
18
+ import { onBriefPage } from './c3-brief-data.js';
19
+ import { button, ensureComponentsCss } from './primitives.js';
20
+
21
+ const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
22
+ const post = (path, body) => pmPost(path, body).catch(() => ({ ok: false }));
23
+ export const WHEN = [['later-today', 'Later today'], ['tomorrow', 'Tomorrow'], ['next-week', 'Next week']];
24
+ const WHEN_TOAST = { 'later-today': 'Back later today', tomorrow: 'Back tomorrow', 'next-week': 'Back next week' };
25
+
26
+ function leave(row, then) {
27
+ if (reduced()) { then(); return; }
28
+ row.classList.add('leave'); setTimeout(then, 340);
29
+ }
30
+
31
+ export function itemRow(r, ctx = {}) {
32
+ const key = r.key; const offline = !!r.offline;
33
+ const tone = r.kind === 'overdue' ? 't-bad' : offline ? 't-ink' : 't-warn';
34
+ ensureComponentsCss();
35
+ const late = r.days_late > 0 ? (r.days_late === 1 ? '1 day late' : r.days_late + ' days late') : null;
36
+ let row = null; let busy = false;
37
+ const out = el('input', { type: 'text', class: 'ioutcome', 'data-outcome': '', maxlength: '160', placeholder: offline ? 'What happened? One line, optional' : 'Note, optional', 'aria-label': 'Outcome, optional' });
38
+
39
+ const write = (call, okText, undo) => {
40
+ if (busy) return; busy = true;
41
+ call().then((res) => {
42
+ busy = false;
43
+ if (!res || res.ok === false) { toast('That did not go through' + (res && res.error ? ': ' + res.error : '.')); return; }
44
+ BX.open.delete(key);
45
+ leave(row, () => { row.remove(); if (ctx.changed) ctx.changed(); });
46
+ toast(okText, undo ? () => { post(undo.path, undo.body).then(() => { if (ctx.reload) ctx.reload(); }); } : undefined);
47
+ });
48
+ };
49
+ const done = (why) => {
50
+ const o = out.value.trim() || why || '';
51
+ write(() => post('/api/pm-items/done', { id: r.id, ...(o ? { outcome: o } : {}) }), 'Marked done', { path: '/api/pm-items/reopen', body: { id: r.id } });
52
+ };
53
+ const notYet = (when) => write(() => post('/api/pm-items/notyet', { id: r.id, when }), WHEN_TOAST[when]);
54
+ const drop = () => write(() => post('/api/pm-items/remove', { id: r.id }), 'Dropped', { path: '/api/pm-items/undo', body: { id: r.id } });
55
+
56
+ const btn = (label, act, fn, extra) => button({ label, onClick: (e) => { e.stopPropagation(); fn(); }, attrs: { 'data-act': act, ...(extra || {}) } });
57
+ const panel = el('div', { class: 'ip', 'data-item-panel': r.id },
58
+ r.evidence ? el('div', { class: 'looks', 'data-evidence': '', role: 'note' }, c3ic('eye', 'sm'), el('span', null, 'Looks done: ' + r.evidence.text + '. Confirm?'),
59
+ btn('Yes, it is done', 'confirm', () => done('Confirmed: ' + r.evidence.text))) : null,
60
+ out,
61
+ el('div', { class: 'iacts' },
62
+ btn('Done', 'done', () => done('')),
63
+ el('span', { class: 'ny', role: 'group', 'aria-label': 'Not yet: ask again' }, el('span', { class: 'dim c3s2' }, 'Not yet, ask again:'),
64
+ ...WHEN.map(([w, l]) => btn(l, 'notyet', () => notYet(w), { 'data-when': w }))),
65
+ btn('Drop', 'drop', drop)));
66
+ const xp = makeXp(panel);
67
+ const chips = [late ? el('span', { class: 'st warn', 'data-late': String(r.days_late) }, c3ic('clock', 'sm'), late) : null,
68
+ r.kind === 'due-today' ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), 'Due today') : null,
69
+ offline ? el('span', { class: 'tag', 'data-offline': '' }, 'Outside PMOS') : el('span', { class: 'tag' }, 'Yours'),
70
+ r.person ? el('span', { class: 'chip2' }, c3ic('user', 'sm'), r.person) : null,
71
+ r.priority ? el('span', { class: 'fl ' + String(r.priority).toLowerCase() }, r.priority) : null,
72
+ r.evidence ? el('span', { class: 'st ok', 'data-looks-done': '' }, c3ic('eye', 'sm'), 'Looks done') : null];
73
+ row = el('li', { class: 'nrow', 'data-row': '', 'data-id': key, 'data-needs-row': key, 'data-item-id': r.id, 'data-ref-row': r.id, 'data-kind': r.kind, 'data-a': 'expand' },
74
+ el('span', { class: 'ki ' + tone + (r.kind === 'overdue' ? ' hot' : ''), title: r.kind }, c3ic(r.kind === 'overdue' ? 'alert' : offline ? 'user' : 'clock')),
75
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.headline), el('div', { class: 'chips' }, ...chips.filter(Boolean)), xp));
76
+ attachExpand(row, xp, { key: 'item-' + key, initial: BX.open.has(key), onToggle: (on) => { if (on) BX.open.add(key); else BX.open.delete(key); } });
77
+ return row;
78
+ }
79
+
80
+ export function needsSection(S) {
81
+ const count = el('span', { class: 'bdg g' }, '0');
82
+ const quick = el('span', { class: 'chips2 dim mono c3s2' });
83
+ const own = el('ul', { class: 'list', role: 'list', 'data-needs-own': '' });
84
+ const ownNote = el('div');
85
+ const list = el('ul', { class: 'list', role: 'list' });
86
+ const extra = el('div');
87
+ const alertHost = el('div');
88
+ const box = el('section', { class: 'card sect', 'data-c3pg': 'needs', 'aria-label': 'Needs you' },
89
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('inbox', 'sm'), 'Needs you ', count)), alertHost, ownNote, own, list, extra);
90
+ let sig = ''; let last = null; let ownSig = ''; let page = null;
91
+ const nothing = () => el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing needs you.');
92
+ const recount = () => {
93
+ const n = box.querySelectorAll('.nrow[data-needs-row]').length;
94
+ count.textContent = String(n);
95
+ const f = list.querySelector('.fold');
96
+ if (!n && !f) list.replaceChildren(nothing());
97
+ else if (n && f && list.children.length === 1 && f.textContent.startsWith('Nothing needs')) f.remove();
98
+ };
99
+ const ctx = { changed: recount };
100
+
101
+ const fillOwn = (j) => {
102
+ page = j;
103
+ if (!j || j.ok === false) { ownNote.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'Your own items could not be read: ' + ((j && j.error) || 'unreadable'))); return; }
104
+ ownNote.replaceChildren();
105
+ const rows = (j.needs || []).filter((r) => r.kind !== 'decision');
106
+ const g = JSON.stringify(rows.map((r) => [r.key, r.headline, r.days_late, !!r.evidence]));
107
+ if (g === ownSig) return; ownSig = g;
108
+ own.replaceChildren(...rows.map((r) => itemRow(r, { ...ctx, reload: () => import('./c3-brief-data.js').then((m) => m.loadBriefPage()) })));
109
+ recount();
110
+ };
111
+
112
+ const fill = (b) => {
113
+ last = b;
114
+ const inc = !!S.incident;
115
+ let needs = b && Array.isArray(b.needs) && b.needs.length ? b.needs.filter((n) => !BX.gone[n.key] && (!inc || n.hard)) : null;
116
+ const decide = ((b && b.cards) || []).filter((c) => c && (c.move || c.tag === 'Critical'));
117
+ const key = needs ? needs.map((n) => n.key + n.headline) : decide.map((c) => (c.key || '') + c.headline);
118
+ if (needs && S.pinNeed) { const at = needs.findIndex((n) => n.key === S.pinNeed); if (at > 0) needs.unshift(...needs.splice(at, 1)); }
119
+ const g = JSON.stringify([key, inc, !!(b && b.kept_out), S.pinNeed || '']);
120
+ if (g === sig) return; sig = g;
121
+ alertHost.replaceChildren(...(inc ? [el('div', { class: 'incident', role: 'status', 'data-c3pg': 'incident' }, c3ic('alert', 'lg'), el('div', null, el('b', null, 'Card payments failing since 07:40'), el('div', { class: 'dim c3s3' }, '12% of checkouts. Sam on call. Only deadlines shown.')))] : []));
122
+ if (needs) {
123
+ if (!quick.isConnected) box.querySelector('.sh2').append(quick);
124
+ quick.textContent = ''; quick.append(c3ic('bolt', 'sm'), needs.filter((n) => n.quick).length + ' under 2 min');
125
+ list.replaceChildren(...needs.slice(0, 5).map((n, i) => needRow(n, i, ctx)));
126
+ extra.replaceChildren(...(inc ? [] : belowLine(needs.slice(5), (n, i) => needRow(n, i + 5, ctx), b.kept_out)));
127
+ recount();
128
+ return;
129
+ }
130
+ const cards = decide.map(cardView);
131
+ quick.remove();
132
+ list.replaceChildren(...cards.map((v, i) => {
133
+ const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
134
+ return needRow({ key: v.key || String(i), headline: v.headline, kind: v.tag, icon: T.icon, color: T.color, time: v.deadline && v.deadline.text ? v.deadline.text : null, src: v.tag, view: v }, i, ctx);
135
+ }));
136
+ extra.replaceChildren();
137
+ recount();
138
+ };
139
+ S.refreshNeeds = () => { sig = ''; if (last) fill(last); };
140
+ S.needsFill = fill;
141
+ onBriefing((b) => { S.brief = b; fill(b); });
142
+ onBriefPage(fillOwn);
143
+ loadBriefing();
144
+ return box;
145
+ }