create-pmos 0.150.0 → 0.152.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.
@@ -7,7 +7,7 @@
7
7
  * [{ id, t, minute, text, owner, cat, cat_by, by: 'agent', open?, check?, dropped? }];
8
8
  * an open bullet has cat null. bullets_state: { agent: 'on'|'off'|'failed',
9
9
  * reason, session, next_at_ms, minute_ms, minutes }
10
- * POST /api/notes/bullet { id, bullet, cat?, drop?, text? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
10
+ * POST /api/notes/bullet { id, bullet, cat?, drop?, text?, owner?, due? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
11
11
  * POST /api/notes/new { title?, meeting?, initiative? } → { ok, note, reused } a calendar event that already has a note
12
12
  * gets that note back (reused: true), so one meeting keeps one note (Run 4)
13
13
  * POST /api/notes/link { id, meeting?, initiative?, from_event? } → { ok, note, kept? } the PM's link; null unlinks (Run 4)
@@ -62,7 +62,7 @@
62
62
  * POST /api/notes/highlight-remove { id, hid } → { ok }
63
63
  * POST /api/notes/wispr-fetch { id } → { ok, wispr } the PM asks for Wispr Flow's notes now, after the automatic fetch failed or
64
64
  * found nothing; the agent session is asked at once and again every two minutes for up to 20 minutes
65
- * POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text } → { ok, final_notes } the PM's correction
65
+ * POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text, field?: 'owner'|'due'|'why' } → { ok, final_notes } the PM's correction
66
66
  *
67
67
  * Notes are the PM's own and live in the per-user runtime directory (ctx.runtimeDir), never in a repository, so no route here
68
68
  * writes into a registered workspace. Every POST goes through pm-guard `requirePagePress` (a body that is not an object or carries
@@ -812,7 +812,7 @@ async function handle(req, res, url, ctx) {
812
812
  const q = queueFinal(ctx, id);
813
813
  return done(q.queued ? { ok: true, queued: true } : { ok: false, status: 409, error: q.reason });
814
814
  }
815
- if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text }));
815
+ if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text, owner: body.owner, due: body.due }));
816
816
  if (p === '/api/notes/line') {
817
817
  const pick = (k) => (body[k] === undefined ? undefined : body[k]);
818
818
  return done(notes.editLine(dir, { id, index: Number(body.index), cat: pick('cat'), summary: pick('summary'), tags: pick('tags'), people: pick('people'), text: pick('text') }));
@@ -963,7 +963,7 @@ async function handle(req, res, url, ctx) {
963
963
  }
964
964
  if (p === '/api/notes/highlight-remove') return done(notes.removeHighlight(dir, { id, hid: body.hid }));
965
965
  if (p === '/api/notes/wispr-fetch') return done(fetchWisprNow(ctx, id));
966
- if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text }));
966
+ if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text, field: body.field }));
967
967
  // summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
968
968
  let w = null;
969
969
  if (body.ws != null) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.150.0",
3
+ "version": "0.152.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.150.0",
9
+ "version": "0.152.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.150.0",
3
+ "version": "0.152.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",
@@ -562,7 +562,7 @@ export function handle(S, method, path, p, body) {
562
562
 
563
563
  /* ── views of other routes, for the Releases, Docs and initiative tabs (c3-ib-releases.js, c3-ib-docs.js, c3-ii-tabs.js) ──
564
564
  * These answer only a request that carries `board=` (the tabs add it; the old page never does), so the old demo fixtures keep answering
565
- * everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions.
565
+ * everything else: GET /api/releases, /api/docs/library, /api/my-work, /api/c3-init/breakdown, /api/doc and /api/doc-sheet/revisions, /proposals and /log.
566
566
  * The rows are made from the boards' featured initiatives. IDPF-07 gets no documents on purpose, so the empty state can be seen. */
567
567
  function handleMore(S, method, path, p) {
568
568
  if (method !== 'GET') return undefined;
@@ -574,6 +574,11 @@ function handleMore(S, method, path, p) {
574
574
  if (path === '/api/doc') return { status: 200, text };
575
575
  return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', before_hash: '', after_hash: '' }] });
576
576
  }
577
+ /* the document sheet's Proposals and Edit log tabs: none yet, one first version */
578
+ if ((path === '/api/doc-sheet/proposals' || path === '/api/doc-sheet/log') && /^planning\/ib\//.test(p.path || '')) {
579
+ if (path === '/api/doc-sheet/proposals') return okb({ path: p.path, proposals: [] });
580
+ return okb({ path: p.path, revisions: [{ rev: 1, by: 'human:wawan', at: at(-3, 9) + '.000Z', why: 'First version', via: 'sheet', text_by: 'person', before_hash: '', after_hash: '' }], events: [] });
581
+ }
577
582
  if (!p.board) return undefined;
578
583
  if (path === '/api/releases') {
579
584
  return okb({ board: { name: 'Demo board' }, releases: S.releases.filter((x) => x.board === p.board).map((x) => ({ id: x.id, name: x.name, type: 'both', initiative_ids: x.scope.slice(), deploy_at: x.date + 'T11:00:00', release_at: x.date + 'T12:00:00', accepted_at: x.status === 'Released' ? x.date + 'T12:30:00' : null, gapFlags: [], tags: { detected: [], reason: null }, summary: [], status: x.status, board: x.board })) });
@@ -266,15 +266,56 @@ POST['/api/doc-sheet/edit'] = (b) => {
266
266
  const PROPS = {};
267
267
  POST['/api/doc-sheet/propose'] = (b) => {
268
268
  const R = revsOf(b.path); if (!R) return { ok: false, error: 'could not read the document' };
269
- const id = nextId('prop'); PROPS[id] = { path: b.path, prompt: b.prompt, after: b.after, hash: hashOf(b.after), before: R[R.length - 1].text };
269
+ const id = nextId('prop'); PROPS[id] = { path: b.path, prompt: b.prompt, after: b.after, hash: hashOf(b.after), before: R[R.length - 1].text, state: 'proposed', at: '2026-10-08T08:10:00.000Z' };
270
270
  return { ok: true, id, state: 'proposed', hash: PROPS[id].hash, diff: [], voided_hash: null };
271
271
  };
272
272
  POST['/api/doc-sheet/accept'] = (b) => {
273
273
  const p = PROPS[b.id]; if (!p || p.path !== b.path) return { ok: false, error: 'no such proposal' };
274
274
  if (b.hash !== p.hash) return { ok: false, error: 'the hash does not match the proposal the PM saw', reason: 'hash mismatch' };
275
- const R = revsOf(b.path); if (R[R.length - 1].text !== p.before) return { ok: false, error: 'stale: the document changed since this proposal was made', reason: 'stale' };
276
- return { ok: true, path: b.path, rev: withRev(b.path, 'claude-code/session', p.prompt, p.after) };
275
+ const R = revsOf(b.path); const cur = R[R.length - 1].text;
276
+ const names = Array.isArray(b.sections) ? b.sections : null;
277
+ const m = names ? demoMerge(cur, p.before, p.after, names) : cur === p.before ? { ok: true, text: p.after } : { ok: false, stale: ['the document'] };
278
+ if (!m.ok) return { ok: false, error: 'stale: ' + m.stale.join(', ') + ' changed since this proposal was made', reason: 'stale' };
279
+ p.state = 'accepted'; p.taken = names || demoChanged(p.before, p.after); p.closed_at = '2026-10-08T08:12:00.000Z';
280
+ const rev = withRev(b.path, 'claude-code/session', p.prompt, m.text); const r = R[R.length - 1]; r.via = 'proposal'; r.text_by = 'agent'; r.proposal = b.id; r.sections = p.taken; r.of = demoChanged(p.before, p.after).length;
281
+ return { ok: true, path: b.path, rev };
277
282
  };
283
+ /* the rich document sheet (c3-doc-sheet-rich): the proposals, the edit log, reject, a reason, mark current — the routes' shapes, from the fixtures */
284
+ const demoSecs = (t) => { const out = []; let cur = { name: 'Overview', head: '', body: '' }; for (const l of String(t).split('\n')) { const h = /^#{1,6}\s+(.*)$/.exec(l); if (h) { out.push(cur); cur = { name: h[1].trim(), head: l + '\n', body: '' }; } else cur.body += l + '\n'; } out.push(cur); return out.filter((x) => x.head || x.body.trim()); };
285
+ const demoChanged = (a, b) => { const A = demoSecs(a); const B = demoSecs(b); return [...B.filter((x) => { const y = A.find((q) => q.name === x.name); return !y || y.body.trim() !== x.body.trim(); }).map((x) => x.name), ...A.filter((y) => !B.some((x) => x.name === y.name)).map((y) => y.name)]; };
286
+ function demoMerge(cur, before, after, names) {
287
+ const C = demoSecs(cur); const A = demoSecs(before); const P = demoSecs(after);
288
+ const stale = names.filter((n) => { const a = A.find((x) => x.name === n); const c = C.find((x) => x.name === n); return a ? !c || c.body.trim() !== a.body.trim() : !!c; });
289
+ if (stale.length) return { ok: false, stale };
290
+ for (const n of names) { const p = P.find((x) => x.name === n); const k = C.findIndex((x) => x.name === n); if (!p) { if (k >= 0) C.splice(k, 1); } else if (k >= 0) C[k] = p; else C.push(p); }
291
+ return { ok: true, text: C.map((x) => x.head + x.body).join('').replace(/\n*$/, '\n') };
292
+ }
293
+ GET['/api/doc-sheet/proposals'] = (q) => {
294
+ const path = pathOf(q); const R = revsOf(path); if (!R) return { ok: false, error: 'could not read the document' };
295
+ const cur = demoSecs(R[R.length - 1].text);
296
+ return { ok: true, path, proposals: Object.entries(PROPS).filter(([, p]) => p.path === path).map(([id, p]) => {
297
+ const A = demoSecs(p.before); const B = demoSecs(p.after);
298
+ return { id, state: p.state, prompt: p.prompt, at: p.at, hash: p.hash, by: 'agent', why: p.why || null, taken: p.taken || null, of: null, closed_at: p.closed_at || null,
299
+ sections: demoChanged(p.before, p.after).map((name) => { const a = A.find((x) => x.name === name); const b = B.find((x) => x.name === name); const c = cur.find((x) => x.name === name);
300
+ return { name, kind: !a ? 'added' : !b ? 'removed' : 'changed', before: a ? a.body.trim() : '', after: b ? b.body.trim() : '', current: c ? c.body.trim() : null, stale: a ? !c || c.body.trim() !== a.body.trim() : !!c }; }) };
301
+ }).reverse() };
302
+ };
303
+ const DEMO_EVENTS = [];
304
+ GET['/api/doc-sheet/log'] = (q) => {
305
+ const path = pathOf(q); const R = revsOf(path); if (!R) return { ok: false, error: 'could not read the document' };
306
+ const props = Object.entries(PROPS).filter(([, p]) => p.path === path).map(([id, p]) => ({ kind: 'proposed', at: p.at, id, prompt: p.prompt, by: 'agent' }));
307
+ return { ok: true, path, revisions: R.map(({ text, ...r }) => ({ ...r, via: r.via || 'sheet', text_by: r.text_by || (/^claude/.test(r.by) ? 'agent' : 'person'), before_hash: '', after_hash: hashOf(text) })),
308
+ events: [...props, ...DEMO_EVENTS.filter((e) => e.path === path)] };
309
+ };
310
+ POST['/api/doc-sheet/reject'] = (b) => {
311
+ const p = PROPS[b.id]; if (!p || p.path !== b.path) return { ok: false, error: 'no such proposal' };
312
+ if (typeof b.why !== 'string' || !b.why.trim()) return { ok: false, error: 'a why is required' };
313
+ p.state = 'rejected'; p.why = b.why.trim(); p.closed_at = '2026-10-08T08:12:00.000Z';
314
+ DEMO_EVENTS.push({ path: b.path, kind: 'rejected', at: p.closed_at, id: b.id, why: p.why, by: 'pm' });
315
+ return { ok: true, id: b.id, state: 'rejected' };
316
+ };
317
+ POST['/api/doc-sheet/why'] = (b) => { const R = revsOf(b.path); const r = R && R.find((x) => x.rev === Number(b.rev)); if (!r) return { ok: false, error: 'no such revision' }; if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; r.why_added = b.why.trim(); r.why_added_at = '2026-10-08T08:12:00.000Z'; return { ok: true, rev: r.rev }; };
318
+ POST['/api/doc-sheet/mark-current'] = (b) => { if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; DEMO_EVENTS.push({ path: b.path, kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return { ok: true, at: '2026-10-08T08:12:00.000Z' }; };
278
319
  POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
279
320
 
280
321
  /* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
@@ -4,12 +4,12 @@
4
4
  * report section ([heading, text]). Its path here is planning/reports/<family>-<report>-<audience>.md; the text and the revisions are made on first
5
5
  * read from window.__c3repix (c3-demo-rep-ix.js) and kept in memory for the page's life. Routes answered, in the REAL response shapes of
6
6
  * workspace/lib/routes-doc-sheet.js and routes-doc-health.js:
7
- * GET /api/doc, /api/doc-sheet/revisions, /api/doc-sheet/at
8
- * POST /api/doc-sheet/edit, /propose, /accept, /api/doc-health/check
7
+ * GET /api/doc, /api/doc-sheet/revisions, /api/doc-sheet/at, /api/doc-sheet/proposals, /api/doc-sheet/log
8
+ * POST /api/doc-sheet/edit, /propose, /accept, /reject, /why, /mark-current, /api/doc-health/check
9
9
  * A path outside planning/reports/ goes on to the earlier demo wrappers untouched. Nothing is saved anywhere. */
10
10
 
11
11
  const PREFIX = 'planning/reports/';
12
- const DOCS = {}; // path -> { revs: [{ rev, v, by, at, why, text }] }
12
+ const DOCS = {}; // path -> { revs: [{ rev, v, by, at, why, text }], events: [] }
13
13
  const PROPS = {}; // id -> { path, hash, before, after, prompt }
14
14
  let seq = 0;
15
15
 
@@ -29,10 +29,19 @@ function sectionsFor(path) {
29
29
  function docOf(path) {
30
30
  if (DOCS[path]) return DOCS[path];
31
31
  const s = sectionsFor(path); if (!s) return null;
32
- DOCS[path] = { revs: [{ rev: 1, v: 'v1', by: 'claude-code/session', at: '2026-10-08T08:12:00.000Z', why: 'Assembled from live data.', text: md(s) }] };
32
+ DOCS[path] = { revs: [{ rev: 1, v: 'v1', by: 'claude-code/session', at: '2026-10-08T08:12:00.000Z', why: 'Assembled from live data.', text: md(s) }], events: [] };
33
33
  return DOCS[path];
34
34
  }
35
- const push = (path, by, why, text) => { const d = DOCS[path]; const rev = d.revs.length + 1; d.revs.push({ rev, v: 'v' + rev, by, at: '2026-10-08T08:12:00.000Z', why, text }); return rev; };
35
+ const push = (path, by, why, text, extra) => { const d = DOCS[path]; const rev = d.revs.length + 1; d.revs.push({ rev, v: 'v' + rev, by, at: '2026-10-08T08:12:00.000Z', why, text, ...extra }); return rev; };
36
+ /* the sections of a report text by heading, for the proposals' per-section view */
37
+ const secsOf = (t) => String(t).split(/^(?=## )/m).map((s) => ({ name: (/^## (.*)/.exec(s) || ['', 'Overview'])[1].trim(), body: s.replace(/^## .*\n?/, '').trim() }));
38
+ function propView(id, p, cur) {
39
+ const A = secsOf(p.before); const B = secsOf(p.after); const C = secsOf(cur);
40
+ const names = [...B.filter((x) => { const y = A.find((a) => a.name === x.name); return !y || y.body !== x.body; }).map((x) => x.name), ...A.filter((a) => !B.some((x) => x.name === a.name)).map((a) => a.name)];
41
+ return { id, state: p.state || 'proposed', prompt: p.prompt, at: '2026-10-08T08:12:00.000Z', hash: p.hash, by: 'agent', why: p.why || null, taken: null, of: null, closed_at: null,
42
+ sections: names.map((name) => { const a = A.find((x) => x.name === name); const b = B.find((x) => x.name === name); const c = C.find((x) => x.name === name);
43
+ return { name, kind: !a ? 'added' : !b ? 'removed' : 'changed', before: a ? a.body : '', after: b ? b.body : '', current: c ? c.body : null, stale: a ? !c || c.body !== a.body : !!c }; }) };
44
+ }
36
45
 
37
46
  /* the mockup's docHealth for a report that is still a draft: Fresh passes, Complete does not (a draft), Traced passes, and the claims check. */
38
47
  function health(path) {
@@ -52,6 +61,8 @@ function get(path, q) {
52
61
  if (path === '/api/doc') return d ? new Response(d.revs[d.revs.length - 1].text, { status: 200, headers: { 'content-type': 'text/markdown; charset=utf-8' } }) : new Response('could not read the document', { status: 404 });
53
62
  if (!d) return json({ ok: false, error: 'could not read the document' });
54
63
  if (path === '/api/doc-sheet/revisions') return json({ ok: true, path: p, revisions: d.revs.map(({ text, ...r }) => ({ ...r, before_hash: '', after_hash: hashOf(text) })) });
64
+ if (path === '/api/doc-sheet/proposals') { const cur = d.revs[d.revs.length - 1].text; return json({ ok: true, path: p, proposals: Object.entries(PROPS).filter(([, x]) => x.path === p).map(([id, x]) => propView(id, x, cur)).reverse() }); }
65
+ if (path === '/api/doc-sheet/log') return json({ ok: true, path: p, revisions: d.revs.map(({ text, ...r }) => ({ ...r, via: r.via || 'sheet', text_by: r.text_by || (/^claude/.test(r.by) ? 'agent' : 'person'), before_hash: '', after_hash: hashOf(text) })), events: d.events });
55
66
  if (path === '/api/doc-sheet/at') { const r = d.revs.find((x) => x.rev === Number(q.get('rev'))); return json(r ? { ok: true, path: p, rev: r.rev, text: r.text } : { ok: false, error: 'no such revision' }); }
56
67
  return undefined;
57
68
  }
@@ -68,8 +79,18 @@ function post(path, b) {
68
79
  const p = PROPS[b.id]; if (!p || p.path !== b.path) return json({ ok: false, error: 'no such proposal' });
69
80
  if (b.hash !== p.hash) return json({ ok: false, error: 'the hash does not match the proposal the PM saw', reason: 'hash mismatch' });
70
81
  if (d.revs[d.revs.length - 1].text !== p.before) return json({ ok: false, error: 'stale: the document changed since this proposal was made', reason: 'stale' });
71
- return json({ ok: true, path: b.path, rev: push(b.path, 'claude-code/session', p.prompt, p.after) });
82
+ let text = p.after;
83
+ if (Array.isArray(b.sections) && b.sections.length) { /* only the ticked sections: the rest stay as they are */
84
+ const C = secsOf(p.before); const P = secsOf(p.after);
85
+ for (const n of b.sections) { const x = P.find((y) => y.name === n); const k = C.findIndex((y) => y.name === n); if (!x) { if (k >= 0) C.splice(k, 1); } else if (k >= 0) C[k] = x; else C.push(x); }
86
+ text = C.map((x) => '## ' + x.name + '\n' + x.body + '\n').join('\n'); /* a report is all '## ' sections (md above) */
87
+ }
88
+ p.state = 'accepted';
89
+ return json({ ok: true, path: b.path, rev: push(b.path, 'claude-code/session', p.prompt, text, { via: 'proposal', text_by: 'agent', proposal: b.id }) });
72
90
  }
91
+ if (path === '/api/doc-sheet/reject') { const p = PROPS[b.id]; if (!p || p.path !== b.path) return json({ ok: false, error: 'no such proposal' }); p.state = 'rejected'; p.why = String(b.why || '').trim() || null; d.events.push({ kind: 'rejected', at: '2026-10-08T08:12:00.000Z', id: b.id, why: p.why, by: 'pm' }); return json({ ok: true, id: b.id, state: 'rejected' }); }
92
+ if (path === '/api/doc-sheet/why') { const r = d.revs.find((x) => x.rev === Number(b.rev)); if (!r) return json({ ok: false, error: 'no such revision' }); if (!String(b.why || '').trim()) return json({ ok: false, error: 'a why is required' }); r.why_added = b.why.trim(); return json({ ok: true, rev: r.rev }); }
93
+ if (path === '/api/doc-sheet/mark-current') { if (!String(b.why || '').trim()) return json({ ok: false, error: 'a why is required' }); d.events.push({ kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return json({ ok: true, at: '2026-10-08T08:12:00.000Z' }); }
73
94
  return undefined;
74
95
  }
75
96