create-pmos 0.140.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.140.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@86ccd34
1
+ kernel-source: pmos@69e497b
2
2
  assembled: 2026-10-12
3
3
  mode: vendor
@@ -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:22:45Z"}
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:22:45Z"}
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:22:45Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:34:35Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -114,6 +114,7 @@ const { harness } = require('./c3-ini-harness');
114
114
  ok('L8 Move lists the stages of the initiative\'s own workflow', opts === 'Discover,Plan,Build & Deliver,Monitor & Evaluate', opts);
115
115
  await page.click('[data-ini-moveto="Discover"]');
116
116
  await until(() => store(() => window.__c3boards.rows.find((r) => r.id === 'IDPF-07').stage === 'Discover'));
117
+ await until(async () => /IDPF-07 moved to Discover/.test(await toast()));
117
118
  ok('L9 a pick moves it and the toast confirms', /IDPF-07 moved to Discover/.test(await toast()), await toast());
118
119
  await H.finish();
119
120
  })();
@@ -84,6 +84,21 @@ const { harness } = require('./c3-ini-harness');
84
84
  const nb = await store(() => { const b = window.__c3boards.boards.find((x) => x.id === 'partner-portal'); return b && { build: b.build, defTpl: b.defTpl }; });
85
85
  ok('NB2 Create board adds the board (Build and deliver off, Simple by default) and opens its Triage', nb && nb.build === false && nb.defTpl === 'simple' && await st(() => document.querySelector('[data-ini-tab="triage"]').getAttribute('aria-pressed') === 'true'), JSON.stringify(nb));
86
86
 
87
+ /* ── a board whose open initiatives carry no work and none is featured still lists them at a glance ── */
88
+ await page.click('[data-ini-tab="initiatives"]');
89
+ await until(() => st(() => !!document.querySelector('[data-ini="new-initiative"]')));
90
+ await page.click('[data-ini="new-initiative"]');
91
+ await until(() => st(() => !!document.querySelector('[data-ini="new-ini-form"]')));
92
+ await page.fill('[data-ini="ni-title"]', 'Partner sign-up page');
93
+ await page.fill('[data-ini="ni-anchor"]', 'KR 2.1 Activation to 45%');
94
+ await page.click('[data-ini="ni-create"]');
95
+ await until(() => st(() => !!document.querySelector('[data-ini-crumb="board"]')));
96
+ await page.click('[data-ini-crumb="board"]');
97
+ await until(() => st(() => !!document.querySelector('[data-ini-tab="summary"]')));
98
+ await page.click('[data-ini-tab="summary"]');
99
+ const pg = await until(() => st(() => document.querySelector('[data-ini-panel="summary"] [data-ini-catgo]') && { rows: document.querySelectorAll('[data-ini="glance"] [data-ini-row]').length, empty: /No open initiatives yet/.test(document.querySelector('[data-ini-panel="summary"]').textContent) }));
100
+ ok('B9 Summary lists an open initiative at a glance even when it has no work and is not featured', pg && pg.rows === 1 && !pg.empty, JSON.stringify(pg));
101
+
87
102
  /* ── New initiative, with its own workflow ── */
88
103
  await open('board:idpf:initiatives');
89
104
  await page.click('[data-ini="new-initiative"]');
@@ -24,7 +24,7 @@ Under the rows are a timeline of all boards (board › initiative › epic) and
24
24
 
25
25
  The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, **Timeline**, **Releases**, **Docs** and **Sprints**. A board with Build and deliver off has no Releases and no Sprints tab.
26
26
 
27
- - **Summary** shows the initiatives at a glance and one bar per category. Press a bar to open the Initiatives tab with that category.
27
+ - **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
28
28
  - **Initiatives** lists the initiatives of the board, 25 on each page. Use the saved views (Now and next, Mine, At risk, No owner, All), the filters and the sort. Press **Move** on a row to move the initiative to a stage of its own workflow.
29
29
  - **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). Press **Ask for more** to ask a question, or **Decline** and give a reason.
30
30
  - **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The **Initiative** and **Source** filters are the same on the three tabs.
@@ -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-11
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 own items in six views
72
+ ## The Work page: your work in six views
73
73
 
74
- The Work page holds the items you add for yourself. These are your own to-dos, such as "send Dana the new date". The server decides where each item shows, so one item is never in two views.
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
- const folded = pmItems.fold(itemLines).map((i) => ({ ...i, steps: byItem[i.id] || [] }));
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.140.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.140.0",
9
+ "version": "0.142.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.140.0",
3
+ "version": "0.142.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -335,8 +335,9 @@ export function mountInitiatives(pane, opts) {
335
335
  const inis = r.initiatives; const work = r.items; const open = inis.filter((i) => i.status !== 'done');
336
336
  const risk = inis.filter((i) => ['at risk', 'late'].includes(i.status)).sort((x, y) => String(x.end || '9').localeCompare(String(y.end || '9')));
337
337
  const withWork = new Set(work.map((w) => w.ini));
338
- const glance = open.filter((i) => withWork.has(i.id) || i.featured || (i.horizon === 'now' && i.status !== 'on track'))
339
- .sort((x, y) => (withWork.has(y.id) - withWork.has(x.id)) || ((y.featured ? 1 : 0) - (x.featured ? 1 : 0)) || String(x.end || '9').localeCompare(String(y.end || '9'))).slice(0, 6);
338
+ /* up to six open initiatives: the ones with work, featured, or Now and not on track first, then the rest by end date */
339
+ const prio = (i) => withWork.has(i.id) || i.featured || (i.horizon === 'now' && i.status !== 'on track') ? 1 : 0;
340
+ const glance = [...open].sort((x, y) => (prio(y) - prio(x)) || (withWork.has(y.id) - withWork.has(x.id)) || ((y.featured ? 1 : 0) - (x.featured ? 1 : 0)) || String(x.end || '9').localeCompare(String(y.end || '9'))).slice(0, 6);
340
341
  const epicsOf = (id) => work.filter((w) => w.ini === id && w.kind === 'Epic').length;
341
342
  const iniGlance = (i) => nrow({ ic: i.status === 'late' || i.status === 'at risk' ? 'alert' : 'init', hot: i.status === 'late' || i.status === 'at risk', title: i.title, onOpen: () => ctx.openInitiative(i.id), openLabel: 'Open ' + i.id,
342
343
  chips: [chipMono(i.id), stepsMini(wfOf(i, b), i.stage, tplName(i, b)), statusChip(i.status), progChip(i.progress || 0, toneOf(i)), ownerChip(i.owner), dateChip(i.start, i.end, i.status !== 'done' && i.end && daysBetween(ctx.today, i.end) < 0), chip(icon('bolt', 'sm'), plural(epicsOf(i.id), 'epic'))],
@@ -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));
@@ -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
- const real = (id) => fs.existsSync(path.join(root, 'planning', 'initiatives', id + '.md'));
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,