create-pmos 0.141.0 → 0.142.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/my-work.md +13 -3
- package/workspace/lib/routes-pm-items.js +35 -4
- package/workspace/lib/work-assigned.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/pm-work-view.js +11 -1
- package/workspace/server.js +3 -1
- package/workspace/test-files.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.142.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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:34:35Z"}
|
|
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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:34:35Z"}
|
|
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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:34:35Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "My work: every Jira ticket and WBS item assigned to you, ranked, briefed to an agent, approved in Agenda"
|
|
4
|
-
timestamp: 2026-10-
|
|
4
|
+
timestamp: 2026-10-12
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# My work
|
|
@@ -69,9 +69,19 @@ Agenda shows one card, **N items need your brief**, when items wait in To brief.
|
|
|
69
69
|
|
|
70
70
|
None of these briefs an agent or takes an item. Do that in My work.
|
|
71
71
|
|
|
72
|
-
## The Work page: your
|
|
72
|
+
## The Work page: your work in six views
|
|
73
73
|
|
|
74
|
-
The Work page holds
|
|
74
|
+
The Work page holds all the work that is yours:
|
|
75
|
+
|
|
76
|
+
- the items you add for yourself, such as "send Dana the new date";
|
|
77
|
+
- every open Jira ticket and WBS item assigned to you, from the same read as My work;
|
|
78
|
+
- the items today's morning brief suggests for you. They are flagged, so they are in Today.
|
|
79
|
+
|
|
80
|
+
A row of assigned work shows its Jira key (a link to the ticket) or **WBS** and the repository, and the agent's suggestion when the read has one. A suggested row shows **Suggested** and the reason. An agent at work on a ticket puts it in Waiting; a ticket in progress, or with a draft ready for you, is flagged. A ticket that is done in Jira leaves the page.
|
|
81
|
+
|
|
82
|
+
Every action works on these rows. The first action writes the item to your own list, with its source. **Drop** takes it off your Work page only; Jira and the Board keep it, and it does not come back.
|
|
83
|
+
|
|
84
|
+
The server decides where each item shows, so one item is never in two views.
|
|
75
85
|
|
|
76
86
|
The six views follow the OmniFocus style. The **Work** tab badge counts Inbox plus Today.
|
|
77
87
|
|
|
@@ -47,6 +47,7 @@ const jobs = require('./jobs-to-be-done.js');
|
|
|
47
47
|
const workFocus = require('./work-focus.js');
|
|
48
48
|
const workEvidence = require('./work-evidence.js');
|
|
49
49
|
const steps = require('./item-steps.js');
|
|
50
|
+
const assigned = require('./work-assigned.js');
|
|
50
51
|
const { repoScript } = require('./repo-script.js');
|
|
51
52
|
|
|
52
53
|
const FIELDS = ['title', 'due', 'priority', 'waiting_on', 'job', 'defer', 'flagged', 'resolves', 'kind', 'person', 'evidence', 'outcome'];
|
|
@@ -77,10 +78,13 @@ function resolvesTitles(items, ctx, url) {
|
|
|
77
78
|
} catch { return {}; }
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function view(itemLines, jobLines, now, stepLines = [], focusLines = [], proof = null) {
|
|
81
|
+
function view(itemLines, jobLines, now, stepLines = [], focusLines = [], proof = null, extra = []) {
|
|
81
82
|
const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
82
83
|
const byItem = steps.foldSteps(stepLines).byItem;
|
|
83
|
-
|
|
84
|
+
// the work assigned to the PM and what the morning brief suggests (work-shows-assigned): the ledger's copy when it holds one, else the source's
|
|
85
|
+
const byId = new Map((extra || []).map((v) => [v.id, v]));
|
|
86
|
+
const folded = pmItems.fold(itemLines).map((i) => ({ ...assigned.overlay(i, byId), steps: byItem[i.id] || [] }))
|
|
87
|
+
.concat(assigned.addable(extra, itemLines).map((v) => ({ ...v, steps: [] })));
|
|
84
88
|
const items = proof ? workEvidence.withEvidence(folded, { ...proof, now: now.getTime() }) : folded;
|
|
85
89
|
const currentJobs = jobs.foldJobs(jobLines).map((j) => ({ ...j, ...jobs.progress(j.id, items) }));
|
|
86
90
|
const lists = Object.fromEntries(pmItems.LISTS.map((l) => [l, []]));
|
|
@@ -106,6 +110,28 @@ function proofSources(ctx, itemLines) {
|
|
|
106
110
|
return { events, notes };
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
// The work the PM did not type in (work-shows-assigned): open my-work items (Jira, WBS) and today's brief items for the PM.
|
|
114
|
+
// Best effort: a source that cannot be read adds nothing. `myWork` is GET /api/my-work's answer, read by the caller.
|
|
115
|
+
function readBrief(ctx, now) {
|
|
116
|
+
if (!ctx.runtimeDir) return null;
|
|
117
|
+
try { return JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', require('./brief.js').localDate(now.getTime()) + '.json'), 'utf8')); } catch { return null; }
|
|
118
|
+
}
|
|
119
|
+
function assignedFrom(myWork, brief, now) {
|
|
120
|
+
const todayIso = require('./brief.js').localDate(now.getTime());
|
|
121
|
+
return [...assigned.fromMyWork(myWork && myWork.ok ? myWork.items : []), ...assigned.fromBrief(brief, todayIso)];
|
|
122
|
+
}
|
|
123
|
+
async function assignedAsync(ctx, now) {
|
|
124
|
+
let mw = null;
|
|
125
|
+
try { mw = await require('./routes-my-work.js').listForAsync(ctx, now.getTime()); } catch { mw = null; }
|
|
126
|
+
return assignedFrom(mw, readBrief(ctx, now), now);
|
|
127
|
+
}
|
|
128
|
+
function assignedSync(ctx, now) {
|
|
129
|
+
let mw = null;
|
|
130
|
+
try { mw = require('./routes-my-work.js').listFor(ctx, now.getTime()); } catch { mw = null; }
|
|
131
|
+
return assignedFrom(mw, readBrief(ctx, now), now);
|
|
132
|
+
}
|
|
133
|
+
const VIRTUAL_ID = /^(jira:|wbs:|brief:)/;
|
|
134
|
+
|
|
109
135
|
// Each operation returns { status, body, events } and writes nothing itself; the caller appends `events`.
|
|
110
136
|
function checkFields(fields, allowed) {
|
|
111
137
|
if (!fields || typeof fields !== 'object') return 'fields must be an object';
|
|
@@ -307,12 +333,12 @@ function prepare(ctx, now = new Date()) {
|
|
|
307
333
|
|
|
308
334
|
module.exports = {
|
|
309
335
|
prefixes: ['/api/pm-items', '/api/pm-jobs'],
|
|
310
|
-
view, resolvesTitles, flagRoute, deferItem, somedayItem, waitingItem, notYetItem, keepItem, focusEvent, doneItem, reopenItem, parkItem, unparkItem, moveItem, addStepRoute, stepWho, planSteps, addItem, editItem, removeItem, undoItem, addJob, editJob, deleteJob, checkFields, ROUTES, prepare,
|
|
336
|
+
view, resolvesTitles, assignedFrom, flagRoute, deferItem, somedayItem, waitingItem, notYetItem, keepItem, focusEvent, doneItem, reopenItem, parkItem, unparkItem, moveItem, addStepRoute, stepWho, planSteps, addItem, editItem, removeItem, undoItem, addJob, editJob, deleteJob, checkFields, ROUTES, prepare,
|
|
311
337
|
handle: async (req, res, url, ctx) => {
|
|
312
338
|
const p = url.pathname;
|
|
313
339
|
if (p === '/api/pm-items' && req.method === 'GET') {
|
|
314
340
|
const c = prepare(ctx);
|
|
315
|
-
const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines, proofSources(ctx, c.itemLines));
|
|
341
|
+
const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines, proofSources(ctx, c.itemLines), await assignedAsync(ctx, c.now));
|
|
316
342
|
v.resolves_titles = resolvesTitles(v.items, ctx, url);
|
|
317
343
|
ctx.json(res, v);
|
|
318
344
|
return true;
|
|
@@ -325,6 +351,11 @@ module.exports = {
|
|
|
325
351
|
const g = guard.requirePagePress(req, body);
|
|
326
352
|
if (!g.ok) { ctx.json(res, { ok: false, error: g.error }, g.status); return true; }
|
|
327
353
|
const c = prepare(ctx);
|
|
354
|
+
// the first press on assigned or suggested work puts it in the PM's ledger, with its source, before the press itself
|
|
355
|
+
if (typeof body.id === 'string' && VIRTUAL_ID.test(body.id) && !findItem(c.itemLines, body.id)) {
|
|
356
|
+
const v = assigned.addable(assignedSync(ctx, c.now), c.itemLines).find((x) => x.id === body.id);
|
|
357
|
+
if (v) { const ev = assigned.createdEvent(v); pmItems.appendEvent(itemsFile(ctx), ev, c.now); c.itemLines.push({ ...ev, at: c.now.toISOString() }); }
|
|
358
|
+
}
|
|
328
359
|
const r = ROUTES[key](body, c);
|
|
329
360
|
if (r.status === 200 && r.events) {
|
|
330
361
|
for (const ev of r.events) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* lib/work-assigned.js — the work the Work page shows that the PM did not type in (initiative work-shows-assigned).
|
|
3
|
+
*
|
|
4
|
+
* fromMyWork(items) → [item] open my-work items (Jira tickets, WBS items owned by `you`) as Work items
|
|
5
|
+
* fromBrief(brief, todayIso) → [item] today's brief items for the PM (owner pm, not worth_asking), flagged for Today
|
|
6
|
+
* addable(virtuals, itemLines) → [item] the ones the PM's own ledger has never held (not added, not dropped)
|
|
7
|
+
* createdEvent(v) → event the `created` line that puts one in the PM's ledger on its first press
|
|
8
|
+
* overlay(item, byId) → item a ledger copy with the source's current status, suggestion and state
|
|
9
|
+
*
|
|
10
|
+
* PURE: no file, process or clock. A virtual item has the Work item's fields (title, due, priority, waiting_on, flagged) and
|
|
11
|
+
* says where it came from: origin jira | wbs | brief, plus key, url, status, suggest, why, my_work_state, repo. Its `source`
|
|
12
|
+
* is its origin, so a Drop is a drop (pm-items.js removalEvent), never a delete. */
|
|
13
|
+
|
|
14
|
+
const TIER_PRIORITY = { P1: 'P1', P2: 'P2', P3: 'P3' };
|
|
15
|
+
const AGENT = 'an agent';
|
|
16
|
+
const SHOWN = ['origin', 'key', 'url', 'status', 'suggest', 'why', 'my_work_state', 'repo', 'initiative'];
|
|
17
|
+
|
|
18
|
+
function fromMyWork(items) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const x of items || []) {
|
|
21
|
+
if (!x || !x.id || !x.title || x.state === 'done' || x.category === 'done') continue;
|
|
22
|
+
const v = {
|
|
23
|
+
id: String(x.id), title: String(x.title), source: x.source === 'wbs' ? 'wbs' : 'jira', origin: x.source === 'wbs' ? 'wbs' : 'jira', assigned: true,
|
|
24
|
+
due: x.due || null, priority: TIER_PRIORITY[x.wsjf && x.wsjf.tier] || null, waiting_on: null, job: null,
|
|
25
|
+
key: x.key || null, url: x.url || null, status: x.status || null, suggest: x.suggest || null, my_work_state: x.state || null,
|
|
26
|
+
repo: x.repo || null, initiative: x.initiative || null,
|
|
27
|
+
};
|
|
28
|
+
if (x.state === 'agent') v.waiting_on = AGENT;
|
|
29
|
+
if (x.state === 'ready' || x.category === 'doing') v.flagged = true;
|
|
30
|
+
out.push(v);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fromBrief(brief, todayIso) {
|
|
36
|
+
if (!brief || brief.date !== todayIso) return [];
|
|
37
|
+
const list = [...(Array.isArray(brief.priorities) ? brief.priorities : []), ...(Array.isArray(brief.items) ? brief.items : [])];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const x of list) {
|
|
41
|
+
if (!x || !x.id || !x.title || x.owner !== 'pm' || x.worth_asking || seen.has(x.id)) continue;
|
|
42
|
+
seen.add(x.id);
|
|
43
|
+
out.push({ id: 'brief:' + x.id, title: String(x.title), source: 'brief', origin: 'brief', suggested: true, flagged: true,
|
|
44
|
+
due: null, priority: null, waiting_on: null, job: null, why: x.why_now || null, initiative: x.initiative_id || null });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The virtual items whose id the ledger never held: an item the PM added, pressed or dropped is the ledger's from then on. */
|
|
50
|
+
function addable(virtuals, itemLines) {
|
|
51
|
+
const held = new Set((itemLines || []).filter((l) => l && l.id).map((l) => l.id));
|
|
52
|
+
return (virtuals || []).filter((v) => !held.has(v.id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createdEvent(v) {
|
|
56
|
+
const { id, source, ...fields } = v;
|
|
57
|
+
return { event: 'created', id, fields, source };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A ledger copy of an assigned item takes the source's current status, suggestion and state; the PM's own fields stay. */
|
|
61
|
+
function overlay(item, byId) {
|
|
62
|
+
const v = byId && byId.get(item.id);
|
|
63
|
+
if (!v) return item;
|
|
64
|
+
const out = { ...item };
|
|
65
|
+
for (const k of SHOWN) if (v[k] !== undefined && v[k] !== null) out[k] = v[k];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, AGENT };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.142.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.142.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -165,7 +165,7 @@ export const COPY = Object.freeze({
|
|
|
165
165
|
outcome: 'What came out of it (optional)', notYet: 'Not yet:', laterToday: 'Later today', tomorrow: 'Tomorrow', nextWeek: 'Next week',
|
|
166
166
|
looksDone: 'Looks done?', confirm: 'Confirm done', flag: 'Flag', unflag: 'Unflag', notBefore: 'Not before', someday: 'Someday', bringBack: 'Bring back',
|
|
167
167
|
waitingField: 'Waiting on', waitingWho: 'Who?', stopWaiting: 'Stop waiting', flagged: 'Flagged',
|
|
168
|
-
notBeforeChip: (d) => 'Not before ' + d, advances: (t) => 'Advances team task ' + t, moved: (n) => `Moved ${n}×`,
|
|
168
|
+
notBeforeChip: (d) => 'Not before ' + d, advances: (t) => 'Advances team task ' + t, suggested: 'Suggested', agentSuggests: (t) => 'Agent: ' + t, moved: (n) => `Moved ${n}×`,
|
|
169
169
|
overdue: (n) => (n === 1 ? 'Overdue since yesterday' : `${n} days overdue`), dueToday: 'Due today', dueOn: (d) => 'Due ' + d,
|
|
170
170
|
waitingSince: (w, since, ago) => `Waiting for ${w}` + (since ? ` since ${since}` : '') + (ago ? ` (${ago})` : ''), repeats: (r) => 'Repeats ' + String(r).toLowerCase(),
|
|
171
171
|
/* Review (PRD R7) */
|
|
@@ -793,6 +793,16 @@ function dueChip(inst, item) {
|
|
|
793
793
|
/* The chips the Work page adds to a row: outside PMOS and the person, Not before, Flagged, how often it moved, Looks done, the team task it advances. */
|
|
794
794
|
function workChips(inst, item, o = {}) {
|
|
795
795
|
const today = inst.data.today; const c = [];
|
|
796
|
+
/* work the PM did not type in (work-shows-assigned): where it came from, with its link, and what the agent suggests */
|
|
797
|
+
if (item.origin === 'jira' || item.origin === 'wbs') {
|
|
798
|
+
const label = item.origin === 'jira' ? 'Jira ' + (item.key || '') : 'WBS' + (item.repo ? ' ' + item.repo : '') + (item.key ? ' ' + item.key : '');
|
|
799
|
+
const attrs = { class: 'tag', 'data-pm-origin': item.origin, title: item.status || '' };
|
|
800
|
+
c.push(item.url ? el('a', { ...attrs, href: item.url, target: '_blank', rel: 'noopener', onclick: (e) => e.stopPropagation() }, label.trim()) : el('span', attrs, label.trim()));
|
|
801
|
+
if (item.suggest) c.push(el('span', { class: 'chip2', 'data-pm-suggest': item.id }, c3ic('spark', 'sm'), COPY.agentSuggests(item.suggest)));
|
|
802
|
+
} else if (item.origin === 'brief') {
|
|
803
|
+
c.push(el('span', { class: 'tag', 'data-pm-origin': 'brief', title: item.why || '' }, c3ic('spark', 'sm'), COPY.suggested));
|
|
804
|
+
if (item.why) c.push(el('span', { class: 'chip2', 'data-pm-why': item.id }, item.why));
|
|
805
|
+
}
|
|
796
806
|
if (item.kind === 'offline') c.push(el('span', { class: 'tag wk-off', 'data-pm-offline-tag': item.id }, COPY.offline), el('span', { class: 'chip2' }, c3ic('user', 'sm'), item.person || 'someone'));
|
|
797
807
|
if (item.defer && item.defer > today) c.push(el('span', { class: 'tag wk-nb cal3-up', 'data-pm-notbefore': item.id }, COPY.notBeforeChip(dayLabel(item.defer))));
|
|
798
808
|
if (item.flagged && !o.noFlag) c.push(el('span', { class: 'tag cal3-up', 'data-pm-flagged': item.id }, COPY.flagged));
|
package/workspace/server.js
CHANGED
|
@@ -7028,7 +7028,9 @@ async function handleRequest(req, res) {
|
|
|
7028
7028
|
const owners = () => {
|
|
7029
7029
|
const own = new Map();
|
|
7030
7030
|
if (wsShape(root) !== 'pmos') return own;
|
|
7031
|
-
|
|
7031
|
+
// a record lives in any of the repository's initiative folders: planning/initiatives/ here, pmos/state/initiatives/ in a kit
|
|
7032
|
+
const dirs = initiativeDirs(root);
|
|
7033
|
+
const real = (id) => dirs.some((d) => fs.existsSync(path.join(d, id + '.md')));
|
|
7032
7034
|
for (const g of initiativeIndex(root).initiatives) {
|
|
7033
7035
|
if (!real(g.id)) continue;
|
|
7034
7036
|
for (const a of g.artifacts) { const rel = String(a.path).split(path.sep).join('/'); if (!a.shared && !own.has(rel)) own.set(rel, { id: g.id, title: g.title || g.id }); }
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"work-focus.test.js",
|
|
30
30
|
"work-evidence.test.js",
|
|
31
31
|
"brief-page.test.js",
|
|
32
|
+
"work-assigned.test.js",
|
|
32
33
|
"calendar-events.test.js",
|
|
33
34
|
"item-steps.test.js",
|
|
34
35
|
"status-facts-panel.test.js",
|
|
@@ -158,6 +159,7 @@
|
|
|
158
159
|
"work-focus.test.js": 1,
|
|
159
160
|
"work-evidence.test.js": 1,
|
|
160
161
|
"brief-page.test.js": 1,
|
|
162
|
+
"work-assigned.test.js": 1,
|
|
161
163
|
"calendar-events.test.js": 1,
|
|
162
164
|
"pm-brief-page-test.js": 30,
|
|
163
165
|
"item-steps.test.js": 1,
|