create-pmos 0.116.0 → 0.118.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.
Files changed (33) 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/guide/notes.md +43 -3
  7. package/workspace/guide/today.md +22 -13
  8. package/workspace/lib/brief.js +2 -2
  9. package/workspace/lib/day-marks.js +29 -9
  10. package/workspace/lib/goals.js +68 -10
  11. package/workspace/lib/notes.js +139 -17
  12. package/workspace/lib/routes-notes.js +68 -11
  13. package/workspace/package-lock.json +2 -2
  14. package/workspace/package.json +1 -1
  15. package/workspace/public/pm/components.css +289 -91
  16. package/workspace/public/pm/dest-notes.js +136 -29
  17. package/workspace/public/pm/note-dock.js +204 -164
  18. package/workspace/public/pm/notes-context.js +60 -0
  19. package/workspace/public/pm/notes-links.js +32 -6
  20. package/workspace/public/pm/primitives.js +1 -1
  21. package/workspace/public/pm/today-agenda.js +27 -53
  22. package/workspace/public/pm/today-copy.js +34 -15
  23. package/workspace/public/pm/today-day.js +98 -76
  24. package/workspace/public/pm/today-goals.js +159 -15
  25. package/workspace/public/pm/today-jobs.js +84 -0
  26. package/workspace/public/pm/today-mentions.js +50 -0
  27. package/workspace/public/pm/topbar.js +71 -32
  28. package/workspace/public/pm/view-notes.js +142 -73
  29. package/workspace/server.js +10 -8
  30. package/workspace/test-files.json +4 -0
  31. package/workspace/test-lists/today-goals-jobs-ui.json +5 -0
  32. package/workspace/test-lists/today-goals-jobs.json +5 -0
  33. package/workspace/test-lists/today-mentions-cards.json +5 -0
@@ -27,9 +27,11 @@
27
27
  import { el } from './dom.js';
28
28
  import { button, iconButton, badge, toast, row, tabs, field, menu, meter } from './primitives.js';
29
29
  import { pmGet, pmPost } from './http.js';
30
- import { FIXTURE } from './state.js';
30
+ import { FIXTURE, state } from './state.js';
31
31
  import { COPY } from './today-copy.js';
32
- import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
32
+ import { goalRows, goalAdd, goalDetail, goalJobsBlock, jobsWithoutGoal, addGoal, goalsSig } from './today-goals.js';
33
+ import { statusPill, whoActs, mark as whoMark, tag, tagged, originOf, goalPicker } from './today-jobs.js';
34
+ import { hasCard, mentionCard } from './today-mentions.js';
33
35
  import { peopleLinks } from './people-link.js';
34
36
  import { agendaPanel, meetingDetail, memoryBlock, meetingFacts, keyItems, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
35
37
  import { notesBlock, onNotes } from './notes-links.js';
@@ -58,7 +60,7 @@ const cache = new Map(); // ws -> { data, error }
58
60
  const runs = new Map(); // ws -> the last "Prepare my day" press: { state: 'busy'|'started'|'running'|'failed', at, session, error }
59
61
  const ui = {
60
62
  open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
61
- section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions', 'summary'].includes(v) ? v : 'agenda'; })(),
63
+ section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions'].includes(v) ? v : 'agenda'; })(),
62
64
  sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
63
65
  revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
64
66
  // today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
@@ -129,7 +131,9 @@ function jobOf(item, task, worth) {
129
131
  const status = task ? task.status : kind === 'you' ? 'you' : 'notStarted';
130
132
  const group = worth ? 'worth' : ['you', 'review', 'failed'].includes(status) ? 'needs' : ['done', 'sent'].includes(status) ? 'done' : 'working';
131
133
  return {
132
- id: item ? item.id : task.item_id, item, task, kind, status, group, worth,
134
+ id: item ? item.id : task.item_id || task.id, item, task, kind, status, group, worth,
135
+ // the id a link mark is kept under: the brief item, else the run task (PRD L1)
136
+ linkId: item ? item.id : task.id,
133
137
  title: (item && item.title) || (task && task.title) || '', source: (item && item.source) || (task && task.source) || '',
134
138
  link: (item && isUrl(item.link) && item.link) || (task && isUrl(task.link) && task.link) || null,
135
139
  goal: (item && item.goal) || (task && task.goal) || null, isNew: !!(item && item.new),
@@ -181,7 +185,9 @@ function model(d, view) {
181
185
  const needsPrep = !ended && hadPrep;
182
186
  const st = mark && mark.state;
183
187
  const group = !needsPrep ? null : st === 'done' || st === 'dismissed' ? 'done' : st === 'snoozed' ? 'later' : 'needs';
184
- return { ...e, id: e.id || 'ev-' + i, s, f, time: hhmm(e.start), ended, mark, before, hadPrep, needsPrep, group };
188
+ // the goal a meeting counts toward: the brief's own, else the goal the PM made from it (event_id), PRD A4
189
+ const gid = (e.goal && goals.some((g) => g.id === e.goal) ? e.goal : null) || ((goals.find((g) => g.event_id && g.event_id === e.id) || {}).id) || null;
190
+ return { ...e, id: e.id || 'ev-' + i, gid, s, f, time: hhmm(e.start), ended, mark, before, hadPrep, needsPrep, group };
185
191
  }).sort((a, b) => a.s - b.s);
186
192
  const settings = (d && d.settings) || null;
187
193
  const scopes = (settings && settings.scopes) || {};
@@ -194,6 +200,15 @@ function model(d, view) {
194
200
  const repos = (d && Array.isArray(d.repos)) ? d.repos : [];
195
201
  const repoName = (id) => { const r = repos.find((x) => x.id === id); return r ? r.name : id; };
196
202
  for (const j of jobs) j.repo = itemRepos[j.id] || null;
203
+ // the goal of a job (initiative today-goals-jobs, PRD G4, L1): the plan puts a brief item under at most one open goal,
204
+ // after the PM's link mark; a job the plan does not place keeps the PM's mark, else the goal it came with
205
+ const stepGoal = new Map();
206
+ for (const g of goals) for (const st of g.steps || []) if (st.item_id) stepGoal.set(st.item_id, g.id);
207
+ for (const j of jobs) {
208
+ const lk = marks[j.linkId];
209
+ const linked = !!lk && lk.goal !== undefined;
210
+ j.goal = (j.item && stepGoal.get(j.item.id)) || (linked ? lk.goal || null : j.goal);
211
+ }
197
212
  // the PM's marks (today-act-prepare, PRD M1 to M6): a job the PM owns leaves Needs you when it is done, snoozed, held
198
213
  // or not the PM's; a snooze or hold whose time has passed comes back as open (the server reads it so)
199
214
  for (const j of jobs) {
@@ -247,16 +262,6 @@ export function repoPicker(m, cur, onPick, attrs) {
247
262
  sel.addEventListener('change', () => onPick(sel.value || null));
248
263
  return sel;
249
264
  }
250
- function statusPill(j) {
251
- const s = D.status2;
252
- const map = {
253
- notStarted: [s.notStarted, ''], waiting: [s.waiting, ''], working: [s.working, 'info'], sending: [s.sending, 'info'],
254
- review: [j.kind === 'auto' ? s.reviewAuto : s.review, 'purple'], sent: [s.sent, 'ok'], done: [s.done, 'ok'],
255
- failed: [s.failed, 'bad'],
256
- };
257
- const [t, tone] = map[j.status] || [j.status, ''];
258
- return pill(t, tone, { 'data-day-status': j.status });
259
- }
260
265
  /** A link that opens outside the workspace, drawn as a small button. Only http and https addresses are drawn. */
261
266
  function extLink(label, href, attrs) {
262
267
  if (!isUrl(href)) return null;
@@ -412,24 +417,37 @@ function topBar(ctx) {
412
417
  const started = m.run && m.run.started_at ? hhmm(m.run.started_at) : '';
413
418
  const status = !m.run ? (busy ? D.starting : D.status.idle) : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
414
419
  : D.status.last(started, m.needs);
415
- let check = null;
420
+ // S2: the status line (Last prepared), Check now, Settings and the way to the items with no repository are one menu
421
+ const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
422
+ const lines = [{ label: status, attrs: { 'data-day': 'status', 'aria-disabled': 'true' }, onSelect: () => {} }];
423
+ if (m.tasks.length) {
424
+ const c = { review: 0, done: 0, working: 0, failed: 0 };
425
+ for (const t of m.tasks) {
426
+ if (t.status === 'review') c.review++;
427
+ else if (t.status === 'done' || t.status === 'sent') c.done++;
428
+ else if (t.status === 'failed') c.failed++;
429
+ else c.working++;
430
+ }
431
+ lines.push({ label: D.summary(c), attrs: { 'data-day': 'summary', 'aria-disabled': 'true' }, onSelect: () => {} });
432
+ }
416
433
  if (m.run && m.check) {
417
434
  const every = m.settings && m.settings.dayCheck && typeof m.settings.dayCheck.every === 'number' ? m.settings.dayCheck.every : m.check.every;
418
435
  const running = !!m.check.running || ui.busy.has('check');
419
- check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check', title: D.checkOff }, D.checkOff)
420
- : el('span', { class: 'td-day__check', 'data-day': 'check', title: running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next)) },
421
- iconButton({ icon: 'refresh-cw', ariaLabel: D.checkNow, size: 'sm', attrs: { 'data-day': 'check-now', disabled: running ? 'true' : null }, onClick: () => checkNow(ws) }));
436
+ lines.push({ label: every === 0 ? D.checkOff : running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next)), attrs: { 'data-day': 'check', 'aria-disabled': 'true' }, onSelect: () => {} });
437
+ if (every !== 0 && !running) lines.push({ label: D.checkNow, icon: 'refresh-cw', attrs: { 'data-day': 'check-now' }, onSelect: () => checkNow(ws) });
422
438
  }
423
- const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
424
- const open = m.allGoals.filter((g) => g.status === 'open').length;
439
+ const items = [...lines, { sep: true },
440
+ { label: D.sources, icon: 'settings', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all' }, onSelect: () => select('sources', 'all') }];
441
+ if (m.narrow) items.push({ label: D.menu.showAll, attrs: { 'data-day': 'show-all' }, onSelect: () => ctx.showAll() });
442
+ const more = iconButton({ icon: 'ellipsis', ariaLabel: D.menu.label, size: 'sm', attrs: { 'data-day': 'bar-menu', 'aria-haspopup': 'menu', 'aria-current': ctx.cur('sources', 'all') },
443
+ onClick: () => menu({ anchor: more, ariaLabel: D.menu.label, items }) });
425
444
  return el('header', { class: 'td-day__bar' },
426
- (() => { const when = [dateLabel(m.now), D.unfinished(open, m.allGoals.length)].filter(Boolean).join(' · ');
445
+ (() => { const when = dateLabel(m.now);
427
446
  return el('div', { class: 'td-day__hello', title: greet(m.now) + ' ' + when }, el('b', null, greet(m.now)), ' ', el('span', { class: 'td-day__dim' }, when)); })(),
428
447
  steps,
429
- el('span', { class: 'td-day__status', role: 'status', 'data-day': 'status', title: status }, status),
430
- check,
431
- iconButton({ icon: 'settings', ariaLabel: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
448
+ el('span', { class: 'td-day__spacer', 'aria-hidden': 'true' }),
432
449
  button({ label, variant: 'primary', size: 'sm', disabled: busy || reading, attrs: { 'data-day': 'prepare', 'data-goals': 'prepare-day', title: D.prepareHint, 'aria-busy': busy ? 'true' : 'false' }, onClick: () => prepareDay(ws) }),
450
+ more,
433
451
  iconButton({ icon: 'chevron-down', ariaLabel: D.fold, size: 'sm', expanded: true, attrs: { 'data-day': 'toggle' }, onClick: () => fold(false) }));
434
452
  }
435
453
 
@@ -477,7 +495,7 @@ function calendarRows(ctx) {
477
495
  const nowRow = () => el('li', { class: 'td-day__nowrow', 'data-day': 'now', 'aria-hidden': 'true' }, D.now(hhmm(m.now)));
478
496
  for (const e of m.events) {
479
497
  if (!nowShown && e.s > m.now) { rows.push(nowRow()); nowShown = true; }
480
- const n = e.goal ? m.goalNo.get(e.goal) : null;
498
+ const n = e.gid ? m.goalNo.get(e.gid) : null;
481
499
  rows.push(rowBtn(ctx, 'event', e.id, { lead: e.time, title: e.title, meta: e.purpose || e.with || '',
482
500
  trail: el('span', { class: 'td-day__chips' }, n ? pill(D.pill.goal(n), 'goal') : null, eventChip(e)) },
483
501
  { 'data-past': e.ended ? 'true' : null, 'data-goal-tie': n ? 'true' : null }));
@@ -499,10 +517,14 @@ function jobRow(ctx, j) {
499
517
  : [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
500
518
  const mine = j.status === 'you';
501
519
  const due = mine && j.item && j.item.due ? Date.parse(j.item.due) : NaN;
502
- const meta = mine && j.mark && j.mark.state === 'waiting' ? `${A.hold.replace('…', '')} ${j.mark.on}` : sub;
520
+ const held = mine && j.mark && j.mark.state === 'waiting' ? `${A.hold.replace('…', '')} ${j.mark.on}` : sub;
521
+ // J1: the row says who acts, in words and with a square (open for the PM, filled for the agent)
522
+ const who = whoActs(j);
523
+ const meta = [who.text, held].filter(Boolean).join(' · ');
503
524
  return rowBtn(ctx, 'job', j.id, { lead: mine ? verbChip(verbOf(j.item)) : n ? D.pill.goal(n) : '·', title: j.title, meta,
504
525
  trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mine ? dueChip(ctx.m, j.mark, due, false) : statusPill(j)),
505
- dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
526
+ dot: el('span', { class: 'td-day__marks2' }, whoMark(who.who), j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null) },
527
+ { 'data-who': who.who });
506
528
  }
507
529
 
508
530
  /* What the repositories owe the PM outside today's jobs (outcome checks, report drafts, sessions) are jobs too: each one is a
@@ -552,31 +574,6 @@ function jobsPanel(ctx) {
552
574
  }));
553
575
  }
554
576
 
555
- /* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
556
- * session each and keeps their state; this reads that state, so it needs no session of its own. */
557
- function summaryPanel(ctx) {
558
- const { m } = ctx;
559
- const ts = m.tasks;
560
- if (!ts.length) return null;
561
- const c = { review: 0, done: 0, working: 0, failed: 0 };
562
- for (const t of ts) {
563
- if (t.status === 'review') c.review++;
564
- else if (t.status === 'done' || t.status === 'sent') c.done++;
565
- else if (t.status === 'failed') c.failed++;
566
- else c.working++;
567
- }
568
- const finished = ts.filter((t) => t.status !== 'waiting' && t.status !== 'working' && t.status !== 'sending');
569
- const byTask = new Map(m.jobs.filter((j) => j.task).map((j) => [j.task.id, j]));
570
- const rows = finished.map((t) => {
571
- const j = byTask.get(t.id);
572
- const what = t.status === 'failed' ? t.error : t.sent && t.sent.link ? t.sent.link : t.draft ? t.draft.summary : '';
573
- return j ? rowBtn(ctx, 'job', j.id, { lead: '·', title: j.title, meta: what || '', trail: statusPill(j) }) : null;
574
- }).filter(Boolean);
575
- return panel('summary', null,
576
- el('p', { class: 'td-day__note', 'data-day': 'summary' }, c.working ? D.summary(c) : D.summaryAll(ts.length) + ' ' + D.summary(c)),
577
- rows.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'summary' }, ...rows) : null);
578
- }
579
-
580
577
  /* What waits for the PM outside today's jobs (the Today cards: outcome checks, reports, sessions). Today hands its list
581
578
  * in, so the cards keep their own controls and the open day shows one layout. */
582
579
  function goalsPanel(ctx) {
@@ -584,7 +581,7 @@ function goalsPanel(ctx) {
584
581
  // a goal of another repository is not lost: the panel says how many the Show switch hides, with the way to them
585
582
  const hidden = m.hiddenGoals ? el('p', { class: 'td-day__note', 'data-day': 'goals-hidden' }, D.repo.hiddenGoals(m.hiddenGoals) + ' ',
586
583
  button({ label: D.repo.showAll, variant: 'ghost', size: 'sm', attrs: { 'data-day': 'show-all' }, onClick: () => ctx.showAll() })) : null;
587
- return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), hidden, goalAdd(ctx));
584
+ return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), hidden, goalAdd(ctx), jobsWithoutGoal(ctx));
588
585
  }
589
586
 
590
587
  function mentionStatus(j) {
@@ -610,6 +607,8 @@ function mentionsPanel(ctx) {
610
607
  const shown = m.mentions.filter((j) => ui.mtab === 'all' || j.source === ui.mtab);
611
608
  const rows = shown.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'mentions' }, ...shown.map((j) => {
612
609
  const it = j.item;
610
+ // Slack, Confluence and Figma draw as a card of their own (PRD M1); the press, the link and the status are the row's
611
+ if (hasCard(j.source)) return rowBtn(ctx, 'mention', j.id, { title: mentionCard(j, srcName(j.source)), trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) }, { 'data-mention-card': j.source });
613
612
  return rowBtn(ctx, 'mention', j.id, { lead: el('span', { class: 'td-day__chip' }, srcName(j.source)),
614
613
  title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) });
615
614
  })) : note(D.noMentions);
@@ -752,7 +751,7 @@ function composer(ctx, j, verb) {
752
751
  sendPress(ctx.ws, j, { text, verdict: review ? verdict : undefined }, to);
753
752
  };
754
753
  return el('div', { class: 'td-day__compose', 'data-day': 'compose' }, f, chips, toggle,
755
- el('div', { class: 'td-day__acts' }, button({ label: review ? A.postReview(to) : A.sendTo(to), variant: 'primary', size: 'sm', iconEnd: 'arrow-right', disabled: busy, attrs: { 'data-day-act': 'send' }, onClick: go })),
754
+ el('div', { class: 'td-day__acts' }, button({ label: tagged(review ? A.postReview(to) : A.sendTo(to), 'you'), variant: 'primary', size: 'sm', iconEnd: 'arrow-right', disabled: busy, attrs: { 'data-day-act': 'send' }, onClick: go })),
756
755
  el('span', { class: 'td-day__dim' }, A.sendNote));
757
756
  }
758
757
  /** The four presses of a job (Mark done, Snooze, Waiting on…, Not mine) or the three of a meeting (Mark ready, Remind
@@ -764,19 +763,19 @@ function marksBar(ctx, { id, title, kind, due, start, strong }) {
764
763
  const st = (m.d && m.d.marks && m.d.marks[id] && m.d.marks[id].state) || null;
765
764
  const times = snoozeTimes(m, { due, start });
766
765
  const kids = [];
767
- if (st !== 'done') kids.push(button({ label: prep ? A.ready : A.done, variant: strong ? 'primary' : 'outline', size: 'sm', icon: 'check', disabled: busy, attrs: { 'data-day-mark': 'done' },
766
+ if (st !== 'done') kids.push(button({ label: prep ? A.ready : tagged(A.done, 'you'), variant: strong ? 'primary' : 'outline', size: 'sm', icon: 'check', disabled: busy, attrs: { 'data-day-mark': 'done' },
768
767
  onClick: () => markPress(ws, id, { action: 'done' }, (prep ? A.did.ready : A.did.done)(title)) }));
769
768
  if (times.length) {
770
- const b = button({ label: prep ? A.remind : A.snooze, size: 'sm', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'snooze', 'aria-haspopup': 'menu' },
769
+ const b = button({ label: prep ? A.remind : tagged(A.snooze, 'you'), size: 'sm', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'snooze', 'aria-haspopup': 'menu' },
771
770
  onClick: () => menu({ anchor: b, ariaLabel: prep ? A.remind : A.snooze, items: times.map(([l, t]) => ({ label: `${l} · ${hhmm(t)}`, attrs: { 'data-day-snooze': hhmm(t) },
772
771
  onSelect: () => markPress(ws, id, { action: 'snooze', until: new Date(t).toISOString() }, A.did.snooze(hhmm(t))) })) }) });
773
772
  kids.push(b);
774
773
  }
775
- if (!prep) kids.push(button({ label: A.hold, size: 'sm', disabled: busy, attrs: { 'data-day-mark': 'hold', 'aria-expanded': ui.holding === id ? 'true' : 'false' },
774
+ if (!prep) kids.push(button({ label: tagged(A.hold, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-mark': 'hold', 'aria-expanded': ui.holding === id ? 'true' : 'false' },
776
775
  onClick: () => { ui.holding = ui.holding === id ? null : id; ui.holdDraft = { who: '', when: 'h2' }; rerender(); } }));
777
776
  const reasons = prep ? ['nothing', 'elsewhere'] : ['no-action', 'someone-else', 'done-elsewhere'];
778
777
  if (st !== 'dismissed') {
779
- const nb = button({ label: prep ? A.skip : A.notMine, size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'dismiss', 'aria-haspopup': 'menu' },
778
+ const nb = button({ label: prep ? A.skip : tagged(A.notMine, 'you'), size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'dismiss', 'aria-haspopup': 'menu' },
780
779
  onClick: () => menu({ anchor: nb, ariaLabel: prep ? A.skip : A.notMine, items: reasons.map((w) => ({ label: A.why[w], attrs: { 'data-day-why': w },
781
780
  onSelect: () => markPress(ws, id, { action: 'dismiss', why: w }, A.did.dismiss(A.why[w])) })) }) });
782
781
  kids.push(el('span', { class: 'td-day__grow', 'aria-hidden': 'true' }), nb);
@@ -840,7 +839,7 @@ function actCard(ctx, j) {
840
839
  canSend ? composer(ctx, j, verb) : null,
841
840
  open ? el('div', { class: 'td-day__acts', 'data-day': 'cta' },
842
841
  (canSend ? extLink : primaryLink)(A.open[j.source] || A.openLink, j.link, { 'data-day-link': 'conversation' }),
843
- j.item && j.item.owner === 'pm' ? button({ label: A.draft, size: 'sm', disabled: ui.busy.has('draft:' + j.id), attrs: { 'data-day-act': 'draft' }, onClick: () => draftPress(ctx.ws, j) }) : null) : null,
842
+ j.item && j.item.owner === 'pm' ? button({ label: tagged(A.draft, 'agent'), size: 'sm', disabled: ui.busy.has('draft:' + j.id), attrs: { 'data-day-act': 'draft' }, onClick: () => draftPress(ctx.ws, j) }) : null) : null,
844
843
  marksBar(ctx, { id: j.id, title: j.title, kind: 'item', due, strong: allDone }),
845
844
  more ? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.more), more, note(A.readOnly)) : null);
846
845
  }
@@ -928,8 +927,8 @@ function stepItem(st) {
928
927
  function eventDetail(ctx, e) {
929
928
  const { m } = ctx;
930
929
  const F = A.focus;
931
- const n = e.goal ? m.goalNo.get(e.goal) : null;
932
- const g = n ? m.allGoals[n - 1] : null;
930
+ const g = e.gid ? m.allGoals.find((x) => x.id === e.gid) || null : null;
931
+ const n = g ? m.goalNo.get(g.id) : null;
933
932
  const ini = e.initiative && e.initiative.id ? e.initiative : null;
934
933
  const steps = e.steps || {};
935
934
  const notes = e.notes || {};
@@ -947,7 +946,7 @@ function eventDetail(ctx, e) {
947
946
  mailBlock || note(D.noMail)) : null;
948
947
  const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text)) : null;
949
948
  const addGoalItem = !g && m.allGoals.length < m.maxGoals
950
- ? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } }] : [];
949
+ ? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null, { event_id: e.id })) toast({ text: D.goalAdded }); } }] : [];
951
950
  // the one line under the title: the time, how many people join, and a star for a key meeting (F5)
952
951
  const meta = [`${e.time}${e.end ? '–' + hhmm(e.end) : ''}`, facts && facts.people ? F.people(facts.people) : null, facts && facts.key ? F.key : null].filter(Boolean).join(' · ');
953
952
  const duringAfter = [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem))];
@@ -969,6 +968,7 @@ function eventDetail(ctx, e) {
969
968
  e.purpose ? el('p', { class: 'td-day__purpose', 'data-day': 'purpose' }, e.purpose) : null,
970
969
  started ? notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'live', liveText: F.live }) : null,
971
970
  e.hadPrep ? prepBlock(ctx, e, { fold: started }) : null,
971
+ g ? goalJobsBlock(ctx, g, n) : null,
972
972
  after,
973
973
  started ? null : notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'quiet' }),
974
974
  el('div', { class: 'td-day__foot', 'data-day': 'foot' },
@@ -987,27 +987,27 @@ function jobCard(ctx, j) {
987
987
  const revs = t && Array.isArray(t.revisions) ? t.revisions.length : 0;
988
988
  const statusNote = { notStarted: D.notTask, waiting: D.waitingNote, working: D.workingNote, sending: D.sendingNote }[j.status]
989
989
  || (j.status === 'done' && j.kind === 'auto' ? D.autoNote : null);
990
- const draft = t && t.draft ? el('div', { class: 'td-day__draftbox', 'data-day': 'draft' }, el('h4', null, D.draft),
990
+ // J5: a finished job shows its result (the run's summary and draft) inside the job; there is no separate Result section
991
+ const finished = !!t && (j.status === 'done' || j.status === 'sent');
992
+ const draft = t && t.draft ? el('div', { class: 'td-day__draftbox', 'data-day': finished ? 'result' : 'draft' }, el('h4', null, finished ? D.result : D.draft),
991
993
  t.draft.summary ? el('p', null, t.draft.summary) : null,
992
994
  t.draft.body ? el('pre', { class: 'td-day__draft' }, t.draft.body) : null) : null;
993
995
  const acts = [];
994
996
  const canRevise = t && (j.status === 'review' || j.status === 'done') && revs < MAX_REVISIONS;
995
- if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: D.approve, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
996
- if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: D.approveAuto, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
997
- if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: D.revise, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
998
- if (t && j.status === 'failed') acts.push(button({ label: D.retry, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'retry' }, onClick: () => decide(ws, t, 'retry') }));
997
+ if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: tagged(D.approve, 'you'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
998
+ if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: tagged(D.approveAuto, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
999
+ if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: tagged(D.revise, 'agent'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
1000
+ if (t && j.status === 'failed') acts.push(button({ label: tagged(D.retry, 'agent'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'retry' }, onClick: () => decide(ws, t, 'retry') }));
999
1001
  acts.push(extLink(D.openConversation, j.link, { 'data-day-link': 'conversation' }));
1000
1002
  if (t && t.draft) acts.push(extLink(D.openDraft, t.draft.link, { 'data-day-link': 'draft' }));
1001
1003
  if (t && t.sent) acts.push(extLink(D.openSent, t.sent.link, { 'data-day-link': 'sent' }));
1002
- const sid = t && (t.session_id || t.stalled_session);
1003
- if (sid) acts.push(button({ label: D.openSession, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' }, onClick: () => openSession({ id: sid, name: 'day-task' }) }));
1004
1004
  let form = null;
1005
1005
  if (t && ui.revising.has(t.id) && canRevise) {
1006
1006
  const f = field({ label: D.noteLabel, kind: 'textarea', value: ui.notes.get(t.id) || '', placeholder: D.noteHint, hideLabel: true, attrs: { maxlength: '500', 'data-day-input': 'note-' + t.id } });
1007
1007
  const ta = f.control;
1008
1008
  ta.addEventListener('input', () => { ui.notes.set(t.id, ta.value); });
1009
1009
  form = el('div', { class: 'td-day__revise', 'data-day': 'revise' }, f, el('div', { class: 'td-day__acts' },
1010
- button({ label: D.sendNote, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'send-note' }, onClick: () => {
1010
+ button({ label: tagged(D.sendNote, 'agent'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'send-note' }, onClick: () => {
1011
1011
  const v = (ui.notes.get(t.id) || '').trim();
1012
1012
  if (!v || v.length > 500) { toast({ text: D.noteNeeded, tone: 'destructive' }); return; }
1013
1013
  decide(ws, t, 'revise', v);
@@ -1028,15 +1028,39 @@ function jobCard(ctx, j) {
1028
1028
  form);
1029
1029
  }
1030
1030
 
1031
+ /** The goal of a job is the PM's link mark (PRD L1). The press says what it did, with Undo, like the other marks. */
1032
+ function linkPress(ctx, linkId, goalId) {
1033
+ const g = goalId ? ctx.m.allGoals.find((x) => x.id === goalId) : null;
1034
+ return markPress(ctx.ws, linkId, { action: 'link', goal: goalId }, g ? D.linked(g.text) : D.unlinked);
1035
+ }
1036
+
1037
+ /** What a job says about itself (PRD J1, J3, J4): who acts, where it came from, its goal and, with a session, the way to it. */
1038
+ function jobMeta(ctx, j) {
1039
+ const { m } = ctx;
1040
+ const who = whoActs(j);
1041
+ const t = j.task;
1042
+ const sid = t && (t.session_id || t.stalled_session);
1043
+ const known = sid ? (state.sessions || []).find((x) => x && x.id === sid) : null;
1044
+ const name = (known && known.name) || 'day-task';
1045
+ const origin = originOf(j);
1046
+ const pick = goalPicker(m, j.goal, (g) => linkPress(ctx, j.linkId, g), { attrs: { 'data-day-job-goal': j.id } });
1047
+ return el('div', { class: 'td-day__jobmeta', 'data-day': 'jobmeta' },
1048
+ el('p', { class: 'td-day__who2', 'data-day': 'who', 'data-who': who.who }, whoMark(who.who), el('span', null, who.text)),
1049
+ dl([[D.origin.label, el('span', { 'data-day': 'origin', 'data-origin': origin }, D.origin[origin])], [D.goalCtl.label, pick]]),
1050
+ sid ? el('div', { class: 'td-day__acts' }, button({ label: tagged(D.openSessionOf(name), 'you'), size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' },
1051
+ onClick: () => openSession({ id: sid, name }) })) : null);
1052
+ }
1053
+
1031
1054
  function jobDetail(ctx, j) {
1032
1055
  const { m } = ctx;
1033
1056
  const n = j.goal ? m.goalNo.get(j.goal) : null;
1034
- const meets = m.events.filter((e) => e.goal && e.goal === j.goal);
1057
+ const meets = m.events.filter((e) => e.gid && e.gid === j.goal);
1035
1058
  const mention = j.item && j.item.mention;
1036
1059
  return [
1037
1060
  dhead(n ? D.kind.jobGoal(n) : D.kind.job, null),
1038
1061
  j.repo ? el('div', { class: 'td-day__acts' }, repoTag(m, j.repo)) : null,
1039
1062
  jobCard(ctx, j),
1063
+ jobMeta(ctx, j),
1040
1064
  meets.length || mention ? el('div', { class: 'td-day__rel' }, el('h4', null, D.related), el('ul', { class: 'td-day__rows' },
1041
1065
  ...meets.map((e) => relRow(ctx, 'event', e.id, e.title, e.time)),
1042
1066
  mention ? relRow(ctx, 'mention', j.id, j.item.ask || j.title, srcName(j.source)) : null)) : null,
@@ -1048,6 +1072,7 @@ function mentionDetail(ctx, j) {
1048
1072
  const reply = typeof it.reply_needed === 'boolean' ? (it.reply_needed ? D.yes : D.no) : null;
1049
1073
  return [
1050
1074
  dhead(D.kind.mention(srcName(j.source)), it.about || j.title),
1075
+ hasCard(j.source) ? el('div', { class: 'td-day__mcbox', 'data-day': 'mention-card' }, mentionCard(j, srcName(j.source))) : null,
1051
1076
  dl([[D.where, D.fromWho(it.who, it.where)], [D.ask, it.ask || it.why_now || null], [D.reply, reply]]),
1052
1077
  el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(j.source)), j.link, { 'data-day-link': 'conversation' }), mentionStatus(j)),
1053
1078
  el('div', { class: 'td-day__rel' }, el('h4', null, D.theJob), el('ul', { class: 'td-day__rows' }, relRow(ctx, 'job', j.id, j.title, '', j))),
@@ -1265,7 +1290,6 @@ function sectionSelector(ctx) {
1265
1290
  { id: 'goals', icon: 'target', label: D.sections.goals, count: openGoals },
1266
1291
  { id: 'jobs', icon: 'list-checks', label: D.sections.jobs, count: m.jobs.length },
1267
1292
  { id: 'mentions', icon: 'text-quote', label: D.sections.mentions, count: m.mentions.length },
1268
- { id: 'summary', icon: 'bot', label: D.sections.summary, count: null },
1269
1293
  ];
1270
1294
  return tabs({ variant: 'segmented', role: 'tablist', ariaLabel: D.sections.label, value: ui.section, full: true, attrs: { 'data-day': 'section-tabs' },
1271
1295
  items,
@@ -1284,8 +1308,6 @@ function sectionContent(ctx) {
1284
1308
  return jobsPanel(ctx);
1285
1309
  case 'mentions':
1286
1310
  return mentionsPanel(ctx);
1287
- case 'summary':
1288
- return summaryPanel(ctx) || note(D.noResult, { 'data-day': 'no-result' });
1289
1311
  default:
1290
1312
  return agenda(ctx);
1291
1313
  }
@@ -1309,7 +1331,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
1309
1331
  }
1310
1332
  const sel = ui.sel;
1311
1333
  const ctx = {
1312
- ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker,
1334
+ ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker, mark: (id, op, said) => markPress(ws, id, op, said),
1313
1335
  showAll: () => { if (setState) setState({ todayScope: 'all' }); },
1314
1336
  cur: (type, id) => { const s = sel || defaultSel(m, ctx.owed); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
1315
1337
  owed: owedItems(owed),