create-pmos 0.61.1 → 0.63.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 (46) 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/first-paint-audit.js +4 -2
  7. package/workspace/guide/index.md +6 -3
  8. package/workspace/guide/layout.md +5 -4
  9. package/workspace/guide/notes.md +72 -0
  10. package/workspace/guide/people.md +70 -0
  11. package/workspace/lib/calendar-memory.js +2 -1
  12. package/workspace/lib/jev-notes.js +162 -0
  13. package/workspace/lib/notes-repo.js +133 -0
  14. package/workspace/lib/notes-whisper.js +200 -0
  15. package/workspace/lib/notes.js +511 -0
  16. package/workspace/lib/people-cli.js +135 -0
  17. package/workspace/lib/people-store.js +123 -0
  18. package/workspace/lib/people.js +399 -0
  19. package/workspace/lib/pmos-verbs.js +3 -0
  20. package/workspace/lib/route-modules.js +1 -0
  21. package/workspace/lib/routes-notes.js +230 -0
  22. package/workspace/package-lock.json +1 -1
  23. package/workspace/package.json +1 -1
  24. package/workspace/public/pm/app.js +2 -2
  25. package/workspace/public/pm/components.css +441 -0
  26. package/workspace/public/pm/dest-people.js +385 -0
  27. package/workspace/public/pm/dest-settings.js +44 -3
  28. package/workspace/public/pm/frame-copy.js +7 -0
  29. package/workspace/public/pm/initiative-sheet.js +5 -2
  30. package/workspace/public/pm/initiatives-copy.js +1 -0
  31. package/workspace/public/pm/item-sheet.js +18 -1
  32. package/workspace/public/pm/panel-cockpit.js +3 -0
  33. package/workspace/public/pm/people-link.js +106 -0
  34. package/workspace/public/pm/registry.js +2 -0
  35. package/workspace/public/pm/today-agenda.js +3 -1
  36. package/workspace/public/pm/today-day.js +2 -0
  37. package/workspace/public/pm/topbar.js +4 -3
  38. package/workspace/public/pm/view-notes.js +723 -0
  39. package/workspace/replaced-tests.json +14 -0
  40. package/workspace/server.js +30 -1
  41. package/workspace/skills/gather-requirements.skill +5 -3
  42. package/workspace/skills/jira-sync.skill +5 -4
  43. package/workspace/skills/live-notes.skill +3 -1
  44. package/workspace/skills/morning-brief.skill +5 -3
  45. package/workspace/test-lists/meeting-notes.json +15 -0
  46. package/workspace/test-lists/people-directory.json +6 -0
@@ -0,0 +1,106 @@
1
+ /* pm/people-link.js — the ways into People from other places (initiative people-directory, PRD R12 and R13).
2
+ *
3
+ * peopleOf(ws) → Promise<view | null> the /api/people view, read at most every few seconds per repository
4
+ * namedIn(view, text) → people the people a free-text line names (an Agenda `with`, an item owner)
5
+ * personLink(person) → a button that opens the person's page in People
6
+ * openPerson(id) → opens People on that person
7
+ * forgetPeople(ws) → drop the cached view (People calls it after a press)
8
+ *
9
+ * The matching rule is lib/people.js matchPeople's, in the browser: a person's full name, a first name of three letters
10
+ * or more, or a name, speaker, chat or Jira handle, as a whole word, ignoring case. Two people with one first name both
11
+ * match; each link names the person in full.
12
+ */
13
+
14
+ import { el } from './dom.js';
15
+ import { button, row } from './primitives.js';
16
+ import { pmGet } from './http.js';
17
+ import { openDestination } from './registry.js';
18
+
19
+ const FRESH_MS = 10000;
20
+ const cache = new Map(); // ws -> { at, promise }
21
+
22
+ export function peopleOf(ws) {
23
+ if (!ws) return Promise.resolve(null);
24
+ const c = cache.get(ws);
25
+ if (c && Date.now() - c.at < FRESH_MS) return c.promise;
26
+ const promise = pmGet('/api/people?ws=' + encodeURIComponent(ws)).then((v) => (v && v.ok !== false ? v : null), () => null);
27
+ cache.set(ws, { at: Date.now(), promise });
28
+ return promise;
29
+ }
30
+ export function forgetPeople(ws) { if (ws) cache.delete(ws); else cache.clear(); }
31
+
32
+ const fold = (s) => String(s || '').replace(/\s+/g, ' ').trim();
33
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ function names(hay, needle) {
35
+ const n = fold(needle);
36
+ if (n.length < 2) return false;
37
+ return new RegExp(`(^|[^\\p{L}\\p{N}])${escRe(n)}($|[^\\p{L}\\p{N}])`, 'iu').test(String(hay || ''));
38
+ }
39
+ function words(p) {
40
+ const w = new Set();
41
+ const full = fold(p.name);
42
+ if (full) w.add(full);
43
+ const first = full.split(' ')[0];
44
+ if (first && first.length >= 3 && first !== full) w.add(first);
45
+ for (const h of p.handles || []) if (['name', 'speaker', 'slack', 'jira'].includes(h.kind)) w.add(fold(h.value).replace(/^@/, ''));
46
+ return [...w].filter((x) => x.length >= 2);
47
+ }
48
+ export function namedIn(view, text) {
49
+ if (!view || !text) return [];
50
+ return (view.people || []).filter((p) => words(p).some((w) => names(text, w)));
51
+ }
52
+
53
+ export function openPerson(id) { return openDestination('people', { slug: id }); }
54
+
55
+ export function personLink(p, { size = 'sm' } = {}) {
56
+ return button({ label: p.name, variant: 'ghost', size, iconEnd: 'chevron-right',
57
+ attrs: { 'data-person-link': p.id, 'aria-label': `Open ${p.name} in People` }, onClick: () => openPerson(p.id) });
58
+ }
59
+
60
+ /**
61
+ * A holder that fills with one link per person `text` names, once the repository's people are read. Empty (and hidden
62
+ * by having no children) when nobody matches, so a place with no People records looks as it did before.
63
+ */
64
+ export function peopleLinks(ws, text, attrs) {
65
+ const holder = el('span', { class: 'rp-people-links', 'data-people-links': '', ...(attrs || {}) });
66
+ if (!ws || !text) return holder;
67
+ peopleOf(ws).then((v) => {
68
+ const found = namedIn(v, text);
69
+ if (found.length) holder.replaceChildren(...found.map((p) => personLink(p)));
70
+ });
71
+ return holder;
72
+ }
73
+
74
+ const REL = {
75
+ reading: 'Reading…',
76
+ none: 'Nobody in People owns an item of this initiative or is named in an interaction about it.',
77
+ owns: (n) => `Owns ${n} item${n === 1 ? '' : 's'}`,
78
+ blocked: (n) => `${n} blocked`,
79
+ named: (n) => `Named in ${n} interaction${n === 1 ? '' : 's'}`,
80
+ };
81
+
82
+ /**
83
+ * The initiative sheet's People tab (R13): the people whose interactions name the initiative or who own one of its
84
+ * items, with how they relate. `onOpen` runs before the person opens (the sheet closes itself).
85
+ */
86
+ export function initiativePeople(ws, slug, { onOpen } = {}) {
87
+ const box = el('div', { class: 'rp-people-rel', 'data-people-initiative': slug || '' }, el('p', { class: 'rp-people-hint' }, REL.reading));
88
+ peopleOf(ws).then((v) => {
89
+ const out = [];
90
+ for (const p of (v && v.people) || []) {
91
+ const items = p.shared.items.filter((it) => it.initiative === slug);
92
+ const named = p.shared.initiatives.find((g) => g.id === slug);
93
+ if (!items.length && !named) continue;
94
+ const blocked = items.filter((it) => it.blocked).length;
95
+ const meta = [items.length ? REL.owns(items.length) : null, blocked ? REL.blocked(blocked) : null, named ? REL.named(named.count) : null].filter(Boolean);
96
+ out.push({ p, meta, weight: items.length * 10 + blocked * 5 + (named ? named.count : 0) });
97
+ }
98
+ out.sort((a, b) => b.weight - a.weight || a.p.name.localeCompare(b.p.name));
99
+ box.replaceChildren(out.length
100
+ ? el('div', { role: 'list' }, out.map(({ p, meta }) => el('div', { class: 'rp-people-row', role: 'listitem', 'data-people-rel': p.id },
101
+ row({ title: p.name, meta: [[p.role, p.org].filter(Boolean).join(' · ') || p.group, meta.join(' · ')], openLabel: `Open ${p.name} in People`,
102
+ onOpen: () => { if (onOpen) onOpen(); openPerson(p.id); }, openAttrs: { 'data-person-link': p.id } }))))
103
+ : el('p', { class: 'rp-people-hint' }, REL.none));
104
+ });
105
+ return box;
106
+ }
@@ -50,6 +50,8 @@ export const DESTINATIONS = Object.freeze([
50
50
  Object.freeze({ id: 'reports', label: 'Reports', key: 'g r', module: 'dest-reports.js', run: 5 }),
51
51
  // Not a pm-os-absorb run (run: null): docs-three-way-sync added it — is each linked document the same as its copies.
52
52
  Object.freeze({ id: 'docs', label: 'Docs', key: 'g d', module: 'dest-docs.js', run: null }),
53
+ // Not a pm-os-absorb run (run: null): people-directory added it — the people the PM works with in a repository.
54
+ Object.freeze({ id: 'people', label: 'People', key: 'g p', module: 'dest-people.js', run: null }),
53
55
  Object.freeze({ id: 'settings', label: 'Settings', key: null, module: 'dest-settings.js', run: 3 }),
54
56
  ]);
55
57
  export const EXTENSIONS = Object.freeze([
@@ -13,6 +13,7 @@
13
13
  * Nothing here writes: the calendar memory changes only through the morning-brief session's brief.
14
14
  */
15
15
 
16
+ import { peopleLinks } from './people-link.js';
16
17
  import { el } from './dom.js';
17
18
  import { button, iconButton, badge, tabs } from './primitives.js';
18
19
  import { pmGet } from './http.js';
@@ -292,7 +293,8 @@ export function meetingDetail(ctx, id, h) {
292
293
  const end = (() => { const d = new Date(e.end); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; })();
293
294
  return [
294
295
  h.dhead(A.kindWord[e.kind], e.title),
295
- el('p', { class: 'td-day__dim td-day__mono' }, `${dayLabel(f.day.date)} · ${e.time}–${end}${e.attendees ? ' · ' + A.people(e.attendees) : ''}`),
296
+ el('p', { class: 'td-day__dim td-day__mono' }, `${dayLabel(f.day.date)} · ${e.time}–${end}${e.attendees ? ' · ' + A.people(e.attendees) : ''}${e.with ? ' · ' + e.with : ''}`),
297
+ e.with ? peopleLinks(ctx && ctx.ws, e.with, { 'data-ag': 'people' }) : null,
296
298
  memoryBlock(id),
297
299
  e.prep ? el('div', { class: 'td-day__phase', 'data-ag': 'before' }, el('h4', null, A.before),
298
300
  el('p', null, A.prepLine(e.prep_done || 0, e.prep.length)),
@@ -28,6 +28,7 @@ import { pmGet, pmPost } from './http.js';
28
28
  import { FIXTURE } from './state.js';
29
29
  import { COPY } from './today-copy.js';
30
30
  import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
31
+ import { peopleLinks } from './people-link.js';
31
32
  import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
32
33
 
33
34
  const D = COPY.day;
@@ -538,6 +539,7 @@ function eventDetail(ctx, e) {
538
539
  return [
539
540
  dhead(D.kind.meeting, e.title),
540
541
  el('p', { class: 'td-day__dim td-day__mono' }, `${e.time}${e.end ? '–' + hhmm(e.end) : ''}${e.with ? ' · ' + e.with : ''}${e.ended ? ' · ' + D.ended : ''}`),
542
+ e.with ? peopleLinks(ctx.ws, e.with, { 'data-day': 'people' }) : null,
541
543
  e.purpose ? el('p', null, el('b', null, D.purpose + ': '), e.purpose) : null,
542
544
  e.prep ? el('p', null, el('b', null, D.prep + ': '), e.prep) : null,
543
545
  ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
@@ -8,7 +8,7 @@
8
8
  * runs `git status` against the remote-tracking ref the last fetch left).
9
9
  *
10
10
  * createTopbar({ go, openPalette, repoMenu, sync }) → { node, update(model) }
11
- * go(id) open destination id (today, initiatives, board, reports, docs, settings)
11
+ * go(id) open destination id (today, initiatives, board, reports, docs, people, settings)
12
12
  * openPalette() open ⌘K
13
13
  * repoMenu(anchor) open the Repository menu under `anchor`
14
14
  * sync(repo) run Sync for `repo`; resolves to the server's answer
@@ -23,8 +23,9 @@ import { button, needsDot, needsCount } from './primitives.js';
23
23
  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
- * PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"), so the bar holds 8 controls. */
27
- export const BAR_DESTS = Object.freeze(['today', 'initiatives', 'board', 'reports', 'docs']);
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']);
28
29
 
29
30
  /** Is this sync reading "not in sync"? Behind, ahead, records not yet recorded, or `remote_stale`: no fetch in the last
30
31
  * few minutes, so another machine's push would be invisible here and "in sync" cannot be claimed. An unreadable status