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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/first-paint-audit.js +4 -2
- package/workspace/guide/index.md +6 -3
- package/workspace/guide/layout.md +5 -4
- package/workspace/guide/notes.md +72 -0
- package/workspace/guide/people.md +70 -0
- package/workspace/lib/calendar-memory.js +2 -1
- package/workspace/lib/jev-notes.js +162 -0
- package/workspace/lib/notes-repo.js +133 -0
- package/workspace/lib/notes-whisper.js +200 -0
- package/workspace/lib/notes.js +511 -0
- package/workspace/lib/people-cli.js +135 -0
- package/workspace/lib/people-store.js +123 -0
- package/workspace/lib/people.js +399 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-notes.js +230 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -2
- package/workspace/public/pm/components.css +441 -0
- package/workspace/public/pm/dest-people.js +385 -0
- package/workspace/public/pm/dest-settings.js +44 -3
- package/workspace/public/pm/frame-copy.js +7 -0
- package/workspace/public/pm/initiative-sheet.js +5 -2
- package/workspace/public/pm/initiatives-copy.js +1 -0
- package/workspace/public/pm/item-sheet.js +18 -1
- package/workspace/public/pm/panel-cockpit.js +3 -0
- package/workspace/public/pm/people-link.js +106 -0
- package/workspace/public/pm/registry.js +2 -0
- package/workspace/public/pm/today-agenda.js +3 -1
- package/workspace/public/pm/today-day.js +2 -0
- package/workspace/public/pm/topbar.js +4 -3
- package/workspace/public/pm/view-notes.js +723 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/server.js +30 -1
- package/workspace/skills/gather-requirements.skill +5 -3
- package/workspace/skills/jira-sync.skill +5 -4
- package/workspace/skills/live-notes.skill +3 -1
- package/workspace/skills/morning-brief.skill +5 -3
- package/workspace/test-lists/meeting-notes.json +15 -0
- 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")
|
|
27
|
-
|
|
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
|