create-pmos 0.71.0 → 0.72.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.
@@ -0,0 +1,242 @@
1
+ /* pm/dest-mywork.js — the My work destination: every Jira ticket and WBS item assigned to the PM, ranked, briefed to an
2
+ * agent, approved in Agenda (initiative my-work, PRD R3 to R5, R7, R10).
3
+ *
4
+ * A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
5
+ * It keeps ONE persistent root per host and rebuilds the body only when its own data or a choice changed.
6
+ *
7
+ * What it shows:
8
+ * - the tabs To brief (the default) · Agents · Ready · All, each with its count, and Read again;
9
+ * - the list, in WSJF order: the tier, the title, a meta line (Jira or WBS, the key, who asked, the due date) and a
10
+ * state icon;
11
+ * - the chosen item: What the agent found (the ask, the thread, the open questions), the instruction box prefilled
12
+ * with the agent's suggestion, Run agent and I'll handle it; Back to brief when the item is the PM's or an agent's;
13
+ * Open Agenda when its draft waits there; Details with each part of the score.
14
+ * Every press is a page press to POST /api/my-work/act or /api/my-work/refresh. Approve, revise and retry are Agenda's
15
+ * (R7): there is no approve button here. In fixture mode it reads nothing.
16
+ */
17
+
18
+ import { el } from './dom.js';
19
+ import { button, row, field, toast, empty, tabs, badge, ensureComponentsCss } from './primitives.js';
20
+ import { icon, ensureSprite } from './icons.js';
21
+ import { pmPost } from './http.js';
22
+ import { wsOf } from './data.js';
23
+ import { registerCommand, openDestination } from './registry.js';
24
+ import { myWork, loadMyWork, onMyWork } from './my-work-data.js';
25
+
26
+ export const COPY = Object.freeze({
27
+ title: 'My work',
28
+ fixture: 'My work shows the work assigned to you in a real session.',
29
+ loading: 'Reading…',
30
+ readError: 'My work could not be read.',
31
+ readAgain: 'Read again',
32
+ readStarted: 'The read started in a session. My work updates when it submits.',
33
+ readAt: (t) => (t ? 'Jira read ' + t : 'Jira not read yet. Press Read again.'),
34
+ tabs: { brief: 'To brief', agents: 'Agents', ready: 'Ready', all: 'All' },
35
+ none: { brief: 'Nothing waits for a brief.', agents: 'No agent is working on your items.', ready: 'No draft waits for you.', all: 'Nothing is assigned to you.' },
36
+ pick: 'Choose an item to see what the agent found.',
37
+ found: 'What the agent found',
38
+ ask: 'Ask', thread: 'Thread', open: 'Still open',
39
+ noCard: 'No context card yet. Read again to have an agent read this ticket.',
40
+ decide: 'This looks like a decision only you can make. An agent can still prepare it.',
41
+ instruction: 'Instruction for the agent',
42
+ placeholder: 'Tell the agent what to do. It drafts, and sends nothing until you approve it in Agenda.',
43
+ run: 'Run agent',
44
+ handle: "I'll handle it",
45
+ back: 'Back to brief',
46
+ agenda: 'Open Agenda',
47
+ source: 'Open the source',
48
+ details: 'Details', hide: 'Hide details',
49
+ score: (w) => `${w.tier} · score ${w.score} = (value ${w.value} + urgency ${w.urgency} + unblocks ${w.unblocks}) / size ${w.size}`,
50
+ yourInstruction: 'Your instruction',
51
+ started: 'The agent is on it. Its draft will wait for you in Agenda.',
52
+ taken: 'It is yours.',
53
+ reopened: 'Back to brief.',
54
+ state: {
55
+ brief: 'Needs your brief', yours: 'You handle it', ready: 'Draft ready in Agenda', done: 'Done',
56
+ agent: { waiting: 'Agent queued', working: 'Agent working', sending: 'Agent sending', failed: 'Agent stopped' },
57
+ },
58
+ due: (d) => 'due ' + d,
59
+ from: (f) => 'from ' + f,
60
+ });
61
+
62
+ registerCommand({ id: 'my-work-open', label: 'My work', hint: 'Every Jira ticket and WBS item assigned to you, ranked', cli: 'pmos my-work list', run: () => openDestination('mywork') });
63
+
64
+ const TAB_STATES = { brief: ['brief'], agents: ['agent'], ready: ['ready'], all: ['brief', 'yours', 'agent', 'ready'] };
65
+ const STATE_ICON = { brief: 'circle-alert', yours: 'flag', ready: 'check', done: 'square-check-big' };
66
+ const AGENT_ICON = { waiting: 'clock', working: 'bot', sending: 'bot', failed: 'circle-alert' };
67
+
68
+ const stateWord = (x) => (x.state === 'agent' ? COPY.state.agent[x.sub] || COPY.state.agent.waiting : COPY.state[x.state]);
69
+ const stateIcon = (x) => {
70
+ const node = icon(x.state === 'agent' ? AGENT_ICON[x.sub] || 'clock' : STATE_ICON[x.state] || 'circle-alert');
71
+ return el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': stateWord(x), title: stateWord(x), 'data-my-work-state': x.state }, node);
72
+ };
73
+
74
+ /* ── mount ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
75
+
76
+ const insts = new WeakMap();
77
+
78
+ export function mountDestination(host, ctx) {
79
+ let inst = insts.get(host);
80
+ if (!inst) {
81
+ inst = { host, root: el('div', { class: 'rp-page rp-mywork', 'data-my-work': '' }), body: el('div', { class: 'rp-mywork-body' }),
82
+ head: null, tab: 'brief', open: null, drafts: new Map(), details: false, busy: false, last: '', ws: null, off: null };
83
+ insts.set(host, inst);
84
+ const held = inst;
85
+ inst.off = onMyWork(() => draw(held));
86
+ held.body.addEventListener('focusout', () => setTimeout(() => { if (!held.last) draw(held); }, 0));
87
+ }
88
+ ensureComponentsCss();
89
+ if (!inst.root.isConnected || inst.root.parentNode !== host) host.replaceChildren(inst.root);
90
+ if (ctx && ctx.FIXTURE) { inst.root.replaceChildren(empty({ text: COPY.fixture })); return; }
91
+ const repo = (ctx && ctx.tab && ctx.tab.repo) || (ctx && ctx.state && ctx.state.product) || null;
92
+ inst.ws = repo ? wsOf(repo) : null;
93
+ ensureSprite();
94
+ loadMyWork();
95
+ draw(inst);
96
+ }
97
+
98
+ export function disposeDestination(host) {
99
+ const inst = insts.get(host);
100
+ if (!inst) return;
101
+ if (inst.off) inst.off();
102
+ inst.root.remove();
103
+ insts.delete(host);
104
+ }
105
+
106
+ /* ── presses ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
107
+
108
+ async function act(inst, body, done) {
109
+ if (inst.busy) return null;
110
+ inst.busy = true; inst.last = ''; draw(inst);
111
+ const r = await pmPost('/api/my-work/act', { ...body, ws: inst.ws });
112
+ inst.busy = false;
113
+ if (!r || !r.ok) toast({ text: (r && r.error) || 'Nothing was saved.', tone: 'error' });
114
+ else if (done) toast({ text: done, timeout: 4000 });
115
+ await loadMyWork(true);
116
+ return r && r.ok ? r : null;
117
+ }
118
+
119
+ async function readAgain(inst) {
120
+ const r = await pmPost('/api/my-work/refresh', { ws: inst.ws });
121
+ toast(r && r.ok ? { text: COPY.readStarted, timeout: 5000 } : { text: (r && r.error) || 'The read did not start.', tone: 'error' });
122
+ }
123
+
124
+ /* ── the list ────────────────────────────────────────────────────────────────────────────────────────────────────── */
125
+
126
+ function itemRow(inst, x) {
127
+ const open = inst.open === x.id;
128
+ const meta = [x.source === 'jira' ? 'Jira ' + x.key : 'WBS ' + (x.repo ? x.repo + ' · ' : '') + x.key];
129
+ if (x.from) meta.push(COPY.from(x.from));
130
+ if (x.due) meta.push(COPY.due(x.due));
131
+ return el('div', { class: 'rp-mywork-row' + (open ? ' rp-mywork-row--open' : ''), role: 'listitem', 'data-my-work-item': x.id },
132
+ row({
133
+ title: x.title,
134
+ meta,
135
+ lead: [badge({ text: x.wsjf.tier, tone: x.wsjf.tier === 'P1' ? 'warning' : 'outline', attrs: { 'data-my-work-tier': x.wsjf.tier, title: COPY.score(x.wsjf) } })],
136
+ trail: [x.kind === 'decide' && x.state === 'brief' ? el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': COPY.decide, title: COPY.decide }, icon('diamond')) : null, stateIcon(x)].filter(Boolean),
137
+ onOpen: () => { inst.open = x.id; inst.details = false; inst.last = ''; draw(inst); },
138
+ openLabel: `${x.title}, ${x.wsjf.tier}, ${stateWord(x)}`,
139
+ openAttrs: { 'aria-current': open ? 'true' : null, 'data-my-work-open': x.id },
140
+ }));
141
+ }
142
+
143
+ function listPane(inst, d) {
144
+ const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
145
+ const counts = { brief: d.counts.brief, agents: d.counts.agents, ready: d.counts.ready, all: d.counts.all };
146
+ const controls = el('div', { class: 'rp-mywork-views' },
147
+ tabs({ variant: 'line', value: inst.tab, ariaLabel: 'Show', attrs: { 'data-my-work-tabs': '' },
148
+ items: Object.keys(COPY.tabs).map((id) => ({ id, label: COPY.tabs[id], count: counts[id] })),
149
+ onChange: (v) => { inst.tab = v; inst.open = null; inst.last = ''; draw(inst); } }));
150
+ const rows = shown.length
151
+ ? el('div', { role: 'list', 'data-my-work-list': inst.tab }, shown.map((x) => itemRow(inst, x)))
152
+ : el('p', { class: 'rp-section__empty' }, COPY.none[inst.tab]);
153
+ return el('div', { class: 'rp-mywork-list' }, controls, rows);
154
+ }
155
+
156
+ /* ── the item ────────────────────────────────────────────────────────────────────────────────────────────────────── */
157
+
158
+ function section(title, kids, attrs) {
159
+ return el('section', { class: 'rp-mywork-sec', ...(attrs || {}) }, el('h4', { class: 'rp-mywork-sec__title' }, title), ...kids);
160
+ }
161
+
162
+ function foundSec(x) {
163
+ const kids = [];
164
+ if (!x.ask && !x.thread && !(x.open || []).length) kids.push(el('p', { class: 'rp-mywork-hint' }, x.source === 'jira' ? COPY.noCard : x.status || ''));
165
+ if (x.ask) kids.push(el('p', { class: 'rp-mywork-text', 'data-my-work-ask': '' }, el('strong', null, COPY.ask + ': '), x.ask));
166
+ if (x.thread) kids.push(el('p', { class: 'rp-mywork-text' }, el('strong', null, COPY.thread + ': '), x.thread));
167
+ if ((x.open || []).length) kids.push(el('p', { class: 'rp-mywork-sub' }, COPY.open), el('ul', { class: 'rp-mywork-open' }, x.open.map((o) => el('li', null, o))));
168
+ if (x.kind === 'decide') kids.push(el('p', { class: 'rp-mywork-hint', 'data-my-work-decide': '' }, COPY.decide));
169
+ return section(COPY.found, kids, { 'data-my-work-sec': 'found' });
170
+ }
171
+
172
+ function briefSec(inst, x) {
173
+ const f = field({ label: COPY.instruction, kind: 'textarea', hideLabel: true, placeholder: COPY.placeholder, value: inst.drafts.has(x.id) ? inst.drafts.get(x.id) : (x.suggest || ''), attrs: { 'data-my-work-field': 'instruction' } });
174
+ f.control.addEventListener('input', () => inst.drafts.set(x.id, f.control.value));
175
+ const run = button({ label: COPY.run, variant: 'primary', size: 'sm', icon: 'bot', disabled: inst.busy, attrs: { 'data-my-work-act': 'brief' },
176
+ onClick: async () => {
177
+ const instruction = f.control.value.trim();
178
+ const r = await act(inst, { action: 'brief', item: x.id, instruction }, COPY.started);
179
+ if (r) inst.drafts.delete(x.id);
180
+ } });
181
+ const mine = button({ label: COPY.handle, variant: 'outline', size: 'sm', disabled: inst.busy, attrs: { 'data-my-work-act': 'handle' },
182
+ onClick: () => act(inst, { action: 'handle', item: x.id }, COPY.taken) });
183
+ return el('div', { class: 'rp-mywork-brief' }, f, el('div', { class: 'rp-mywork-acts' }, run, mine));
184
+ }
185
+
186
+ function statusSec(inst, x) {
187
+ const kids = [el('p', { class: 'rp-mywork-text', 'data-my-work-status': x.state + (x.sub ? ':' + x.sub : '') }, stateIcon(x), ' ', stateWord(x))];
188
+ if (x.task && x.task.error) kids.push(el('p', { class: 'rp-mywork-hint' }, x.task.error));
189
+ if (x.task && x.task.draft) kids.push(el('p', { class: 'rp-mywork-hint' }, x.task.draft));
190
+ if (x.instruction) kids.push(el('p', { class: 'rp-mywork-sub' }, COPY.yourInstruction), el('p', { class: 'rp-mywork-text rp-mywork-quote' }, x.instruction));
191
+ const acts = [];
192
+ if (x.state === 'ready' || (x.state === 'agent' && x.sub === 'failed')) acts.push(button({ label: COPY.agenda, variant: 'primary', size: 'sm', attrs: { 'data-my-work-act': 'agenda' }, onClick: () => openDestination('today') }));
193
+ const busyAgent = x.state === 'agent' && (x.sub === 'working' || x.sub === 'sending');
194
+ if (x.state !== 'done' && !busyAgent) acts.push(button({ label: COPY.back, variant: 'ghost', size: 'sm', icon: 'undo-2', disabled: inst.busy, attrs: { 'data-my-work-act': 'reopen' }, onClick: () => act(inst, { action: 'reopen', item: x.id }, COPY.reopened) }));
195
+ if (acts.length) kids.push(el('div', { class: 'rp-mywork-acts' }, acts));
196
+ return el('div', { class: 'rp-mywork-status' }, kids);
197
+ }
198
+
199
+ function itemPane(inst, d) {
200
+ const x = d.items.find((y) => y.id === inst.open);
201
+ if (!x) return el('div', { class: 'rp-mywork-page' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
202
+ const head = el('header', { class: 'rp-mywork-who' },
203
+ el('h3', { class: 'rp-mywork-who__title' }, x.title),
204
+ el('p', { class: 'rp-mywork-hint' }, [x.source === 'jira' ? 'Jira' : 'WBS', x.key, x.repo, x.status, x.due ? COPY.due(x.due) : null, x.from ? COPY.from(x.from) : null].filter(Boolean).join(' · ')),
205
+ x.url ? el('a', { class: 'rp-mywork-link', href: x.url, target: '_blank', rel: 'noopener noreferrer' }, icon('link'), ' ', COPY.source) : null);
206
+ const details = button({ label: inst.details ? COPY.hide : COPY.details, variant: 'ghost', size: 'sm', iconEnd: inst.details ? 'chevron-down' : 'chevron-right',
207
+ attrs: { 'aria-expanded': inst.details ? 'true' : 'false', 'data-my-work-details': '' }, onClick: () => { inst.details = !inst.details; inst.last = ''; draw(inst); } });
208
+ return el('article', { class: 'rp-mywork-page', 'data-my-work-page': x.id },
209
+ head, foundSec(x),
210
+ x.state === 'brief' ? section(COPY.instruction, [briefSec(inst, x)], { 'data-my-work-sec': 'brief' }) : statusSec(inst, x),
211
+ el('div', { class: 'rp-mywork-acts' }, details), inst.details ? el('p', { class: 'rp-mywork-hint', 'data-my-work-score': '' }, COPY.score(x.wsjf)) : null);
212
+ }
213
+
214
+ /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
215
+
216
+ function header(inst) {
217
+ if (inst.head) return inst.head;
218
+ inst.readLine = el('p', { class: 'rp-mywork-hint', 'data-my-work-read': '' });
219
+ inst.head = el('div', { class: 'rp-mywork-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.readLine,
220
+ button({ label: COPY.readAgain, variant: 'outline', size: 'sm', icon: 'refresh-cw', attrs: { 'data-my-work-act': 'refresh' }, onClick: () => readAgain(inst) }));
221
+ return inst.head;
222
+ }
223
+
224
+ function draw(inst) {
225
+ const root = inst.root;
226
+ const d = myWork();
227
+ const head = header(inst);
228
+ if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body);
229
+ const fp = JSON.stringify([d, inst.tab, inst.open, inst.details, inst.busy]);
230
+ if (fp === inst.last) return;
231
+ // Focus stays in an instruction being typed: the redraw waits until the field is left.
232
+ const typing = document.activeElement && inst.body.contains(document.activeElement) && document.activeElement.matches('textarea');
233
+ if (typing) { inst.last = ''; return; }
234
+ inst.last = fp;
235
+ if (!d) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
236
+ if (!d.ok) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, d.error || COPY.readError)); return; }
237
+ inst.readLine.textContent = COPY.readAt(d.read_at ? new Date(d.read_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : null);
238
+ const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
239
+ if (!shown.some((x) => x.id === inst.open)) inst.open = shown.length ? shown[0].id : null;
240
+ const banner = (d.banner || []).length ? el('p', { class: 'rp-mywork-hint', role: 'status', 'data-my-work-banner': '' }, d.banner.map((b) => b.source + ': ' + b.error).join(' · ')) : null;
241
+ inst.body.replaceChildren(...[banner, el('div', { class: 'rp-mywork-grid' }, listPane(inst, d), itemPane(inst, d))].filter(Boolean));
242
+ }
@@ -23,6 +23,7 @@ export const COPY = {
23
23
  nav: {
24
24
  label: 'Destinations',
25
25
  today: 'Agenda',
26
+ mywork: 'My work',
26
27
  initiatives: 'Initiatives',
27
28
  board: 'Board',
28
29
  reports: 'Reports',
@@ -33,6 +34,8 @@ export const COPY = {
33
34
  },
34
35
  /** The Today count on the top bar and on the phone tab, for a screen reader. */
35
36
  needs: (n) => (n === 1 ? '1 needs you' : n + ' need you'),
37
+ /** My work's count on the top bar, for a screen reader (initiative my-work). */
38
+ toBrief: (n) => (n === 1 ? '1 item to brief' : n + ' items to brief'),
36
39
  search: 'Search or capture',
37
40
  searchKey: '⌘K',
38
41
  sync: {
@@ -41,6 +44,15 @@ export const COPY = {
41
44
  title: 'Sync this repository: fetch, bring in the work of the other machine, record your changes, push.',
42
45
  done: 'The repository is in sync.',
43
46
  stopped: (step, why) => 'Sync stopped at ' + step + ': ' + why,
47
+ /** Sync stopped because both machines changed the same lines. The PM chooses which version to keep. */
48
+ conflict: {
49
+ title: 'Both machines changed the same files',
50
+ sub: (files) => (files.length ? files.join(', ') : 'Git did not name the files.'),
51
+ body: 'Nothing changed. Choose which version to keep in these files. All other changes from both machines are kept.',
52
+ push: 'Push this machine’s version',
53
+ pull: 'Pull the other machine’s version',
54
+ cancel: 'Not now',
55
+ },
44
56
  },
45
57
  settings: 'Settings',
46
58
  dock: {
@@ -0,0 +1,51 @@
1
+ /* pm/my-work-card.js — Agenda's one card for the items that wait for a brief (initiative my-work, PRD R8).
2
+ *
3
+ * One record, two lenses (the PM's choice, 2026-10-07): My work owns the assigned backlog and the brief, and Agenda
4
+ * shows ONE card, "N items need your brief", when N is 1 or more. Its button opens My work. The card counts once in
5
+ * Agenda's count, whichever repositories Agenda shows: the list is the PM's, not a repository's.
6
+ *
7
+ * Loaded at boot by registry.js loadExtensions. It reads the list once, then again every 30 seconds and whenever the
8
+ * window gets the focus back, so the bar's count and this card stay current.
9
+ */
10
+
11
+ import { registerTodaySource, openDestination } from './registry.js';
12
+ import { setState } from './state.js';
13
+ import { todayCard } from './today-cards.js';
14
+ import { myWork, loadMyWork, onMyWork } from './my-work-data.js';
15
+
16
+ export const COPY = Object.freeze({
17
+ cause: (n) => (n === 1 ? '1 item needs your brief' : `${n} items need your brief`),
18
+ evidence: (titles) => 'Top: ' + titles.join(' · '),
19
+ act: 'Open My work',
20
+ cli: 'pmos my-work list',
21
+ });
22
+
23
+ const brief = () => { const d = myWork(); return d && d.ok ? d.counts.brief : 0; };
24
+
25
+ registerTodaySource({
26
+ id: 'my-work-brief',
27
+ kind: 6, // capture: the agent read new assigned work and the PM triages it. It ranks after the gates, sessions and Keep cards
28
+ rank: 40,
29
+ count: () => (brief() > 0 ? 1 : 0),
30
+ cards: () => {
31
+ const n = brief();
32
+ if (!n) return [];
33
+ const top = myWork().items.filter((x) => x.state === 'brief').slice(0, 2).map((x) => x.key + ' ' + x.title);
34
+ return [todayCard({
35
+ kind: 6, id: 'my-work|brief', cause: COPY.cause(n), evidence: COPY.evidence(top), act: { label: COPY.act, cli: COPY.cli },
36
+ onAct: () => openDestination('mywork'), attrs: { 'data-my-work-card': String(n) },
37
+ })];
38
+ },
39
+ });
40
+
41
+ if (typeof window !== 'undefined') {
42
+ // re-render Agenda only when the card would change: a re-render on every read would take the focus off a card the PM is on
43
+ let shown = '';
44
+ onMyWork((d) => {
45
+ const sig = d && d.ok ? d.counts.brief + '|' + d.items.filter((x) => x.state === 'brief').slice(0, 2).map((x) => x.id).join(',') : '0';
46
+ if (sig !== shown) { shown = sig; setState({}); }
47
+ });
48
+ loadMyWork(true);
49
+ setInterval(() => loadMyWork(), 30000);
50
+ window.addEventListener('focus', () => loadMyWork());
51
+ }
@@ -0,0 +1,36 @@
1
+ /* pm/my-work-data.js — the one read of GET /api/my-work that the page shares (initiative my-work, PRD R8, R10).
2
+ *
3
+ * My work, the Agenda card and the top bar's count all read the same list, once: a second reader inside READ_MS gets
4
+ * the answer already held. Every reader is told when a new answer arrives (onMyWork), and the top bar hears it as the
5
+ * window event `pmos:mywork` with { brief } (the count of items to brief), so the bar needs no import of this module.
6
+ * In fixture mode nothing loads it.
7
+ */
8
+
9
+ import { pmGet } from './http.js';
10
+
11
+ const READ_MS = 30000;
12
+ let data = null;
13
+ let at = 0;
14
+ let loading = null;
15
+ const subs = new Set();
16
+
17
+ /** The last answer, or null before the first one. */
18
+ export const myWork = () => data;
19
+
20
+ /** Read the list again when the last answer is older than READ_MS, or always with `force`. Resolves to the answer. */
21
+ export function loadMyWork(force) {
22
+ if (loading) return loading;
23
+ if (!force && data && Date.now() - at < READ_MS) return Promise.resolve(data);
24
+ loading = pmGet('/api/my-work').then((j) => {
25
+ loading = null;
26
+ at = Date.now();
27
+ data = j && j.ok ? j : { ok: false, error: (j && j.error) || 'My work could not be read.', items: [], counts: { brief: 0, agents: 0, ready: 0, yours: 0, done: 0, all: 0 }, banner: [] };
28
+ for (const fn of subs) { try { fn(data); } catch (e) { console.error('[pm] my-work reader failed:', e); } }
29
+ if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('pmos:mywork', { detail: { brief: data.ok ? data.counts.brief : 0 } }));
30
+ return data;
31
+ });
32
+ return loading;
33
+ }
34
+
35
+ /** Call `fn(answer)` on each new answer. Returns the function that stops it. */
36
+ export function onMyWork(fn) { subs.add(fn); return () => subs.delete(fn); }
@@ -45,6 +45,8 @@ import { empty, ensureComponentsCss } from './primitives.js';
45
45
 
46
46
  export const DESTINATIONS = Object.freeze([
47
47
  Object.freeze({ id: 'today', label: 'Agenda', key: 'g t', module: 'dest-today.js', run: 3 }),
48
+ // Not a pm-os-absorb run (run: null): initiative my-work added it — the work assigned to the PM, ranked and briefed.
49
+ Object.freeze({ id: 'mywork', label: 'My work', key: 'g w', module: 'dest-mywork.js', run: null }),
48
50
  Object.freeze({ id: 'initiatives', label: 'Initiatives', key: 'g i', module: 'dest-initiatives.js', run: 2 }),
49
51
  Object.freeze({ id: 'board', label: 'Board', key: 'g b', module: 'dest-board.js', run: 4 }),
50
52
  Object.freeze({ id: 'reports', label: 'Reports', key: 'g r', module: 'dest-reports.js', run: 5 }),
@@ -58,6 +60,7 @@ export const EXTENSIONS = Object.freeze([
58
60
  'provider-cards.js', // the base: every needs provider's rows as Today cards (Run 3, 3B, owns it after)
59
61
  'bd-timeline.js', // Run 4 (4B): Breakdown › Timeline
60
62
  'initiatives-timeline.js', // Run 4 (4C): Initiatives › Timeline
63
+ 'my-work-card.js', // initiative my-work: Agenda's one card for the items that need a brief
61
64
  ]);
62
65
 
63
66
  const loaded = {}; // module file name -> the module
@@ -24,8 +24,9 @@ import { COPY } from './frame-copy.js';
24
24
 
25
25
  /** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
26
26
  * PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"). People joined after Docs when the
27
- * PM approved the People mockup and said build it (2026-10-06, people-directory), so the bar holds 9 controls. */
28
- export const BAR_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports', 'docs', 'people']);
27
+ * PM approved the People mockup and said build it (2026-10-06, people-directory). My work joined beside Agenda when the PM
28
+ * approved the mockup that puts it there and said build it (2026-10-07, my-work), so the bar holds 10 controls. */
29
+ export const BAR_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'people']);
29
30
 
30
31
  /** Is this sync reading "not in sync"? Behind, ahead, records not yet recorded, or `remote_stale`: no fetch in the last
31
32
  * few minutes, so another machine's push would be invisible here and "in sync" cannot be claimed. An unreadable status
@@ -54,6 +55,19 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
54
55
  }
55
56
  const todayMark = el('span', { class: 'fr-mark' }, dot, count);
56
57
  links.today.appendChild(todayMark);
58
+ // My work's count of items to brief (initiative my-work, R10): my-work-data.js tells it with the `pmos:mywork` event
59
+ const workCount = needsCount('0', { 'data-my-work-badge': '' });
60
+ const workMark = el('span', { class: 'fr-mark' }, workCount);
61
+ workMark.hidden = true;
62
+ links.mywork.appendChild(workMark);
63
+ if (typeof window !== 'undefined') {
64
+ window.addEventListener('pmos:mywork', (e) => {
65
+ const n = Math.max(0, Number(e && e.detail && e.detail.brief) || 0);
66
+ workCount.textContent = String(n);
67
+ workCount.setAttribute('aria-label', COPY.toBrief(n));
68
+ workMark.hidden = n === 0;
69
+ });
70
+ }
57
71
  const nav = el('nav', { class: 'fr-nav', 'aria-label': COPY.nav.label }, BAR_DESTS.map((id) => links[id]));
58
72
 
59
73
  const search = button({
@@ -3802,6 +3802,11 @@ function jobSessionName(title) {
3802
3802
  /** The repository a job belongs to (its item's tag: its goal's, its initiative's or its place's repository), when that
3803
3803
  * repository is registered and has the day-task skill; else null and the job runs where Prepare my day was pressed. */
3804
3804
  function taskWsId(run, t) {
3805
+ // a task briefed in My work names its repository on its context card (a WBS item's own repository)
3806
+ if (t.context && typeof t.context.ws === 'string') {
3807
+ const w = getWs(t.context.ws);
3808
+ return w && w.id === t.context.ws && w.id !== run.ws && !daySkillPrompt(run, 'day-task', w.id).error ? w.id : null;
3809
+ }
3805
3810
  try {
3806
3811
  const brief = briefOfDay(run.date);
3807
3812
  if (!brief || !t.item_id) return null;
@@ -3835,6 +3840,22 @@ function startDayTask(run, t, now) {
3835
3840
  const f = dayRunLib.fail(st.run, t.id, String(sk.error || (s && s.note) || 'the session could not start').slice(0, 240), now);
3836
3841
  return f.ok ? f.run : st.run;
3837
3842
  }
3843
+ /** My work's Run agent (initiative my-work, PRD R5): add the briefed task to today's run and start what may start. A day
3844
+ * with no run gets one, marked opened by My work, in the repository the PM pressed from. Returns { ok, run } or why not. */
3845
+ function myWorkBrief(spec, wsId) {
3846
+ const now = Date.now(); const date = briefLib.localDate(now);
3847
+ let run = readDayRun(date);
3848
+ if (!run) {
3849
+ const w = getWs(wsId);
3850
+ if (!w) return { ok: false, status: 400, error: 'no workspace to run the agent in' };
3851
+ run = { ...dayRunLib.newRun(date, now), ws: w.id, opened_by: 'my-work' };
3852
+ }
3853
+ const a = dayRunLib.addBriefed(run, spec);
3854
+ if (!a.ok) return { ok: false, status: 409, error: a.error };
3855
+ const next = startDayTasks(a.run, now);
3856
+ if (!writeDayRun(next)) return { ok: false, status: 500, error: 'could not save — nothing changed' };
3857
+ return { ok: true, run: next, task: dayRunLib.findTask(next, spec.id) };
3858
+ }
3838
3859
  /** Start what lib/day-run.js says may start now (at most 5 sessions at once). */
3839
3860
  function startDayTasks(run, now) {
3840
3861
  for (const t of dayRunLib.toStart(run)) run = startDayTask(run, t, now);
@@ -3887,6 +3908,19 @@ function dayTick() {
3887
3908
  if (JSON.stringify(run) !== before) writeDayRun(run);
3888
3909
  }
3889
3910
  // ── end of the today day run helpers ─────────────────────────────────────────────────────────────────────────────
3911
+ // My work (initiative my-work, PRD R1) is not part of the day run: its read session is started here, outside it.
3912
+ /** My work's Read again: start the my-work-read skill in a session of repository `wsId`. */
3913
+ function myWorkRead(wsId) {
3914
+ const w = getWs(wsId);
3915
+ if (!w) return { ok: false, status: 400, error: 'no workspace' };
3916
+ const task = taskById('my-work-read', w.root);
3917
+ if (!task || task.kind !== 'agent' || !task.prompt) return { ok: false, status: 409, error: 'the my-work-read skill is not installed for this repository' };
3918
+ const prompt = skillStore.retargetFiling(task.prompt, filingContract(w));
3919
+ const s = spawnSession({ name: 'My work · read', agent: task.agent || 'claude', ws: w.id, origin: 'my-work-read', initialInput: prompt + '\r',
3920
+ modelAsk: { scheduleTier: null, skillTier: task.tier || null, skill: task.skill } });
3921
+ if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the read session could not start' };
3922
+ return { ok: true, session: s.id };
3923
+ }
3890
3924
  /* The calendar the Agenda remembers (G23): one file beside the briefs, written only by the brief route. */
3891
3925
  const CAL_MEMORY = () => path.join(STATE, 'calendar-memory.json');
3892
3926
  // the PM's key-meeting marks (lib/calendar-memory.js setMark): a small file beside the calendar memory, written only by
@@ -7095,7 +7129,9 @@ async function handleRequest(req, res) {
7095
7129
  if (syncing.has(w.root)) return json(res, { ok: false, error: 'a sync is already running for this repository' }, 409);
7096
7130
  syncing.add(w.root);
7097
7131
  try {
7098
- const out = await syncLib.sync(w.root, { label: os.hostname() });
7132
+ // After a stop on a conflict the PM may answer which machine's version to keep; only this press passes it.
7133
+ const prefer = body.prefer === 'mine' || body.prefer === 'theirs' ? body.prefer : undefined;
7134
+ const out = await syncLib.sync(w.root, { label: os.hostname(), prefer });
7099
7135
  boardCache.invalidate(w.root);
7100
7136
  return json(res, { ws: w.id, name: w.name, ...out });
7101
7137
  } finally { syncing.delete(w.root); }
@@ -7733,7 +7769,8 @@ async function handleRequest(req, res) {
7733
7769
  // The brief session ended and no brief arrived: say so, with the session's own reason, so the page does not wait on a
7734
7770
  // read that will never finish (it reads as "stuck on Read" otherwise).
7735
7771
  const bs = run && run.brief_session; const bsess = bs ? sessions.get(bs.id) : null;
7736
- const readFailed = !run || brief ? null
7772
+ // A run My work opened (initiative my-work, R5) has no morning read until Prepare my day is pressed: nothing failed.
7773
+ const readFailed = !run || brief || (run.opened_by === 'my-work' && !bs) ? null
7737
7774
  : !bs ? { session: null, why: 'the morning-brief session did not start.' }
7738
7775
  : (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' }
7739
7776
  : briefSessionDone(run, date, now) === 'idle' ? { session: bs.id, idle: true, why: 'the session has printed nothing for two minutes and has not submitted a brief.' } : null;
@@ -8315,7 +8352,9 @@ async function handleRequest(req, res) {
8315
8352
  // 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.
8316
8353
  // meeting-notes Run 1: the Notes routes keep notes in the per-user runtime directory and start the agent-summary session.
8317
8354
  if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex,
8318
- runtimeDir: STATE, spawnSession, jevModelOpts })) return;
8355
+ runtimeDir: STATE, spawnSession, jevModelOpts,
8356
+ // initiative my-work: every registered repository, the day runs, and the two presses that start sessions
8357
+ workspaceList: () => [...workspaces.values()], readDayRun, myWorkBrief, myWorkRead })) return;
8319
8358
  res.writeHead(404); res.end('not found');
8320
8359
  }
8321
8360
 
@@ -22,7 +22,7 @@ Rules that always hold:
22
22
  7. YOU CANNOT FINISH WITHOUT A REPORT. End every run with exactly one of `day deliver` (do, revise), `day sent` (send) or `day fail`.
23
23
 
24
24
  Procedure:
25
- 1. Run `pmos proactive day show TASK`. It prints the task: the title, the kind (approve or auto), the source, the link, the goal, the item context (where, who, about, ask, how), the draft so far and the notes of the PM for each revision. Read all of it. If the command says the task does not exist, or its status does not fit the mode, stop and run `pmos proactive day fail TASK --reason "<one sentence>"`.
25
+ 1. Run `pmos proactive day show TASK`. It prints the task: the title, the kind (approve or auto), the source, the link, the goal, the item context (where, who, about, ask, how), the draft so far and the notes of the PM for each revision. Read all of it. A task from My work also prints "instruction from the PM (this is the ask)". The PM read the ticket and wrote that instruction. Do what it says and nothing more, and read the item context as background. The rules above still hold: one draft, nothing sent, no pull request, no commit. If the instruction asks for one of those, draft it and say in the summary which step approval would trigger. If the command says the task does not exist, or its status does not fit the mode, stop and run `pmos proactive day fail TASK --reason "<one sentence>"`.
26
26
  2. If MODE is do:
27
27
  a. Read the sources the task names, read-only. Start from the link of the task. Read the conversation, the issue, the page or the file itself, and what it points to. Use `pmos proactive settings` to see which sources are on. If a source is off or you cannot reach it, say so in the summary.
28
28
  b. Do the work locally. A reply, a comment, a summary, a description or a plan is a draft in a file in the scratch folder of this session. Write the text exactly as it would be sent.
@@ -31,7 +31,7 @@ Procedure:
31
31
  Then run `pmos people settings`. It prints `calendar names: on` or `calendar names: off` for this repository (see rule 5).
32
32
  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.
33
33
  2. Read the sources that are on and reachable.
34
- - Calendar: today's events. Then the CALENDAR MEMORY: every event in the window that `pmos proactive calendar` named (at most 1000 events, at most 140 days). For each event keep the start, the end, the title, the calendar's recurring-event id as "series" if it has one, the number of attendees, "external": true when someone outside the PM's company attends, "exec": true when an executive attends, and the PM's "role" ("present" when the PM presents, "decide" when the PM decides, else "attend"). For an event that has ended, add "notes": one line in your own words of what happened, from its Wispr Flow notes, only when there are notes; and "open": up to 3 points still open from it. For a coming event that needs work first, add "prep": up to 6 steps, and "prep_done": how many of them are already done. Read only titles, times, attendee counts and notes. Never copy an invitation's text. Wispr Flow: today's and tomorrow's meeting notes. Jira: issues assigned to the PM or due soon. To tie a Jira issue to an initiative, run `pmos jira which <KEY>` (it reads local files only) and use the initiative it names. A status change of a ticket that `pmos jira which` names is not an item: the Jira status check shows it on the initiative's Jira tickets tab. Confluence: pages changed since yesterday. Mail, only if on: subjects and senders of threads waiting on the PM, no bodies.
34
+ - Calendar: today's events. Then the CALENDAR MEMORY: every event in the window that `pmos proactive calendar` named (at most 1000 events, at most 140 days). For each event keep the start, the end, the title, the calendar's recurring-event id as "series" if it has one, the number of attendees, "external": true when someone outside the PM's company attends, "exec": true when an executive attends, and the PM's "role" ("present" when the PM presents, "decide" when the PM decides, else "attend"). For an event that has ended, add "notes": one line in your own words of what happened, from its Wispr Flow notes, only when there are notes; and "open": up to 3 points still open from it. For a coming event that needs work first, add "prep": up to 6 steps, and "prep_done": how many of them are already done. Read only titles, times, attendee counts and notes. Never copy an invitation's text. Wispr Flow: today's and tomorrow's meeting notes. Jira: issues due soon that are NOT assigned to the PM. Do not read the issues assigned to the PM: My work owns them. Run `pmos my-work list --top 5 --json` instead and add each item it prints as a priority with "source": "jira" (or "github" for a WBS item), "owner": "pm", the item's key in the title, and "how": "Brief it in My work". Never make one of them an agent item: the PM briefs the agent in My work. If the command fails, say so in the evidence of one item and go on. To tie a Jira issue to an initiative, run `pmos jira which <KEY>` (it reads local files only) and use the initiative it names. A status change of a ticket that `pmos jira which` names is not an item: the Jira status check shows it on the initiative's Jira tickets tab. Confluence: pages changed since yesterday. Mail, only if on: subjects and senders of threads waiting on the PM, no bodies.
35
35
  - GitHub, if on: pull requests that wait on the PM for review, pull requests of the PM that have a review or a failed check, and issues assigned to the PM. Keep the repository, the number, the title and the state.
36
36
  - Drive, if on: files shared with the PM or changed since yesterday that a meeting or a goal names. Keep the name, the owner and the link.
37
37
  - Figma, if on: files the PM owns or was asked to look at, with new comments since yesterday. Keep the file, the commenter and a short paraphrase of the comment.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: my-work-read
3
+ label: "My work read"
4
+ description: "Read, read-only, the open Jira issues assigned to the PM with their comments and links, write a short context card and a suggested agent instruction for each, and submit them to My work. It writes nothing to Jira."
5
+ run: agent
6
+ agent: claude
7
+ output:
8
+ - Read
9
+ - Not reached
10
+ - Needs you
11
+ ---
12
+
13
+ /my-work-read — read the Jira issues assigned to the PM and submit one context card for each to My work. You read Jira with the PM's own Atlassian connection. The PM reads your cards in My work and decides what an agent does. You change nothing in Jira.
14
+
15
+ Rules that always hold:
16
+ 1. READ-ONLY on Jira. Use only the Jira search and read tools. Never create, edit, transition, assign, comment on, link, close or delete an issue. Never write to Confluence or to any other source.
17
+ 2. THE ONLY WRITE is `pmos my-work submit --file <read.json>`, once, at the end. Never edit a repository file. Never commit.
18
+ 3. LIGHT ON CONTENT. Write every field in your own words. Quote no more than one short line from a comment. Never copy a description, an attachment, an email address or a token. Skip anything marked private, personal, confidential or HR.
19
+ 4. NOTHING INVENTED. If a field is not in what you read, leave it out. Do not guess a due date, a priority or an estimate.
20
+ 5. YOU DO NOT DECIDE FOR THE PM. A card describes the ask and suggests an instruction. It does not start work, and it does not say the agent must resolve the ticket.
21
+ 6. TEMPORARY FILES OUTSIDE THE REPOSITORY. Write the read file in this session's scratch folder, and delete it after the submit.
22
+
23
+ Procedure:
24
+ 1. Search Jira with `assignee = currentUser() AND resolution = Unresolved ORDER BY priority DESC, duedate ASC`. Read at most 50 issues. If Jira cannot be reached, say so under "Not reached" and submit nothing: an empty read would remove every Jira item from My work.
25
+ 2. For each issue, read its summary, priority, status, status category, due date, estimate (story points or a t-shirt size), its "blocks" links, its comments and the issues and pages it links to.
26
+ 3. Write one card:
27
+ - "key": the issue key. "title": the summary. "url": the issue's browse link.
28
+ - "priority": Highest, High, Medium or Low (map Critical and Blocker to Highest, Minor and Trivial to Low). "status": the status name. "category": todo, doing or done.
29
+ - "due": the due date as YYYY-MM-DD. "estimate": XS, S, M, L or XL (story points 1 is XS, 2 is S, 3 is M, 5 is L, 8 or more is XL). "blocks": how many issues it blocks.
30
+ - "from": the display name of the person who assigned it to the PM, or the reporter.
31
+ - "ask": one or two sentences: what that person wants from the PM.
32
+ - "thread": at most three sentences: what the comments say so far.
33
+ - "open": up to five questions that are still not answered.
34
+ - "suggest": an instruction the PM could give an agent, in the imperative, for example "Draft a reply to Ana that proposes the three tiers and asks who signs off". An agent drafts and sends nothing until the PM approves, and it cannot open a pull request, so suggest a draft, a comment, a summary or a plan.
35
+ - "kind": "decide" when the ask is a decision only the PM can make (a priority, money, a contract, a gate, a person), else "agent".
36
+ - "initiative": the initiative id that `pmos jira which <KEY>` names, if it names one.
37
+ 4. Write {"items": [ …the cards… ]} to a file in the scratch folder. Run `pmos my-work submit --file <that file>`. If it is refused, read the reason, fix the file and submit once more.
38
+ 5. Finish with the output contract.
39
+
40
+ Output contract — produce EXACTLY these sections in your final message:
41
+ - Read
42
+ - Not reached
43
+ - Needs you
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": "initiative my-work: the ranking, the read, the fold, the brief that adds a day task, and the CLI.",
3
+ "modules": [
4
+ "my-work.test.js"
5
+ ],
6
+ "seconds": {
7
+ "my-work.test.js": 6,
8
+ "pm-my-work-test.js": 25
9
+ },
10
+ "panels": [
11
+ "pm-my-work-test.js"
12
+ ]
13
+ }