create-pmos 0.61.0 → 0.62.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 +8 -3
- package/workspace/guide/index.md +5 -3
- package/workspace/guide/layout.md +4 -3
- package/workspace/guide/people.md +70 -0
- package/workspace/guide/today.md +2 -2
- package/workspace/lib/calendar-memory.js +2 -1
- 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/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 +169 -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/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 +6 -3
- package/workspace/public/pm/topbar.js +4 -3
- package/workspace/server.js +27 -0
- 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/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;
|
|
@@ -42,7 +43,8 @@ const store = {
|
|
|
42
43
|
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
|
|
43
44
|
set(k, v) { try { localStorage.setItem(k, v); } catch { /* private window: the default applies */ } },
|
|
44
45
|
};
|
|
45
|
-
|
|
46
|
+
// A new key: a fold stored by the old Today page must not hide the Agenda month.
|
|
47
|
+
const OPEN_KEY = 'pmos.agenda.day.open';
|
|
46
48
|
const SEEN_KEY = 'pmos.today.day.seen';
|
|
47
49
|
let seen = new Set();
|
|
48
50
|
try { seen = new Set(JSON.parse(store.get(SEEN_KEY) || '[]')); } catch { seen = new Set(); }
|
|
@@ -537,6 +539,7 @@ function eventDetail(ctx, e) {
|
|
|
537
539
|
return [
|
|
538
540
|
dhead(D.kind.meeting, e.title),
|
|
539
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,
|
|
540
543
|
e.purpose ? el('p', null, el('b', null, D.purpose + ': '), e.purpose) : null,
|
|
541
544
|
e.prep ? el('p', null, el('b', null, D.prep + ': '), e.prep) : null,
|
|
542
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),
|
|
@@ -836,7 +839,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
836
839
|
ensure(ws);
|
|
837
840
|
const c = cache.get(ws) || {};
|
|
838
841
|
const m = model(c.data, { here: here || null, all: !!all });
|
|
839
|
-
const open = forced !== null ? forced : ui.open !== null ? ui.open :
|
|
842
|
+
const open = forced !== null ? forced : ui.open !== null ? ui.open : true;
|
|
840
843
|
if (!open) {
|
|
841
844
|
return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'false', 'aria-label': D.title },
|
|
842
845
|
button({ label: D.toggle(m.needs), variant: 'ghost', iconEnd: 'chevron-right', attrs: { 'data-day': 'toggle', 'aria-expanded': 'false' }, onClick: () => fold(true) }));
|
|
@@ -863,7 +866,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
863
866
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
864
867
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
865
868
|
el('div', { class: 'td-day__cols' },
|
|
866
|
-
el('div', { class: 'td-day__left' },
|
|
869
|
+
el('div', { class: 'td-day__left' }, agenda(ctx), summaryPanel(ctx), goalsPanel(ctx), jobsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
|
|
867
870
|
detail(ctx)));
|
|
868
871
|
}
|
|
869
872
|
|
|
@@ -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
|
package/workspace/server.js
CHANGED
|
@@ -131,6 +131,7 @@ const focus = require('./lib/focus'); // buildFocus(input) →
|
|
|
131
131
|
const handoff = require('./lib/handoff'); // readLinks(parsedFrontmatterObject) — never a path
|
|
132
132
|
const fourPanel = require('./lib/four-panel'); // GET /api/initiative — the one-initiative join + the declaration stores
|
|
133
133
|
const docSyncStore = require('./lib/doc-sync-store');
|
|
134
|
+
const peopleStore = require('./lib/people-store'); // people-directory (D90) — people.jsonl, folded on read
|
|
134
135
|
// The session each repository's Check now started (workspace id → session id), so a second press while it runs refuses.
|
|
135
136
|
const docCheckSessions = new Map();
|
|
136
137
|
// The session each Use … version press started (workspace id|path|kind → { session, use }), so the line says "Updating…"
|
|
@@ -6621,6 +6622,32 @@ async function handleRequest(req, res) {
|
|
|
6621
6622
|
if (started) docCheckSessions.set(w.id, s.id);
|
|
6622
6623
|
return json(res, { ok: started, session: s.id, error: started ? undefined : (s.note || 'could not start the session') }, started ? 200 : 500);
|
|
6623
6624
|
}
|
|
6625
|
+
// ── People: the people the PM works with in a repository (people-directory, PRD R9 to R14, proposed D90) ──────────
|
|
6626
|
+
// GET folds people.jsonl with the plan items and the coming meetings this machine remembers (the calendar memory and
|
|
6627
|
+
// today's brief). POST is one PM page press: add, ignore, merge, keep separate, edit, tone, a note, or the calendar
|
|
6628
|
+
// setting. Agent sessions record sightings through `pmos people record`; no route here reads a source.
|
|
6629
|
+
if (p === '/api/people' && req.method === 'GET') {
|
|
6630
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
6631
|
+
const root = w ? w.root : SELF_ROOT;
|
|
6632
|
+
const now = Date.now();
|
|
6633
|
+
const mem = readCalMemory();
|
|
6634
|
+
const brief = briefOfDay(briefLib.localDate(now));
|
|
6635
|
+
const meetings = [...((mem && mem.events) || []), ...((brief && brief.calendar) || [])]
|
|
6636
|
+
.filter((e) => e && e.start && Date.parse(e.end || e.start) >= now);
|
|
6637
|
+
const v = peopleStore.view(root, { now, meetings });
|
|
6638
|
+
if (!v.ok) return json(res, v, 500);
|
|
6639
|
+
return json(res, { ...v, ws: w ? w.id : null });
|
|
6640
|
+
}
|
|
6641
|
+
if (p === '/api/people/decide' && req.method === 'POST') {
|
|
6642
|
+
let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6643
|
+
const g = pmGuard.requirePagePress(req, body);
|
|
6644
|
+
if (!g.ok) return json(res, { ok: false, error: g.error }, g.status);
|
|
6645
|
+
const w = body.ws ? workspaces.get(String(body.ws)) : getWs(null);
|
|
6646
|
+
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 404);
|
|
6647
|
+
const { ws: _ws, ...press } = body;
|
|
6648
|
+
const r = peopleStore.decide(w.root, { actor: g.actor, body: press });
|
|
6649
|
+
return json(res, r, r.ok ? 200 : 400);
|
|
6650
|
+
}
|
|
6624
6651
|
// ── Jira tickets: the initiative sheet's Jira tickets tab (jira-wbs, PRD J3 to J12, proposed D88) ─────────────────
|
|
6625
6652
|
// GET reads the ticket draft, the PRD it came from and the last status snapshot, and decides every flag in
|
|
6626
6653
|
// lib/jira-tickets.js. The POSTs write the draft file (draft, edit, epic, prd), or start ONE agent session: Push starts
|
|
@@ -4,10 +4,10 @@ label: "Gather requirements"
|
|
|
4
4
|
description: "Read what changed in the PM's mail, chat, Jira, Confluence, calendar and meeting notes since the last note, and file one dated note of requirements — each with its source, how sure it is, and which initiative it touches."
|
|
5
5
|
run: agent
|
|
6
6
|
agent: claude
|
|
7
|
-
version: 1.
|
|
7
|
+
version: 1.2.0
|
|
8
8
|
owner: wawan
|
|
9
9
|
risk: low
|
|
10
|
-
scope: read:connectors, read:state, write:state/requirements
|
|
10
|
+
scope: read:connectors, read:state, write:state/requirements, write:state/people
|
|
11
11
|
output:
|
|
12
12
|
- New or changed requirements
|
|
13
13
|
- Validations and contradictions
|
|
@@ -55,7 +55,9 @@ Procedure (each step ends with a check that shows it is done):
|
|
|
55
55
|
Done when: the file starts with this frontmatter and holds the four sections in order.
|
|
56
56
|
7. Under "Sources reached and not reached", list every source kind you tried, with `reached` or `not reached — <why>`, and the number of private items skipped.
|
|
57
57
|
Done when: every source kind you tried has a line, and the private count is stated.
|
|
58
|
-
8.
|
|
58
|
+
8. Record the people in People. For each author of an item you filed (other than the PM), write one sighting: {"source": "gather", "handle": {"kind": "mail" | "slack" | "jira" | "confluence", "value": "<their address or name in that source>"}, "name": "<display name>", "role"?: "<their role, when the source states it>", "org"?: "<their organisation, when it is not the PM's>", "external"?: true (when they are outside the PM's company), "interaction": {"date": "YYYY-MM-DD", "kind": "Mail" | "Chat" | "Jira" | "Doc" | "Meeting", "text": "<what they asked or said, in your words, at most 200 characters>", "pointer": "<the item's pointer>", "tone": "warm|neutral|cautious|tense" (only when the exchange shows one), "initiative": "<the item's initiative id, or omit when unmatched>"}}. Tone describes the exchange, not the person. When two handles look like one person (the same name in mail and in chat), add a merge {"a": <handle>, "b": <handle>, "why": "<one line>"}. Put them in one file {"sightings": [...], "merges": [...]} in the scratch folder and run `pmos people record --file <that file>` once. Contact details beyond the handle are never recorded.
|
|
59
|
+
Done when: the command printed how many sightings it recorded, or you said no item had an author.
|
|
60
|
+
9. Finish by saying in one line how many items you filed and where, and how many people you recorded. Do not commit; the PM's Sync carries the file.
|
|
59
61
|
Done when: the count and the path are said once, and no commit was made.
|
|
60
62
|
|
|
61
63
|
Output contract — produce EXACTLY these sections, the same shape every run:
|
|
@@ -14,8 +14,8 @@ output:
|
|
|
14
14
|
|
|
15
15
|
Rules that always hold:
|
|
16
16
|
1. READ-ONLY on Jira. Use only the Jira search tool. 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 jira sync
|
|
18
|
-
3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the sprint field, subtasks and the
|
|
17
|
+
2. THE ONLY WRITE is `pmos jira sync`, and `pmos people record` once at the end (step 5). Never edit a repository file. Never run `pmos item`. Never write a ledger by hand. Never commit; the PM's Sync carries the files.
|
|
18
|
+
3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the sprint field, subtasks, the Flagged field, and the display names of the assignee and the reporter. Never ask for or copy a description, a comment, an attachment, an email address, any other detail of a person, or a token.
|
|
19
19
|
4. TEMPORARY FILES OUTSIDE THE REPOSITORY. Save each search result in this session's scratch folder, never in the repository, and delete it after the sync.
|
|
20
20
|
5. NOTHING INVENTED. Give `pmos jira sync` exactly what Jira returned. If Jira cannot be reached, list the initiative under "Not reached" and write nothing for it.
|
|
21
21
|
|
|
@@ -31,8 +31,9 @@ Procedure:
|
|
|
31
31
|
- `url`: the issue's browse link, `https://<site>.atlassian.net/browse/<KEY>`.
|
|
32
32
|
The shape Jira returned (`{"issues": [{"key", "self", "fields": {...}}]}`) is also accepted.
|
|
33
33
|
4. Run `pmos jira sync <initiative> --from <the file> --query "<the JQL>"`. Then delete the file.
|
|
34
|
-
5.
|
|
35
|
-
|
|
34
|
+
5. Record the people in People. For each assignee and reporter other than the PM, write one sighting per issue: {"source": "jira", "handle": {"kind": "jira", "value": "<their Jira display name>"}, "name": "<display name>", "interaction": {"date": "<the issue's last update, YYYY-MM-DD>", "kind": "Jira", "text": "<Assignee|Reporter> of <KEY>: <summary, at most 150 characters>", "pointer": "jira · <KEY> · <url>", "initiative": "<the initiative id>"}}. Put all of them in one file {"sightings": [...]} in the scratch folder, run `pmos people record --file <that file>` once, and delete the file. An issue with no assignee gives no assignee sighting.
|
|
35
|
+
6. Finish with the three sections of the output contract, one short line each:
|
|
36
|
+
- Checked: the initiative, the number of tickets read, and each change (status, sprint, blocked). If nothing changed, say "no change". Add how many people you recorded.
|
|
36
37
|
- Not reached: each initiative whose search failed, with the reason.
|
|
37
38
|
- Needs you: tickets newly blocked, tickets the search did not return, and any refusal.
|
|
38
39
|
|
|
@@ -48,7 +48,9 @@ Procedure (each step ends with a check that shows it is done):
|
|
|
48
48
|
Done when: you have shown the list and recorded nothing the PM did not say.
|
|
49
49
|
6. When the PM says the conversation is over, run `node workspace/bin/live-notes.js close --repo . --session <id>`. It appends a section to today's requirements note and deletes the raw transcript.
|
|
50
50
|
Done when: the command printed the note path and "transcript: deleted", or the retention date the PM set.
|
|
51
|
-
7.
|
|
51
|
+
7. Record the speakers in People. For each named speaker other than the PM, write one sighting: {"source": "live", "handle": {"kind": "speaker", "value": "<the speaker's name as the transcript gives it>"}, "name": "<display name>", "interaction": {"date": "YYYY-MM-DD", "kind": "Meeting" (or "1:1" when only the PM and this speaker talk), "text": "<what this speaker raised, in your words, at most 200 characters>", "pointer": "<wispr:meeting id, or the segments file> · <date>", "tone": "warm|neutral|cautious|tense" (only when the exchange shows one), "initiative": "<the initiative id their points were placed on, or omit>"}}. Tone describes the exchange, not the person. Never copy what the speaker said. Put the sightings in one file {"sightings": [...]} in the scratch folder and run `pmos people record --file <that file>` once. A speaker with no name ("Speaker 2") is not recorded.
|
|
52
|
+
Done when: the command printed how many sightings it recorded, or you said no speaker was named.
|
|
53
|
+
8. Finish by saying in two lines how many points were placed, how many are still unanswered, which path ran, how many people you recorded, and which sources were not reached. Do not commit; the PM's Sync carries the files.
|
|
52
54
|
Done when: the two lines are said once and no commit was made.
|
|
53
55
|
|
|
54
56
|
Output contract — produce EXACTLY these sections in your final message:
|
|
@@ -20,7 +20,7 @@ Rules that always hold:
|
|
|
20
20
|
2. SETTINGS DECIDE THE SOURCES AND THE SCOPES. Run `pmos proactive settings`. Read a source only if its line under "brief sources" says on. A source that is off is reported as "off". Mail is off unless the PM turned it on. The same command lists the websites and, for Slack, Jira and Confluence, the scope of each channel, project and space (see step 2).
|
|
21
21
|
3. LIGHT ON CONTENT. Never copy a message body, an attachment, a token or a credential. Write what a message says in your own words, in the limits of the field. Skip anything marked private, personal, confidential or HR. At most one short evidence line per item.
|
|
22
22
|
4. NOTHING INVENTED. A source you cannot reach in this session is reported as "not-reached". If nothing is waiting, submit a brief with no items. An honest short brief is correct.
|
|
23
|
-
5. THE ONLY WRITE is `pmos proactive brief submit`. Do not edit repository files and do not commit.
|
|
23
|
+
5. THE ONLY WRITE is `pmos proactive brief submit`, with one exception: when `pmos people settings` prints `calendar names: on`, step 6 records who attends the PM's meetings with `pmos people record`. When it prints `calendar names: off`, record nobody from the calendar and leave "with" out of the memory events. Do not edit repository files and do not commit.
|
|
24
24
|
6. TYPED SCORING only if `pmos proactive settings` says "scoring on" under the brief line. Then use the Typesafe skill for confidence scores on titles and metadata only; the text you send leaves this machine. Otherwise score yourself. Set "judgment": {"used": true|false} to say which.
|
|
25
25
|
7. AT MOST 30 ITEMS. If you have more, keep the 30 that matter most today and drop the rest. The workspace refuses a longer brief.
|
|
26
26
|
|
|
@@ -28,6 +28,7 @@ Procedure:
|
|
|
28
28
|
1. Run `pmos proactive settings` and `pmos proactive today`. The second lists what the workspace derived from the ledgers. Each of its items may become a priority with source "derived".
|
|
29
29
|
Then run `pmos proactive goals`. It lists the PM's goals for today, each with its id (g-xxxxxxxx), its status and, if set, an initiative. The goals are what the PM wants done today: plan for them first.
|
|
30
30
|
Then run `pmos proactive calendar`. Its "read next" line names the window of the calendar memory to read in step 2.
|
|
31
|
+
Then run `pmos people settings`. It prints `calendar names: on` or `calendar names: off` for this repository (see rule 5).
|
|
31
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.
|
|
32
33
|
2. Read the sources that are on and reachable.
|
|
33
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.
|
|
@@ -74,14 +75,15 @@ Procedure:
|
|
|
74
75
|
"digests": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…", "decisions": ["…"], "open": ["…"], "goals": ["…"]}],
|
|
75
76
|
"places": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…"}],
|
|
76
77
|
"memory": {"from": "YYYY-MM-DD", "to": "YYYY-MM-DD",
|
|
77
|
-
"events": [{"start": "<ISO>", "end": "<ISO>", "title": "…", "series": "<recurring id or omit>", "attendees": 2, "external": false, "exec": false, "role": "present|decide|attend",
|
|
78
|
+
"events": [{"start": "<ISO>", "end": "<ISO>", "title": "…", "series": "<recurring id or omit>", "attendees": 2, "external": false, "exec": false, "role": "present|decide|attend", "with": "<attendee names, only with calendar names on>",
|
|
78
79
|
"notes": "<ended only>", "open": ["…"], "prep": ["<coming only>"], "prep_done": 0}]},
|
|
79
80
|
"judgment": {"used": false}}
|
|
80
81
|
```
|
|
81
82
|
"memory" is optional: give it only when calendar is "read". Its "from" and "to" are the window you read, and every event's start is inside it. The workspace replaces the days of that window in what it remembers.
|
|
82
83
|
"places" holds at most 200 entries, each a name of at most 120 characters, only for a source that is "read". A digest has at most 5 decisions, 5 open questions and 5 goal lines, each at most 140 characters, and at most 30 digests. Write a digest only for a place with scope watch, and only for a source that is "read". Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived"). Only use a goal id that `pmos proactive goals` listed today: the workspace refuses any other. Leave out a field you cannot fill: an empty string is worse than no field.
|
|
83
84
|
5. Run `pmos proactive brief submit --file <that file>`. If it is refused, read the reason, fix the file and submit once more. Do not work around a refusal.
|
|
84
|
-
6.
|
|
85
|
+
6. RECORD THE PEOPLE, only when `pmos people settings` printed `calendar names: on` and calendar is "read". For each attendee of today's meetings other than the PM, write one sighting: {"source": "calendar", "handle": {"kind": "mail", "value": "<their address>"} (or {"kind": "name", "value": "<their name>"} when there is no address), "name": "<display name>", "external": true when they are outside the PM's company, "interaction": {"date": "YYYY-MM-DD", "kind": "Meeting" (or "1:1" for two people), "text": "<the meeting title, at most 200 characters>", "pointer": "calendar · <event id or link> · <date>"}}. Skip meetings marked private, personal, confidential or HR. Put the sightings in one file {"sightings": [...]} in the scratch folder and run `pmos people record --file <that file>` once. With `calendar names: off`, skip this step and say "people: off".
|
|
86
|
+
7. Finish by saying in three lines how many goals have a step and which have none, how many priorities and worth-asking items you submitted, and which sources were not reached. Add how many people you recorded, or "people: off".
|
|
85
87
|
|
|
86
88
|
Output contract — produce EXACTLY these sections in your final message:
|
|
87
89
|
- Goals
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "people-directory: `pmos people` and the People place — people recorded by agent sessions, added, merged and toned by the PM.",
|
|
3
|
+
"modules": ["people.test.js"],
|
|
4
|
+
"panels": ["pm-people-test.js"],
|
|
5
|
+
"seconds": { "people.test.js": 4, "pm-people-test.js": 45 }
|
|
6
|
+
}
|