create-pmos 0.89.0 → 0.90.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/guide/people.md +2 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +12 -1
- package/workspace/public/pm/dest-people.js +41 -1
- package/workspace/server.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T17:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T17:45:30Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T17:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T17:45:30Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T17:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T17:45:30Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -37,13 +37,13 @@ A sighting of a new handle adds the person. A sighting with the full name of a p
|
|
|
37
37
|
|
|
38
38
|
## The list
|
|
39
39
|
|
|
40
|
-
People has the same two-card layout as the Agenda. The title says how many people there are. **All**, **Team** and **External** at the top right filter the list. The left card holds the list. Type in **Find a person** to search names, handles and initiatives. **Latest**, **Most** and **Name** sort it by the last interaction, the number of interactions or the name. Each row shows initials, the name, the role and organisation, and the tone as a coloured dot (a ? means you have not confirmed it). Press a person to open their page in the right card. Each card scrolls by itself.
|
|
40
|
+
People has the same two-card layout as the Agenda. The title says how many people there are. **All**, **Team** and **External** at the top right filter the list. The left card holds the list. Type in **Find a person** to search names, handles and initiatives. **Latest**, **Most** and **Name** sort it by the last interaction, the number of interactions or the name. Each row shows initials, the name, the role and organisation, and the tone as a coloured dot (a ? means you have not confirmed it). Press a person to open their page in the right card. Each card scrolls by itself, and the search box keeps its cursor while you type.
|
|
41
41
|
|
|
42
42
|
## A person's page
|
|
43
43
|
|
|
44
44
|
The top of the page shows the person's name and role, a **Confirm** button when a tone is waiting, and a **⋯** menu. Three tiles under it give the number of interactions, the tone and the shared work.
|
|
45
45
|
|
|
46
|
-
- **Interactions.** Newest first. Each line shows the date, the kind (Mail, Chat, Meeting, 1:1, Jira, Doc, Call or Mention), the line, and where it came from with its pointer and tone.
|
|
46
|
+
- **Interactions.** Newest first. A meeting that comes up again is one line that says how often it happens (One-off, Daily, Weekly or Monthly meeting), how many times, and when it was last. A meeting the Agenda marks as key shows a **Key** label. Each other line shows the date, the kind (Mail, Chat, Meeting, 1:1, Jira, Doc, Call or Mention), the line, and where it came from with its pointer and tone.
|
|
47
47
|
- **Tone.** The mix of tones (warm, neutral, cautious, tense) and a proposed overall tone: the most frequent tone of the latest 10 interactions. When two tones are tied, the latest one wins. Press **Confirm** (at the top or here) to keep the proposal, **Set another** to choose a different tone, or **Change** to clear your choice. Tone describes the exchanges, not the person.
|
|
48
48
|
- **Shared work.** The initiatives your interactions name, the plan items the person owns (with **Blocked** when the item is blocked), and the coming meetings that name them when calendar names are on. Nothing here is typed by hand.
|
|
49
49
|
- **Notes.** Type a note and press the **+** button (**Add note**). Press the **×** next to a note to remove it. Only you can add or remove a note. No command writes one.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.90.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -5898,10 +5898,14 @@
|
|
|
5898
5898
|
display: grid;
|
|
5899
5899
|
grid-template-columns: minmax(300px, 2fr) 3fr;
|
|
5900
5900
|
gap: 16px;
|
|
5901
|
-
|
|
5901
|
+
grid-template-rows: minmax(0, 1fr);
|
|
5902
|
+
align-items: stretch;
|
|
5902
5903
|
height: calc(100vh - 220px);
|
|
5903
5904
|
min-height: 420px;
|
|
5904
5905
|
}
|
|
5906
|
+
.rp-people-grid > * {
|
|
5907
|
+
min-height: 0;
|
|
5908
|
+
}
|
|
5905
5909
|
.rp-people-card {
|
|
5906
5910
|
display: flex;
|
|
5907
5911
|
flex-direction: column;
|
|
@@ -5935,6 +5939,8 @@
|
|
|
5935
5939
|
.rp-people-card__list {
|
|
5936
5940
|
display: flex;
|
|
5937
5941
|
flex-direction: column;
|
|
5942
|
+
flex: 1 1 0;
|
|
5943
|
+
min-height: 0;
|
|
5938
5944
|
gap: 2px;
|
|
5939
5945
|
overflow: auto;
|
|
5940
5946
|
min-width: 0;
|
|
@@ -6092,6 +6098,8 @@
|
|
|
6092
6098
|
.rp-people-page__scroll {
|
|
6093
6099
|
display: flex;
|
|
6094
6100
|
flex-direction: column;
|
|
6101
|
+
flex: 1 1 0;
|
|
6102
|
+
min-height: 0;
|
|
6095
6103
|
gap: 14px;
|
|
6096
6104
|
overflow: auto;
|
|
6097
6105
|
min-width: 0;
|
|
@@ -6139,6 +6147,9 @@
|
|
|
6139
6147
|
align-items: flex-start;
|
|
6140
6148
|
padding: 4px 0;
|
|
6141
6149
|
}
|
|
6150
|
+
.rp-people-int__key {
|
|
6151
|
+
margin-left: 8px;
|
|
6152
|
+
}
|
|
6142
6153
|
.rp-people-int__icon {
|
|
6143
6154
|
width: 28px;
|
|
6144
6155
|
height: 28px;
|
|
@@ -245,10 +245,46 @@ function section(title, kids, attrs) {
|
|
|
245
245
|
return el('section', { class: 'rp-people-sec', ...(attrs || {}) }, el('h4', { class: 'rp-people-sec__title' }, title), ...kids);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
const FREQ = { one: 'One-off meeting', daily: 'Daily meeting', weekly: 'Weekly meeting', monthly: 'Monthly meeting' };
|
|
249
|
+
const dayNo = (d) => Math.round(Date.parse(String(d).slice(0, 10) + 'T00:00:00Z') / 86400000);
|
|
250
|
+
/** How often one meeting happens, from the days it was seen: a single day is one-off, a gap of up to 3 days daily, up to 10 weekly, else monthly. */
|
|
251
|
+
function frequencyOf(dates) {
|
|
252
|
+
const days = [...new Set(dates.map(dayNo))].sort((a, b) => a - b);
|
|
253
|
+
if (days.length < 2) return 'one';
|
|
254
|
+
const gaps = days.slice(1).map((d, k) => d - days[k]).sort((a, b) => a - b);
|
|
255
|
+
const mid = gaps[Math.floor(gaps.length / 2)];
|
|
256
|
+
return mid <= 3 ? 'daily' : mid <= 10 ? 'weekly' : 'monthly';
|
|
257
|
+
}
|
|
258
|
+
const meetingKey = (t) => t.toLowerCase().replace(/\d+/g, ' ').replace(/[^a-z\u00c0-\u024f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
259
|
+
/** The same calendar meeting seen again is one entry with its frequency; other interactions stay as they are. */
|
|
260
|
+
function foldInteractions(list) {
|
|
261
|
+
const out = [];
|
|
262
|
+
const groups = new Map();
|
|
263
|
+
for (const i of list) {
|
|
264
|
+
if (i.pointer !== 'calendar' || !/^Meeting: /.test(i.text)) { out.push(i); continue; }
|
|
265
|
+
const k = meetingKey(i.text.slice(9));
|
|
266
|
+
let g = groups.get(k);
|
|
267
|
+
if (!g) { g = { meeting: true, text: i.text.slice(9), source: i.source, dates: [], key: false, first: i }; groups.set(k, g); out.push(g); }
|
|
268
|
+
g.dates.push(i.date);
|
|
269
|
+
if (i.key) g.key = true;
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
248
274
|
function interactionsSec(p) {
|
|
249
275
|
if (!p.interactions.length) return section(COPY.interactions, [el('p', { class: 'rp-people-hint' }, COPY.noInteractions)], { 'data-people-sec': 'interactions' });
|
|
250
276
|
const icons = { mail: 'mail', live: 'message-square', calendar: 'calendar', jira: 'ticket', gather: 'file-text', plan: 'file-text', notes: 'file-text' };
|
|
251
|
-
return section(`${COPY.interactions} · ${p.count}`, [el('div', { class: 'rp-people-ints' }, p.interactions.slice(0, 50).map((i) => {
|
|
277
|
+
return section(`${COPY.interactions} · ${p.count}`, [el('div', { class: 'rp-people-ints' }, foldInteractions(p.interactions.slice(0, 50)).map((i) => {
|
|
278
|
+
if (i.meeting) {
|
|
279
|
+
const f = frequencyOf(i.dates);
|
|
280
|
+
const latest = i.dates.slice().sort().pop();
|
|
281
|
+
return el('div', { class: 'rp-people-int', 'data-people-int': latest, 'data-people-meeting': f },
|
|
282
|
+
el('span', { class: 'rp-people-int__icon' }, icon('calendar')),
|
|
283
|
+
el('div', {},
|
|
284
|
+
el('div', { class: 'rp-people-int__text' }, i.text, i.key ? el('span', { class: 'rp-people-int__key', 'data-people-key-meeting': '' }, badge({ text: 'Key', tone: 'outline' })) : null),
|
|
285
|
+
el('small', {}, `${FREQ[f]}${i.dates.length > 1 ? ` · ${i.dates.length} times` : ''} · last ${day(latest)}`),
|
|
286
|
+
el('small', { class: 'rp-people-int__src' }, SOURCE.calendar)));
|
|
287
|
+
}
|
|
252
288
|
const iconName = icons[i.source] || 'file-text';
|
|
253
289
|
return el('div', { class: 'rp-people-int', 'data-people-int': i.date },
|
|
254
290
|
el('span', { class: 'rp-people-int__icon' }, icon(iconName)),
|
|
@@ -403,7 +439,11 @@ function draw(inst) {
|
|
|
403
439
|
if (typing) { inst.last = ''; return; }
|
|
404
440
|
const n = inst.view.people.length;
|
|
405
441
|
inst.titleEl.textContent = n ? `${n} ${n === 1 ? 'person' : 'people'}` : COPY.title;
|
|
442
|
+
const find = inst.head.find.control;
|
|
443
|
+
const hadFocus = document.activeElement === find;
|
|
444
|
+
const at = hadFocus ? [find.selectionStart, find.selectionEnd] : null;
|
|
406
445
|
const rev = review(inst);
|
|
407
446
|
const grid = el('div', { class: 'rp-people-grid' }, listPane(inst), personPane(inst));
|
|
408
447
|
inst.body.replaceChildren(...[rev, grid].filter(Boolean));
|
|
448
|
+
if (hadFocus) { find.focus({ preventScroll: true }); try { find.setSelectionRange(at[0], at[1]); } catch { /* not a text field */ } }
|
|
409
449
|
}
|
package/workspace/server.js
CHANGED
|
@@ -6849,6 +6849,19 @@ async function handleRequest(req, res) {
|
|
|
6849
6849
|
const meetings = calendar.filter((e) => e && e.start && Date.parse(e.end || e.start) >= now);
|
|
6850
6850
|
const v = peopleStore.view(root, { now, meetings });
|
|
6851
6851
|
if (!v.ok) return json(res, v, 500);
|
|
6852
|
+
// A meeting the Agenda marks as key (its own rule, or the PM's mark) is flagged on the person's calendar interactions.
|
|
6853
|
+
const marks = readCalMarks();
|
|
6854
|
+
const keyTitles = new Set();
|
|
6855
|
+
for (const e of calendar) {
|
|
6856
|
+
if (!e || !e.title) continue;
|
|
6857
|
+
const mk = calMemLib.markOf(marks, e);
|
|
6858
|
+
if (mk ? mk.on : calMemLib.isImportant(e)) keyTitles.add(calMemLib.normTitle(e.title));
|
|
6859
|
+
}
|
|
6860
|
+
for (const person of v.people || []) {
|
|
6861
|
+
for (const i of person.interactions || []) {
|
|
6862
|
+
if (i.pointer === 'calendar' && /^Meeting: /.test(i.text)) i.key = keyTitles.has(calMemLib.normTitle(i.text.slice(9)));
|
|
6863
|
+
}
|
|
6864
|
+
}
|
|
6852
6865
|
return json(res, { ...v, ws: w ? w.id : null });
|
|
6853
6866
|
}
|
|
6854
6867
|
if (p === '/api/people/decide' && req.method === 'POST') {
|