create-pmos 0.53.0 → 0.53.2

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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/README.md +2 -0
  7. package/workspace/REMOTE.md +107 -65
  8. package/workspace/bin/pmos.js +4 -2
  9. package/workspace/first-paint-audit.js +6 -2
  10. package/workspace/fixtures/initiatives-api/list.json +13 -0
  11. package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
  12. package/workspace/fixtures/report-api/draft.json +105 -0
  13. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
  14. package/workspace/guide/command-palette.md +1 -1
  15. package/workspace/guide/index.md +3 -1
  16. package/workspace/guide/initiatives-roadmap.md +1 -0
  17. package/workspace/guide/initiatives-screen.md +1 -0
  18. package/workspace/guide/launcher.md +51 -0
  19. package/workspace/guide/layout.md +7 -2
  20. package/workspace/guide/proactive-engine.md +21 -5
  21. package/workspace/guide/report-slides.md +28 -0
  22. package/workspace/guide/reports.md +2 -0
  23. package/workspace/lib/auto-sync.js +83 -0
  24. package/workspace/lib/board-fold.js +2 -2
  25. package/workspace/lib/breakdown-export.js +1 -6
  26. package/workspace/lib/brief.js +20 -2
  27. package/workspace/lib/goals.js +165 -0
  28. package/workspace/lib/initiative-rows.js +7 -3
  29. package/workspace/lib/launcher-cli.js +207 -0
  30. package/workspace/lib/plan-tree.js +15 -2
  31. package/workspace/lib/pmos-verbs.js +3 -0
  32. package/workspace/lib/proactive-cli.js +58 -2
  33. package/workspace/lib/proactive.js +2 -2
  34. package/workspace/lib/qr.js +293 -0
  35. package/workspace/lib/remote-access.js +184 -12
  36. package/workspace/lib/report-draft.js +34 -6
  37. package/workspace/lib/report-slides-data.js +141 -0
  38. package/workspace/lib/routes-initiatives.js +35 -2
  39. package/workspace/lib/routes-reports.js +3 -3
  40. package/workspace/lib/routes-timeline.js +1 -1
  41. package/workspace/lib/sync.js +61 -6
  42. package/workspace/package-lock.json +1 -1
  43. package/workspace/package.json +1 -1
  44. package/workspace/public/icons/apple-touch-icon.png +0 -0
  45. package/workspace/public/icons/icon-192.png +0 -0
  46. package/workspace/public/icons/icon-512.png +0 -0
  47. package/workspace/public/index.html +11 -0
  48. package/workspace/public/pm/app.js +55 -9
  49. package/workspace/public/pm/components.css +865 -1
  50. package/workspace/public/pm/dest-reports.js +8 -2
  51. package/workspace/public/pm/dest-today.js +8 -2
  52. package/workspace/public/pm/dock.js +73 -3
  53. package/workspace/public/pm/frame-copy.js +5 -0
  54. package/workspace/public/pm/initiative-list.js +18 -1
  55. package/workspace/public/pm/initiatives-copy.js +4 -0
  56. package/workspace/public/pm/panel-tabs.js +14 -12
  57. package/workspace/public/pm/print.css +11 -0
  58. package/workspace/public/pm/report-slides.js +438 -0
  59. package/workspace/public/pm/report-view.js +8 -2
  60. package/workspace/public/pm/reports-copy.js +3 -0
  61. package/workspace/public/pm/roadmap.js +23 -0
  62. package/workspace/public/pm/slides-copy.js +52 -0
  63. package/workspace/public/pm/today-copy.js +29 -0
  64. package/workspace/public/pm/today-goals.js +172 -0
  65. package/workspace/public/pm/tokens.css +13 -0
  66. package/workspace/public/pm/topbar.js +5 -2
  67. package/workspace/replaced-tests.json +36 -1
  68. package/workspace/server.js +165 -15
  69. package/workspace/skills/morning-brief.skill +15 -9
  70. package/workspace/skills/report-biweekly.skill +1 -0
  71. package/workspace/test-files.json +4 -0
  72. package/workspace/test-lists/run6.json +9 -0
  73. package/workspace/test-lists/workspace-launcher.json +5 -0
  74. package/workspace/test-lists/workspace-phone-app.json +5 -0
  75. package/workspace/test-lists/workspace-public-host.json +5 -0
@@ -0,0 +1,172 @@
1
+ /* pm/today-goals.js — the goals for today at the top of Today (initiative today-goals, PRD G5 and G6).
2
+ *
3
+ * goalsSection({ ws, setState }) → the section node, or null in fixture mode
4
+ * goalsSig(ws) → a string that changes whenever what the section shows changes
5
+ *
6
+ * The server joins the goals to what the morning-brief session read and to Today's cards (lib/goals.js planDay, GET /api/proactive/goals).
7
+ * This module draws that answer and sends the PM's presses: add, done, drop, reopen, remove, "Prepare my day" (runs the
8
+ * morning-brief schedule) and, on an agent step that names an initiative, Prepare then Queue it (the shipped background
9
+ * prepare: one approval is one prompt hash).
10
+ *
11
+ * FIRST PAINT. Folded, the section is one control (its toggle) and a list of the goals as text, so Today keeps inside its
12
+ * control budget (first-paint-today.test.js). The input and the buttons are drawn only when the PM opens it.
13
+ */
14
+
15
+ import { el } from './dom.js';
16
+ import { button, field, badge, toast } from './primitives.js';
17
+ import { pmGet, pmPost } from './http.js';
18
+ import { FIXTURE } from './state.js';
19
+ import { COPY } from './today-copy.js';
20
+
21
+ const G = COPY.goals;
22
+ const POLL_MS = 30000;
23
+ const cache = new Map(); // ws -> { data, error, at }
24
+ let open = false;
25
+ let draft = '';
26
+ const prepared = new Map(); // step ref -> { key, branch, item_id, prompt_sha, initiative_id, ws } (a plan shown, not yet queued)
27
+ let rerender = () => {};
28
+ let timer = null;
29
+
30
+ async function load(ws) {
31
+ const out = await pmGet('/api/proactive/goals' + (ws ? '?ws=' + encodeURIComponent(ws) : ''));
32
+ const prev = cache.get(ws);
33
+ const next = out && out.ok ? { data: out, error: null, at: Date.now() } : { data: prev ? prev.data : null, error: (out && out.error) || G.unreached, at: Date.now() };
34
+ cache.set(ws, next);
35
+ if (!prev || JSON.stringify(prev.data) !== JSON.stringify(next.data) || prev.error !== next.error) rerender();
36
+ }
37
+
38
+ function ensure(ws) {
39
+ if (!cache.has(ws)) { cache.set(ws, { data: null, error: null, at: 0 }); load(ws); }
40
+ if (!timer && typeof window !== 'undefined') {
41
+ timer = setInterval(() => { for (const w of cache.keys()) load(w); }, POLL_MS);
42
+ }
43
+ }
44
+
45
+ async function press(ws, body) {
46
+ const r = await pmPost('/api/proactive/goals', body);
47
+ if (!r || r.ok === false) { toast({ text: G.refused((r && r.error) || G.unreached), tone: 'destructive' }); return false; }
48
+ await load(ws);
49
+ return true;
50
+ }
51
+
52
+ async function prepareDay(ws) {
53
+ const r = await pmPost('/api/proactive/goals/plan', { ws });
54
+ toast({ text: r && r.ok ? G.started : G.refused((r && r.error) || G.unreached), tone: r && r.ok ? undefined : 'destructive' });
55
+ load(ws);
56
+ }
57
+
58
+ async function prepareStep(ws, st) {
59
+ const r = await pmPost('/api/proactive/prepare/plan', { ws, initiative_id: st.initiative_id });
60
+ if (!r || r.ok === false) { toast({ text: G.refused((r && r.error) || G.unreached), tone: 'destructive' }); return; }
61
+ if (r.refused) { toast({ text: G.refused(r.refused.why) }); return; }
62
+ prepared.set(st.ref, { key: r.item.key, branch: r.plan && r.plan.branch, item_id: r.item.id, prompt_sha: r.prompt_sha, initiative_id: st.initiative_id, ws: r.ws || ws });
63
+ rerender();
64
+ }
65
+
66
+ async function queueStep(ref) {
67
+ const p = prepared.get(ref);
68
+ if (!p) return;
69
+ const r = await pmPost('/api/proactive/prepare/queue', { confirm: true, ws: p.ws, initiative_id: p.initiative_id, item_id: p.item_id, prompt_sha: p.prompt_sha });
70
+ prepared.delete(ref);
71
+ toast({ text: r && r.ok ? G.queued(p.key) : G.refused((r && r.error) || G.unreached), tone: r && r.ok ? undefined : 'destructive' });
72
+ rerender();
73
+ }
74
+
75
+ const hhmm = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? String(iso) : `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
76
+
77
+ function stepNode(ws, st, opened) {
78
+ const who = st.owner === 'pm' ? G.you : G.agent;
79
+ const meta = [st.source, st.mention ? G.mention : null, st.effort].filter(Boolean).join(' · ');
80
+ const p = prepared.get(st.ref);
81
+ const acts = !opened || !st.prepare ? null
82
+ : p ? el('span', { class: 'td-goal__prep' }, el('span', { class: 'td-goal__prep-text' }, G.prepareShows(p.key, p.branch)),
83
+ button({ label: G.queue, size: 'sm', attrs: { 'data-goal-queue': st.ref }, onClick: () => queueStep(st.ref) }))
84
+ : button({ label: G.prepare, size: 'sm', variant: 'ghost', attrs: { 'data-goal-prepare': st.ref }, onClick: () => prepareStep(ws, st) });
85
+ return el('li', { class: 'td-goal__step', 'data-goal-step': st.ref },
86
+ badge({ text: who, tone: st.owner === 'pm' ? 'outline' : 'muted' }),
87
+ el('span', { class: 'td-goal__step-title' }, st.title),
88
+ meta ? el('span', { class: 'td-goal__meta' }, meta) : null,
89
+ acts);
90
+ }
91
+
92
+ function goalNode(ws, g, opened) {
93
+ const closed = g.status !== 'open';
94
+ const ctrls = opened ? el('span', { class: 'td-goal__ctrls' },
95
+ ...(closed
96
+ ? [button({ label: G.reopen, size: 'sm', variant: 'ghost', attrs: { 'data-goal-act': 'reopen' }, onClick: () => press(ws, { action: 'reopen', id: g.id }) })]
97
+ : [button({ label: G.done, size: 'sm', attrs: { 'data-goal-act': 'done' }, onClick: () => press(ws, { action: 'done', id: g.id }) }),
98
+ button({ label: G.drop, size: 'sm', variant: 'ghost', attrs: { 'data-goal-act': 'drop' }, onClick: () => press(ws, { action: 'drop', id: g.id }) })]),
99
+ button({ label: G.remove, size: 'sm', variant: 'ghost', attrs: { 'data-goal-act': 'remove' }, onClick: () => press(ws, { action: 'remove', id: g.id }) })) : null;
100
+ const ctx = (g.context || []).map((e) => el('li', { class: 'td-goal__ctx' }, `${hhmm(e.start)} ${e.title}${e.with ? ' · ' + e.with : ''}`));
101
+ const body = opened && !closed ? el('div', { class: 'td-goal__body' },
102
+ ctx.length ? el('ul', { class: 'td-goal__ctx-list' }, ...ctx) : null,
103
+ g.steps.length ? el('ul', { class: 'td-goal__steps' }, ...g.steps.map((st) => stepNode(ws, st, opened))) : el('p', { class: 'td-goal__none' }, G.noStep)) : null;
104
+ return el('li', { class: 'td-goal' + (closed ? ' td-goal--closed' : ''), 'data-goal': g.id, 'data-goal-coverage': g.coverage },
105
+ el('div', { class: 'td-goal__head' },
106
+ el('span', { class: 'td-goal__text' }, g.text),
107
+ badge({ text: G.coverage[g.coverage] || g.coverage, tone: g.coverage === 'needs-plan' ? 'warning' : 'muted' }),
108
+ g.initiative_id ? el('span', { class: 'td-goal__meta' }, g.initiative_id) : null,
109
+ ctrls),
110
+ body);
111
+ }
112
+
113
+ function stepList(ws, title, steps, attr) {
114
+ if (!steps.length) return null;
115
+ return el('div', { class: 'td-goals__also', 'data-goals': attr },
116
+ el('p', { class: 'td-goals__sub' }, title),
117
+ el('ul', { class: 'td-goal__steps' }, ...steps.map((st) => stepNode(ws, st, true))));
118
+ }
119
+
120
+ /** The section. `ws` is the repository whose Today cards the goals are joined to. */
121
+ export function goalsSection({ ws, setState } = {}) {
122
+ if (FIXTURE) return null;
123
+ // like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
124
+ // cause a whole-page render under whatever the PM is doing elsewhere
125
+ if (setState) rerender = () => { if (document.querySelector('[data-today="goals"]')) setState({}); };
126
+ ensure(ws);
127
+ const c = cache.get(ws) || {};
128
+ const d = c.data;
129
+ const plan = d && d.plan;
130
+ const goals = plan ? plan.goals : [];
131
+ const openN = goals.filter((g) => g.status === 'open').length;
132
+ const toggle = button({ label: G.toggle(openN, goals.length), variant: 'ghost', iconEnd: open ? 'chevron-down' : 'chevron-right',
133
+ attrs: { 'data-goals': 'toggle', 'aria-expanded': open ? 'true' : 'false' }, onClick: () => { open = !open; rerender(); } });
134
+ const list = goals.length ? el('ol', { class: 'td-goals__list', 'data-goals': 'list' }, ...goals.map((g) => goalNode(ws, g, open))) : null;
135
+ const kids = [el('div', { class: 'td-goals__head' }, toggle), list];
136
+ if (c.error) kids.push(el('p', { class: 'td-goals__note', role: 'status' }, G.refused(c.error)));
137
+ if (open) {
138
+ if (!goals.length) kids.push(el('p', { class: 'td-goals__note' }, G.intro));
139
+ const max = (d && d.max_goals) || 7;
140
+ if (goals.length < max) {
141
+ const f = field({ label: G.inputLabel, value: draft, placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', maxlength: '140' },
142
+ onCommit: async (v) => {
143
+ draft = v;
144
+ if (!String(v).trim()) return;
145
+ if (await press(ws, { action: 'add', text: v })) { draft = ''; if (f.control.resetCommitted) f.control.resetCommitted(''); rerender(); }
146
+ } });
147
+ f.control.addEventListener('input', () => { draft = f.control.value; }); // a rebuild keeps what is typed
148
+ kids.push(f);
149
+ } else kids.push(el('p', { class: 'td-goals__note' }, G.full(max)));
150
+ kids.push(el('div', { class: 'td-goals__bar' },
151
+ button({ label: G.prepareDay, attrs: { 'data-goals': 'prepare-day', title: G.prepareDayHint }, disabled: !openN, onClick: () => prepareDay(ws) })));
152
+ if (plan) {
153
+ const lines = [];
154
+ if (!plan.brief) lines.push(G.noBrief);
155
+ else if (!plan.brief.fresh) lines.push(G.stale(plan.brief.date));
156
+ if (plan.time) {
157
+ lines.push(G.free(plan.time.free_minutes, plan.time.until));
158
+ if (plan.time.next) lines.push(G.comingUp(plan.time.next.title, hhmm(plan.time.next.start)));
159
+ }
160
+ if (plan.not_read && plan.not_read.length) lines.push(G.notRead(plan.not_read.map((x) => x.source).join(', ')));
161
+ if (lines.length) kids.push(el('p', { class: 'td-goals__note', 'data-goals': 'context' }, lines.join(' ')));
162
+ kids.push(stepList(ws, G.also, plan.also || [], 'also'));
163
+ kids.push(stepList(ws, G.worthAsking, plan.worth_asking || [], 'worth-asking'));
164
+ }
165
+ }
166
+ return el('section', { class: 'td-goals', 'data-today': 'goals', 'aria-label': G.title }, ...kids.filter(Boolean));
167
+ }
168
+
169
+ export function goalsSig(ws) {
170
+ const c = cache.get(ws) || {};
171
+ return JSON.stringify([open, c.data && c.data.plan, c.error, [...prepared.keys()]]);
172
+ }
@@ -66,6 +66,19 @@
66
66
  --term-bg:oklch(0.148 0.004 228.8);
67
67
  --mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
68
68
  --sans:"Space Grotesk",ui-sans-serif,system-ui,sans-serif;
69
+ /* the biweekly slides (Run 6): the PM-OS slide palette, the same in both themes because a slide is a printed page */
70
+ --slide-bg:oklch(1 0 0);--slide-fg:oklch(0.148 0.004 228.8);--slide-muted:oklch(0.56 0.021 213.5);
71
+ --slide-foot:oklch(0.556 0 0);--slide-rule:#d8d6cb;--slide-border:oklch(0.925 0.005 214.3);
72
+ --slide-secondary:oklch(0.967 0.001 286.375);--slide-track:oklch(0.963 0.002 197.1);
73
+ --slide-brand:#2433a4;--slide-primary:oklch(0.488 0.243 264.376);--slide-overdue:oklch(0.577 0.245 27.325);
74
+ --slide-stage:oklch(0 0 0);--slide-open:#94a3b8;--slide-done:#10b981;
75
+ --slide-ph-intake:#8b5cf6;--slide-ph-spec:#f59e0b;--slide-ph-build:#3b82f6;--slide-ph-evaluate:#06b6d4;
76
+ --slide-ph-handoff:#10b981;--slide-ph-other:#64748b;
77
+ --slide-now:#1a56db;--slide-next:#d97706;--slide-later:#64748b;
78
+ --slide-launch:#047857;--slide-launch-bg:#10b9811a;--slide-improvement:#2f6feb;--slide-improvement-bg:#2f6feb1a;
79
+ --slide-fix:#92400e;--slide-fix-bg:#f59e0b1f;--slide-security:#b91c1c;--slide-security-bg:#dc26261a;
80
+ --slide-deprecation:#4b5563;--slide-deprecation-bg:#4b55631a;--slide-beta:#6d28d9;--slide-beta-bg:#6d28d91a;
81
+ --slide-note-info:#2563eb;--slide-note-warning:#b45309;
69
82
  }
70
83
  [data-pm-theme="light"],html[data-pm-page-theme="light"]{
71
84
  --background:oklch(1 0 0);--foreground:oklch(0.148 0.004 228.8);
@@ -25,9 +25,12 @@ import { COPY } from './frame-copy.js';
25
25
  /** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. */
26
26
  export const BAR_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports']);
27
27
 
28
- /** Is this sync reading "not in sync"? Behind, ahead or records not yet recorded. An unreadable status is not shown. */
28
+ /** Is this sync reading "not in sync"? Behind, ahead, records not yet recorded, or `remote_stale`: no fetch in the last
29
+ * few minutes, so another machine's push would be invisible here and "in sync" cannot be claimed. An unreadable status
30
+ * is not shown. */
29
31
  export function unsyncedOf(st) {
30
- return !!st && st.ok !== false && ((st.behind | 0) > 0 || (st.ahead | 0) > 0 || (st.dirty_records | 0) > 0);
32
+ return !!st && st.ok !== false
33
+ && ((st.behind | 0) > 0 || (st.ahead | 0) > 0 || (st.dirty_records | 0) > 0 || st.remote_stale === true);
31
34
  }
32
35
 
33
36
  export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
@@ -23,7 +23,42 @@
23
23
  "run": "2A",
24
24
  "ref": "contract-run2 done_criteria 14 (the plan-routes.test.js Origin cases)",
25
25
  "reason": "R9 asserted the route's own no-Origin text, which named the terminal route. After Run 2 the route runs pm-guard first, so R9 now asserts pm-guard's refusal; the 403 and the nothing-written check stay.",
26
- "names": ["R9 a POST with no Origin is 403 and names the terminal route; nothing is written"]
26
+ "names": [
27
+ "R9 a POST with no Origin is 403 and names the terminal route; nothing is written"
28
+ ]
29
+ },
30
+ {
31
+ "id": "STG-SLACK",
32
+ "file": "workspace/proactive.test.js",
33
+ "run": "today-goals",
34
+ "ref": "planning/prd/today-goals-prd.md G2 (slack becomes a brief source)",
35
+ "reason": "These four assertions used slack as the example of an unknown brief source. today-goals G2 makes slack a known source, so each now uses teams as the unknown name; the refusal and its reason are asserted unchanged.",
36
+ "names": [
37
+ "G5 a non-object, a bad date and an unknown source are refused with the reason",
38
+ "G6 a confidence outside 0 to 1, a bad owner, a bad effort, a missing why_now and an unknown item source are each refused",
39
+ "G14 the new settings are checked: name length, source names, booleans",
40
+ "I6 an unknown brief source is refused with the allowed names"
41
+ ]
42
+ },
43
+ {
44
+ "id": "S6-D86",
45
+ "file": "workspace/d86-seams.test.js",
46
+ "run": "6",
47
+ "ref": "Run 6 contract, 'Deliberate test and budget changes' (planning/evals/pm-os-absorb-eval-rubric.md)",
48
+ "reason": "The draft golden gains the slides key: the drafter now writes the slide data with the draft (PRD R22.7). The assertion keeps its name and still checks every D78 section and the reason rule.",
49
+ "names": [
50
+ "10 the draft golden has every section of the D78 frame (headline, timeline, blocks, new, decided, needs your call, sources), and every block not on track has a reason"
51
+ ]
52
+ },
53
+ {
54
+ "id": "S6-FP",
55
+ "file": "workspace/first-paint-count.test.js",
56
+ "run": "6",
57
+ "ref": "Run 6 contract, 'Deliberate test and budget changes' (planning/evals/pm-os-absorb-eval-rubric.md)",
58
+ "reason": "The Reports budget rose to 15 for Present, but specimen A did not change. The calibration now reads SPECIMEN_A, specimen A's own counts, instead of EXPECT, the budgets. The name and the four numbers are unchanged.",
59
+ "names": [
60
+ "(a) calibration: the audit on specimen A gives exactly Today 17, Initiatives 24, Board 33, Reports 14 outside the dock"
61
+ ]
27
62
  }
28
63
  ],
29
64
  "entries": [
@@ -94,6 +94,8 @@ const judgmentLib = require('./lib/judgment');
94
94
  const proactiveLib = require('./lib/proactive');
95
95
  const learnLib = require('./lib/learn');
96
96
  const briefLib = require('./lib/brief');
97
+ const goalsLib = require('./lib/goals');
98
+ const todayLib = require('./lib/today');
97
99
  const prepareQueue = require('./lib/prepare-queue');
98
100
  const harness = require('./lib/harness-parse');
99
101
  const transcript = require('./lib/transcript-read');
@@ -1787,6 +1789,14 @@ function readLedger(w, basename) {
1787
1789
  // D70 — the lifecycle ledger: the latest line for an initiative wins; `active` reopens. The same rule
1788
1790
  // as scripts/workflow-state.js#readLifecycle (a parity case in workspace/test.js keeps them in step).
1789
1791
  const syncing = new Set(); // repositories with a sync in flight — one at a time
1792
+ // D70 as amended 2026-10-06: the same Sync, run without the press at startup and when work sits unpushed and quiet.
1793
+ // Every rule it must pass is in lib/auto-sync.js; a scratch server (its own PMOS_STATE_DIR) does not auto-sync.
1794
+ const autoSync = require('./lib/auto-sync').createAutoSync({
1795
+ roots: () => [...new Set([...workspaces.values()].map((w) => w.root))],
1796
+ busy: syncing,
1797
+ onDone: (root) => boardCache.invalidate(root),
1798
+ label: `${os.hostname()} (auto-sync)`,
1799
+ });
1790
1800
  const STOPPED_STATES = ['closed', 'on-hold', 'cancelled'];
1791
1801
  function readLifecycle(w) {
1792
1802
  const latest = new Map();
@@ -2315,7 +2325,7 @@ function checkDueSchedules() {
2315
2325
  // PMOS_TICK_MS exists for the suites (same footing as PMOS_LIVENESS_MS): proactive-prepare.test.js
2316
2326
  // used to wait on two real minutes of this clock. Unset, the interval is the 60 seconds it always was.
2317
2327
  const TICK_MS = Math.max(200, Number(process.env.PMOS_TICK_MS) || 60000);
2318
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2328
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } autoSync.tick().catch(() => {}); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2319
2329
 
2320
2330
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2321
2331
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -3621,6 +3631,21 @@ function prepareTick({ manual = false } = {}) {
3621
3631
  const PRO_SIGNALS = () => path.join(STATE, 'signals.jsonl');
3622
3632
  const PRO_FEEDBACK = () => path.join(STATE, 'proactive-feedback.jsonl');
3623
3633
  const PRO_SETTINGS = () => path.join(STATE, 'proactive-settings.json');
3634
+ // today-goals: one goals file per local day, and the newest stored brief (the same read GET /api/proactive/brief makes).
3635
+ const GOALS_DIR = () => path.join(STATE, 'goals');
3636
+ const goalsCardsMemo = new Map(); // ws -> { at, cards, error }: GET /api/proactive/goals reads Today's cards at most every 30 s
3637
+ function readGoalsDay(date) {
3638
+ let raw = null;
3639
+ try { raw = JSON.parse(fs.readFileSync(path.join(GOALS_DIR(), date + '.json'), 'utf8')); } catch { /* none yet */ }
3640
+ return goalsLib.readDay(raw, date);
3641
+ }
3642
+ function latestBrief() {
3643
+ try {
3644
+ const dir = path.join(STATE, 'briefs');
3645
+ const f = fs.readdirSync(dir).filter((n) => /^\d{4}-\d{2}-\d{2}\.json$/.test(n)).sort().pop();
3646
+ return f ? JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) : null;
3647
+ } catch { return null; }
3648
+ }
3624
3649
  let proLastScan = 0;
3625
3650
  const PRO_SCAN_EVERY_MS = 10 * 60 * 1000;
3626
3651
  function proSettings() {
@@ -4977,6 +5002,29 @@ async function handleRequest(req, res) {
4977
5002
  res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'max-age=86400' });
4978
5003
  return res.end(svg);
4979
5004
  }
5005
+ // workspace-phone-app: what Add to Home Screen reads, so the phone opens the workspace full screen
5006
+ // with its own icon. The manifest is built here rather than shipped as a file because its colours
5007
+ // are the dark theme's tokens and its start_url must be the bare page: never a ?token= URL, which
5008
+ // would store the secret in the phone's home-screen entry. No service worker, on purpose: this is
5009
+ // live state, and an offline cache would show a stale board as if it were current.
5010
+ if (p === '/manifest.webmanifest') {
5011
+ res.writeHead(200, { 'Content-Type': 'application/manifest+json', 'Cache-Control': 'no-cache' });
5012
+ return res.end(JSON.stringify({
5013
+ name: 'PMOS workspace', short_name: 'PMOS', id: '/', start_url: '/', scope: '/',
5014
+ display: 'standalone', orientation: 'any', background_color: '#090b0c', theme_color: '#090b0c',
5015
+ icons: [
5016
+ { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
5017
+ { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
5018
+ // The mark sits inside the central 62%, so the same image survives an Android maskable crop.
5019
+ { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
5020
+ ],
5021
+ }));
5022
+ }
5023
+ // The three icons, by name (an allowlist, like /lib/ above). iOS also asks for /apple-touch-icon.png
5024
+ // at the root unprompted, so that path answers too.
5025
+ if (p === '/icons/icon-192.png' || p === '/icons/icon-512.png' || p === '/icons/apple-touch-icon.png' || p === '/apple-touch-icon.png') {
5026
+ return serveFile(res, path.join(PUB, 'icons', path.basename(p)), 'image/png');
5027
+ }
4980
5028
  // xterm assets straight from node_modules (offline; no CDN)
4981
5029
  if (p === '/vendor/xterm.js') return serveFile(res, path.join(XROOT, 'xterm', 'lib', 'xterm.js'));
4982
5030
  if (p === '/vendor/xterm.css') return serveFile(res, path.join(XROOT, 'xterm', 'css', 'xterm.css'));
@@ -6446,11 +6494,11 @@ async function handleRequest(req, res) {
6446
6494
  // Local only — no network. Safe to poll; it is what tells the PM whether a press is needed.
6447
6495
  const w = getWs(url.searchParams.get('ws'));
6448
6496
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6449
- return json(res, { ws: w.id, name: w.name, ...(await syncLib.status(w.root)) });
6497
+ return json(res, { ws: w.id, name: w.name, ...(await syncLib.status(w.root)), auto: autoSync.lastOf(w.root) });
6450
6498
  }
6451
6499
  if (p === '/api/sync' && req.method === 'POST') {
6452
- // D70 — the ONE place the workspace runs fetch / integrate / commit / push. Only the PM's press
6453
- // reaches it; nothing schedules or chains it, and it is off every other action's request path.
6500
+ // D70 — the PM's press. The only other caller of the same sync is lib/auto-sync.js (D70 as amended
6501
+ // 2026-10-06), under its own rules; neither is on any other action's request path.
6454
6502
  let body = {};
6455
6503
  try { body = JSON.parse((await readBody(req)) || '{}'); }
6456
6504
  catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
@@ -6911,6 +6959,66 @@ async function handleRequest(req, res) {
6911
6959
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
6912
6960
  return json(res, { ok: true, ws: w.id, name: w.name, ...validateLib.readFindings(validationFindingsFile(w)) });
6913
6961
  }
6962
+ /* ── today-goals (G1, G4, G5): the PM's goals for the day, the plan that joins them to the brief and Today, and "plan my
6963
+ * day". The goals routes write only goals/<date>.json in the state folder. "Plan my day" seeds the morning-brief
6964
+ * schedule when the repository has none and runs it through fireSchedule, the run-now path. Nothing else starts. ── */
6965
+ if (p === '/api/proactive/goals' && req.method === 'GET') {
6966
+ const now = Date.now();
6967
+ const date = briefLib.localDate(now);
6968
+ const w = getWs(url.searchParams.get('ws'));
6969
+ const day = readGoalsDay(date);
6970
+ const brief = latestBrief();
6971
+ let cards = []; let cardsError = null;
6972
+ // Today's cards are read only when an open goal can take them, and at most once per 30 seconds per repository: the
6973
+ // read is synchronous and heavy on a large repository, and the Today feed itself must not wait behind it.
6974
+ if (w && day.goals.some((x) => x.status === 'open')) {
6975
+ const memo = goalsCardsMemo.get(w.id);
6976
+ if (memo && now - memo.at < 30000) ({ cards, error: cardsError } = memo);
6977
+ else {
6978
+ let sess = []; try { sess = sessionList(); } catch { /* no session cards */ }
6979
+ const t = todayLib.todayFor(w, { stateDir: stateDirFor(w), sessions: sess, registered: (root) => { const x = workspaces.get(wsId(root)); return !!x && path.resolve(x.root) === path.resolve(root); } });
6980
+ if (t.ok) cards = t.cards; else cardsError = t.error;
6981
+ goalsCardsMemo.set(w.id, { at: now, cards, error: cardsError });
6982
+ }
6983
+ }
6984
+ const plan = goalsLib.planDay({ day, brief, cards, now });
6985
+ const sched = readSchedules().find((x) => x.skill === 'morning-brief' && (!w || x.ws === w.id)) || null;
6986
+ return json(res, { ok: true, ws: w ? w.id : null, today: date, plan, cards_error: cardsError, max_goals: goalsLib.MAX_GOALS,
6987
+ schedule: sched ? { id: sched.id, time: sched.time, lastRun: sched.lastRun, lastStatus: sched.lastStatus } : null,
6988
+ note: 'Goals are kept in the workspace state folder on this computer. The plan joins them to the morning brief and Today by goal id or initiative, never by guessing.' });
6989
+ }
6990
+ if (p === '/api/proactive/goals' && req.method === 'POST') {
6991
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6992
+ const now = Date.now();
6993
+ const date = briefLib.localDate(now);
6994
+ const r = goalsLib.edit(readGoalsDay(date), body, { now });
6995
+ if (!r.ok) return json(res, { ok: false, error: r.error }, /no goal with that id/.test(r.error) ? 404 : 400);
6996
+ try { fs.mkdirSync(GOALS_DIR(), { recursive: true }); fs.writeFileSync(path.join(GOALS_DIR(), date + '.json'), JSON.stringify(r.day) + '\n'); }
6997
+ catch (e) { warnWriteFail('goals', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
6998
+ return json(res, { ok: true, day: r.day, goal: r.goal });
6999
+ }
7000
+ if (p === '/api/proactive/goals/plan' && req.method === 'POST') {
7001
+ // A PM press: make sure the morning brief is scheduled for this repository, then run it now. The session reads the
7002
+ // goals with `pmos proactive goals` and submits a brief whose items name them.
7003
+ let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
7004
+ const w = getWs(body.ws);
7005
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
7006
+ const open = readGoalsDay(briefLib.localDate(Date.now())).goals.filter((g) => g.status === 'open');
7007
+ if (!open.length) return json(res, { ok: false, error: 'add a goal for today first: the plan is made for your goals' }, 409);
7008
+ let sched = readSchedules().find((x) => x.ws === w.id && x.skill === 'morning-brief');
7009
+ let seeded = false;
7010
+ if (!sched) {
7011
+ if (!taskById('morning-brief', w.root)) return json(res, { ok: false, error: 'the morning-brief skill is not installed for this repository' }, 409);
7012
+ const norm = scheduleLogic.normalizeSchedule({ ...SEED_SETS.brief[0], task: 'morning-brief' });
7013
+ if (!norm.ok) return json(res, { ok: false, error: norm.error }, 500);
7014
+ sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null };
7015
+ sched.nextRun = scheduleLogic.nextRun(sched);
7016
+ const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
7017
+ }
7018
+ const result = fireSchedule(sched);
7019
+ return json(res, { ok: !!result.ok, seeded, schedule: { id: sched.id, time: sched.time }, result, goals: open.length,
7020
+ ...(result.ok ? {} : { error: result.reason || 'the morning-brief session did not start' }) }, result.ok ? 200 : 500);
7021
+ }
6914
7022
  /* ── background prepare (D3): plan is read-only; queue records an approval; the runner starts it later ── */
6915
7023
  if (p === '/api/proactive/prepare/plan' && req.method === 'POST') {
6916
7024
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
@@ -6984,7 +7092,8 @@ async function handleRequest(req, res) {
6984
7092
  const dir = path.join(STATE, 'briefs');
6985
7093
  if (req.method === 'POST') {
6986
7094
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6987
- const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings() });
7095
+ const bday = body && typeof body.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(body.date) ? body.date : briefLib.localDate(Date.now());
7096
+ const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings(), goalIds: readGoalsDay(bday).goals.map((g) => g.id) });
6988
7097
  if (!v.ok) return json(res, { ok: false, error: v.error }, 400);
6989
7098
  try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, v.brief.date + '.json'), JSON.stringify(v.brief) + '\n'); }
6990
7099
  catch (e) { warnWriteFail('briefs', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
@@ -7377,7 +7486,7 @@ async function handleRequest(req, res) {
7377
7486
  return json(res, { ok: true, changes: rows.slice(-200).reverse(), total: rows.length });
7378
7487
  }
7379
7488
  // pm-os-absorb Runs 2–5 — the planning layer's routes, one file per run (lib/route-modules.js). Last, so none shadows a route above.
7380
- if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList })) return;
7489
+ if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex })) return;
7381
7490
  res.writeHead(404); res.end('not found');
7382
7491
  }
7383
7492
 
@@ -7443,25 +7552,57 @@ function writeChangeContext(dir) {
7443
7552
  catch { return null; }
7444
7553
  }
7445
7554
 
7446
- // ── Access guard (D62): loopback is token-free; every non-loopback peer needs the bearer token. ──
7555
+ // ── Access guard (D62): loopback is token-free; every non-loopback or proxied request needs the bearer token. ──
7447
7556
  // Peer-based: what matters is where the request came FROM, not which listener accepted it. With the
7448
7557
  // remote flag off there is no non-loopback listener, so this is transparent for local dev. Headers
7449
7558
  // and the token are never logged (D08).
7450
7559
  function httpGuard(req, res) {
7451
7560
  // Origin/Host allowlist FIRST (D64 A1): reject a cross-site / DNS-rebinding request before the
7452
7561
  // token exemption for loopback can be reached. Header values are never logged (D08).
7453
- if (!remoteAccess.checkHostOrigin({ origin: req.headers.origin, host: req.headers.host, binds: BIND.binds })) {
7562
+ if (!remoteAccess.checkHostOrigin({ origin: req.headers.origin, host: req.headers.host, binds: BIND.binds, publicHost: BIND.publicHost })) {
7454
7563
  res.writeHead(403, { 'Content-Type': 'application/json' });
7455
7564
  res.end(JSON.stringify({ ok: false, error: 'forbidden origin/host' }));
7456
7565
  return;
7457
7566
  }
7567
+ // workspace-public-host: a request to the public hostname must have passed Cloudflare Access. A tunnel
7568
+ // with no Access application in front fails here, before even the sign-in form can be reached.
7569
+ if (!remoteAccess.edgeAllowed({ headers: req.headers, publicHost: BIND.publicHost })) {
7570
+ res.writeHead(403, { 'Content-Type': 'application/json' });
7571
+ res.end(JSON.stringify({ ok: false, error: 'the public host is only reachable through Cloudflare Access' }));
7572
+ return;
7573
+ }
7574
+ // The cookie is Secure when the browser reached us on the public host (HTTPS at the edge).
7575
+ const secureCookie = !!BIND.publicHost && remoteAccess.isProxied(req.headers, BIND.publicHost);
7458
7576
  // Token may arrive as an Authorization header, a ?token= query param, or the seeded cookie. A
7459
7577
  // browser only carries the token on the FIRST navigation URL — it cannot attach a header/query to
7460
7578
  // sub-resource loads (CSS/JS) or the WS upgrade — so the documented "open http://…/?token=<t>"
7461
7579
  // phone flow (REMOTE.md + startup banner) must reach THIS page-load guard, and every later request
7462
7580
  // rides the cookie we seed just below. presentedToken() resolves all channels. (local hotfix)
7463
- if (!remoteAccess.authorize({ remoteAddress: req.socket.remoteAddress,
7581
+ // workspace-phone-app: the sign-in form posts here. It is the one route an unauthorized peer may
7582
+ // reach, and all it can do is compare a token (constant-time) and, on a match, seed the same
7583
+ // cookie a valid ?token= navigation seeds. The body is capped; nothing about it is logged (D08).
7584
+ const navigation = remoteAccess.isPageNavigation({ method: req.method, accept: req.headers.accept });
7585
+ if (req.method === 'POST' && (req.url || '').split('?')[0] === remoteAccess.AUTH_PATH) {
7586
+ let body = '';
7587
+ req.on('data', (c) => { body += c; if (body.length > 4096) req.destroy(); });
7588
+ req.on('end', () => {
7589
+ if (remoteAccess.tokenMatches(remoteAccess.parseAuthForm(body), TOKEN)) {
7590
+ res.writeHead(303, { Location: '/', 'Set-Cookie': remoteAccess.sessionCookie(TOKEN, { secure: secureCookie }), 'Cache-Control': 'no-store' });
7591
+ return res.end();
7592
+ }
7593
+ res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
7594
+ res.end(remoteAccess.signInPage({ failed: true }));
7595
+ });
7596
+ return;
7597
+ }
7598
+ if (!remoteAccess.authorize({ remoteAddress: req.socket.remoteAddress, headers: req.headers, publicHost: BIND.publicHost,
7464
7599
  presentedToken: remoteAccess.presentedToken(req), expectedToken: TOKEN })) {
7600
+ // A phone's browser gets a page it can act on, not JSON. API calls, scripts and anything else
7601
+ // that is not a page load keep the JSON 401 they had.
7602
+ if (navigation) {
7603
+ res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8', 'WWW-Authenticate': 'Bearer', 'Cache-Control': 'no-store' });
7604
+ return res.end(req.method === 'HEAD' ? undefined : remoteAccess.signInPage());
7605
+ }
7465
7606
  res.writeHead(401, { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer' });
7466
7607
  res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
7467
7608
  return;
@@ -7470,12 +7611,18 @@ function httpGuard(req, res) {
7470
7611
  // navigation, so the browser's subsequent sub-resource + WS-upgrade requests authenticate without it
7471
7612
  // in the URL. HttpOnly (JS never needs it) + SameSite=Strict + the D64 Origin/Host allowlist keep a
7472
7613
  // foreign site from riding it; no Secure flag because the tailnet speaks plain HTTP (WireGuard-encrypted).
7473
- if (!remoteAccess.isLoopbackAddr(req.socket.remoteAddress)) {
7614
+ if (!remoteAccess.isLoopbackAddr(req.socket.remoteAddress) || remoteAccess.isProxied(req.headers, BIND.publicHost)) {
7474
7615
  let qTok = null;
7475
7616
  try { qTok = new URL(req.url || '', 'http://localhost').searchParams.get('token'); } catch { /* ignore */ }
7476
7617
  if (qTok && remoteAccess.tokenMatches(qTok.trim(), TOKEN)) {
7477
- res.setHeader('Set-Cookie',
7478
- `${remoteAccess.COOKIE_NAME}=${encodeURIComponent(TOKEN)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=604800`);
7618
+ res.setHeader('Set-Cookie', remoteAccess.sessionCookie(TOKEN, { secure: secureCookie }));
7619
+ // workspace-phone-app: on a page load, redirect to the same URL without the token, so it does
7620
+ // not stay in the address bar, the history, or the start_url of a home-screen install.
7621
+ const clean = navigation ? remoteAccess.stripTokenParam(req.url) : null;
7622
+ if (clean) {
7623
+ res.writeHead(303, { Location: clean, 'Cache-Control': 'no-store' });
7624
+ return res.end();
7625
+ }
7479
7626
  }
7480
7627
  }
7481
7628
  return handleRequest(req, res);
@@ -7490,14 +7637,15 @@ function handleUpgrade(req, socket, head) {
7490
7637
  // Same origin/host allowlist on the WS upgrade handshake (D64 A1): a cross-origin page must not be
7491
7638
  // able to open ws://127.0.0.1:<port> and spawn/type into a shell. Rejected BEFORE the socket is
7492
7639
  // accepted, and before loopback's token exemption applies. Header values never logged (D08).
7493
- if (!remoteAccess.checkHostOrigin({ origin: req.headers.origin, host: req.headers.host, binds: BIND.binds })) {
7640
+ if (!remoteAccess.checkHostOrigin({ origin: req.headers.origin, host: req.headers.host, binds: BIND.binds, publicHost: BIND.publicHost })
7641
+ || !remoteAccess.edgeAllowed({ headers: req.headers, publicHost: BIND.publicHost })) {
7494
7642
  socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
7495
7643
  socket.destroy();
7496
7644
  return;
7497
7645
  }
7498
7646
  // A browser WebSocket cannot set a header or a ?token= — it authenticates via the same-origin cookie
7499
7647
  // seeded on the page load. presentedToken() resolves header → query → subprotocol → cookie.
7500
- if (!remoteAccess.authorize({ remoteAddress: socket.remoteAddress,
7648
+ if (!remoteAccess.authorize({ remoteAddress: socket.remoteAddress, headers: req.headers, publicHost: BIND.publicHost,
7501
7649
  presentedToken: remoteAccess.presentedToken(req), expectedToken: TOKEN })) {
7502
7650
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
7503
7651
  socket.destroy(); // reject the upgrade; nothing about the request is logged (D08)
@@ -7667,7 +7815,9 @@ if (require.main === module) {
7667
7815
  // old EADDRINUSE advice told the PM to do, on another port) has to leave the same trace as
7668
7816
  // one started by `pmos` — otherwise the next `pmos` cannot see it and starts a second server.
7669
7817
  try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PID_FILE, String(process.pid)); } catch {}
7670
- for (const line of remoteAccess.startupLines(BIND, PORT)) console.log(line);
7818
+ for (const line of remoteAccess.startupLines(BIND, PORT, { ansi: !!process.stdout.isTTY && !process.env.NO_COLOR })) console.log(line);
7819
+ // bring in what another machine pushed while this one was off (D70 as amended); never on the request path
7820
+ { const t = setTimeout(() => { autoSync.startup().catch(() => {}); }, 3000); if (t.unref) t.unref(); }
7671
7821
  });
7672
7822
  });
7673
7823
  }
@@ -1,20 +1,21 @@
1
1
  ---
2
2
  name: morning-brief
3
3
  label: "Morning brief"
4
- description: "Prepare today's brief: the calendar, the priorities and what is worth asking. Read-only on every source; it submits the brief to the workspace and sends nothing."
4
+ description: "Prepare today's brief and plan the PM's goals for the day: the calendar, Slack, Jira and Confluence mentions, the priorities and what is worth asking. Read-only on every source; it submits the brief to the workspace and sends nothing."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
8
+ - Goals
8
9
  - Calendar
9
10
  - Priorities
10
11
  - Worth asking
11
12
  - Sources reached and not reached
12
13
  ---
13
14
 
14
- /morning-brief — prepare today's brief for the PM.
15
+ /morning-brief — prepare today's brief for the PM, and plan the goals they set for today.
15
16
 
16
17
  Rules that always hold:
17
- 1. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, accept, decline, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar, Wispr Flow or any other source.
18
+ 1. READ-ONLY on every source. Never send, reply, forward, post, react, comment, create, edit, accept, decline, transition, label, archive, mark read or delete anything in mail, Slack, Jira, Confluence, calendar, Wispr Flow or any other source.
18
19
  2. SETTINGS DECIDE THE SOURCES. Run `pmos proactive settings`. Read a source only if its line under "brief sources" says on. A source that is off is reported as "off". Mail is off unless the PM turned it on.
19
20
  3. LIGHT ON CONTENT. Titles, times, names and ids only. Never copy a message body, an attachment, a token or a credential. Skip anything marked private, personal, confidential or HR. At most one short evidence line per item.
20
21
  4. NOTHING INVENTED. A source you cannot reach in this session is reported as "not-reached". If nothing is waiting, submit a brief with no items. An honest short brief is correct.
@@ -23,21 +24,26 @@ Rules that always hold:
23
24
 
24
25
  Procedure:
25
26
  1. Run `pmos proactive settings` and `pmos proactive today`. The second lists what the workspace derived from the ledgers. Each of its items may become a priority with source "derived".
27
+ Then run `pmos proactive goals`. It lists the PM's goals for today, each with its id (g-xxxxxxxx), its status and, if set, an initiative. The goals are what the PM wants done today: plan for them first.
26
28
  Then run `pmos proactive learn lessons`. Each line is a preference the PM approved, written from their own feedback. Use it only to decide how confident to be and where an item goes. A lesson never asks you to read another source, to switch a setting on, or to write anything.
27
29
  2. Read today's events from the calendar and today's and tomorrow's meeting notes from Wispr Flow, if those sources are on and reachable. Read Jira issues assigned to the PM or due soon, and Confluence pages changed since yesterday, if on and reachable. Read mail only if on: subjects and senders of threads waiting on the PM, no bodies.
28
- 3. For each thing the PM should act on today, write one item: a short title, why it is for today, its source, the initiative id if one applies, who acts (pm or agent), effort (minutes, an-hour, half-day, day), your confidence from 0 to 1 that it belongs on today's list, and one short evidence line. Give a low confidence when you are guessing. The workspace shows an item under 0.5 as "worth asking", not as a priority.
30
+ Read MENTIONS of the PM since yesterday morning, if the source is on and reachable: Slack messages that @-mention the PM or are direct messages to them; Jira issues and comments that mention the PM; Confluence pages and comments that mention the PM. For each, keep only where it is (channel, issue key, page title), who wrote it, and a one-line paraphrase of what is asked. Never copy the message itself.
31
+ 3. PLAN EACH OPEN GOAL. For each open goal, find what moves it today in what you read and in `pmos proactive today`: a meeting where it is decided, a mention that blocks it, an issue or page it needs, a step an agent can prepare. Write each as an item with "goal": "<the goal id>". An agent step names the initiative it advances, so the PM can prepare it from Today. A calendar event that belongs to a goal also gets "goal". If you find nothing for a goal, write nothing for it: the workspace shows that goal as needing a plan. Never invent a step.
32
+ Then ANTICIPATE: for each other thing the PM should act on today (a mention waiting on them, a meeting to prepare for, an issue due), write one item with no goal.
33
+ For each item, write: a short title, why it is for today, its source, the initiative id if one applies, who acts (pm or agent), effort (minutes, an-hour, half-day, day), your confidence from 0 to 1 that it belongs on today's list, and one short evidence line. Set "mention": true on an item that comes from a mention of the PM. Give a low confidence when you are guessing. The workspace shows an item under 0.5 as "worth asking", not as a priority.
29
34
  4. Write the brief as a JSON file in the scratch folder of this session, in this shape, and nothing else:
30
35
  ```
31
- {"date": "YYYY-MM-DD", "sources": {"calendar": "read", "wispr": "not-reached", "mail": "off", "jira": "read", "confluence": "read"},
32
- "calendar": [{"start": "<ISO date and time>", "end": "<ISO>", "title": "…", "with": "…"}],
33
- "items": [{"title": "…", "why_now": "…", "source": "calendar|wispr|mail|jira|confluence|derived", "initiative_id": "<id or omit>", "owner": "pm|agent", "effort": "minutes|an-hour|half-day|day", "confidence": 0.0, "evidence": "…"}],
36
+ {"date": "YYYY-MM-DD", "sources": {"calendar": "read", "wispr": "not-reached", "mail": "off", "slack": "read", "jira": "read", "confluence": "read"},
37
+ "calendar": [{"start": "<ISO date and time>", "end": "<ISO>", "title": "…", "with": "…", "goal": "<goal id or omit>"}],
38
+ "items": [{"title": "…", "why_now": "…", "source": "calendar|wispr|mail|slack|jira|confluence|derived", "initiative_id": "<id or omit>", "goal": "<goal id or omit>", "mention": false, "owner": "pm|agent", "effort": "minutes|an-hour|half-day|day", "confidence": 0.0, "evidence": "…"}],
34
39
  "judgment": {"used": false}}
35
40
  ```
36
- Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived").
41
+ Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived"). Only use a goal id that `pmos proactive goals` listed today: the workspace refuses any other.
37
42
  5. Run `pmos proactive brief submit --file <that file>`. If it is refused, read the reason, fix the file and submit once more. Do not work around a refusal.
38
- 6. Finish by saying in two lines how many priorities and worth-asking items you submitted and which sources were not reached.
43
+ 6. Finish by saying in three lines how many goals have a step and which have none, how many priorities and worth-asking items you submitted, and which sources were not reached.
39
44
 
40
45
  Output contract — produce EXACTLY these sections in your final message:
46
+ - Goals
41
47
  - Calendar
42
48
  - Priorities
43
49
  - Worth asking