create-pmos 0.215.0 → 0.217.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.215.0",
3
+ "version": "0.217.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@8822e45
1
+ kernel-source: pmos@b12dcf9
2
2
  assembled: 2026-10-14
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-14T03:35:59Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:36:06Z"}
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-14T03:35:59Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:36:06Z"}
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-14T03:35:59Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:36:06Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -261,6 +261,24 @@ function startShell(name) {
261
261
  }, cvSel, 10000));
262
262
  ok('S2 the tool line lists each call with its target', await page.evaluate((s) => [...document.querySelectorAll(s + ' details[data-chat="tool"] li')].map((li) => li.querySelector('b').textContent + ' ' + ((li.querySelector('span') || {}).textContent || '')).join() === 'Bash git log', cvSel));
263
263
  ok('S3 the status reads Your turn and the meta names the model', await page.evaluate(() => (document.querySelector('[data-cv="status"]') || {}).textContent === 'Your turn' && /claude-test/.test((document.querySelector('[data-cv="meta"]') || {}).textContent || '')));
264
+ // at Whole the list of sessions shows: each row's title sits above its subtitle, both at the row's left, and rows don't overlap
265
+ await page.click('[data-c3-size="whole"]');
266
+ await sleep(250);
267
+ const lay = await page.evaluate((s) => {
268
+ const rows = [...document.querySelectorAll('[data-c3-sess-list] [data-c3-sess-pick]')].map((b) => {
269
+ const r = b.getBoundingClientRect(), n = b.querySelector('.nm').getBoundingClientRect(), u = b.querySelector('.sub').getBoundingClientRect();
270
+ return { top: r.top, bottom: r.bottom, left: r.left, nl: n.left, nb: n.bottom, ul: u.left, ut: u.top, ub: u.bottom };
271
+ });
272
+ const f = document.querySelector(s + ' [data-cv="feed"]').getBoundingClientRect();
273
+ const m = document.querySelector(s + ' [data-chat="me"],' + s + ' [data-chat="bot"]');
274
+ const w = document.querySelector(s + ' [data-cv="feed"] > .wrap').getBoundingClientRect();
275
+ return { rows, gap: w.left - f.left, shown: !!m && m.getBoundingClientRect().height > 0 };
276
+ }, cvSel);
277
+ ok('S3b the list rows are laid out cleanly (title over subtitle, left aligned, no overlap)', lay.rows.length > 0 && lay.rows.every((r, i) =>
278
+ r.nb <= r.ut + 1 && r.ub <= r.bottom && Math.abs(r.nl - r.ul) < 1 && r.nl - r.left < 14 && (i === 0 || r.top >= lay.rows[i - 1].bottom - 1)), JSON.stringify(lay.rows));
279
+ ok('S3c at Whole the conversation starts at the left of its pane, next to the list', lay.shown && lay.gap < 16, String(lay.gap));
280
+ await page.click('[data-c3-size="half"]');
281
+ await sleep(150);
264
282
  await page.fill('[data-cv="msg"]', 'echo c3-conv-$((5*5))');
265
283
  await page.press('[data-cv="msg"]', 'Enter');
266
284
  ok('S4 a sent message shows at once', await waitFor(page, () => [...document.querySelectorAll('[data-c3-conv] [data-chat="me"]')].some((n) => /c3-conv-/.test(n.textContent)), null, 3000));
@@ -71,7 +71,7 @@ A shell where Claude has not started says "No Claude in this shell yet". Press *
71
71
 
72
72
  If PMOS cannot find the transcript of a Claude session, the view says so, and it does not show the transcript of a different session. Read the session in the terminal. PMOS looks for transcripts in `~/.claude`, in each `~/.claude-*` profile folder, and in the folder that `CLAUDE_CONFIG_DIR` names.
73
73
 
74
- At **2/3** and **Whole**, the sheet also shows a list of your running sessions at the left. Press a session to open it.
74
+ At **2/3** and **Whole**, the sheet also shows a list of your running sessions at the left. Each row shows the session name and, below it, the agent and its state. Press a session to open it. The conversation starts next to the list.
75
75
 
76
76
  ### The scratchpad
77
77
 
@@ -121,7 +121,7 @@ The row chip then shows the agent's state:
121
121
 
122
122
  The page reads the state again every 15 seconds while an agent is queued, working or posting.
123
123
 
124
- When the draft is ready, the case file quotes it. Select **Open the draft** to read all of it. Then choose one option:
124
+ When the draft is ready, the case file shows it as a document, with its headings, lists and tables. Select **Open the draft** to read all of it. Then choose one option:
125
125
 
126
126
  - **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
127
127
  - **Send it back with a note** returns the draft to the agent. Type what to change and press Enter.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.215.0",
3
+ "version": "0.217.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.215.0",
9
+ "version": "0.217.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.215.0",
3
+ "version": "0.217.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",
@@ -4,6 +4,8 @@
4
4
  * normalizeCase(model) -> the canonical case (pure; accepts the mockup's field names and the descriptive ones)
5
5
  * slideFor(caseModel, pickedId) -> { enabled, reason, label } for the slide under the case (pure)
6
6
  * renderCase(model, opts) -> the `.case` element; opts: { picked, onPick(id, option), onConfirm({ option, hash, case }),
7
+ * noConf (no confidence figure: the recommended option says "Suggested", the others nothing),
8
+ * renderQuote(text) → nodes (the quote drawn as rich text, for example a markdown draft),
7
9
  * outbox: boolean, onGo(go) }
8
10
  *
9
11
  * What the mockup's case file holds, top to bottom, and what this draws:
@@ -114,6 +116,7 @@ const fxList = (a) => el('div', { class: 'fx' }, ...a.map((f) => el('span', { cl
114
116
  export function renderCase(model, opts = {}) {
115
117
  const c = normalizeCase(model);
116
118
  const outbox = !!opts.outbox;
119
+ const noConf = !!opts.noConf;
117
120
  let picked = c.options.some((o) => o.id === opts.picked) ? opts.picked : c.recId;
118
121
  const root = el('div', { class: 'case', 'data-case': c.id || null, style: '--tc:' + c.tc });
119
122
  const optsHost = c.options.length ? el('div', { class: 'opts', role: 'radiogroup', 'aria-label': 'Options' }) : null;
@@ -125,11 +128,14 @@ export function renderCase(model, opts = {}) {
125
128
  return el('button', { type: 'button', class: 'opt' + (o.rec ? ' rec' : ''), role: 'radio', 'aria-checked': String(on), 'aria-pressed': String(on), 'data-opt': o.id,
126
129
  onclick: () => pick(o.id) },
127
130
  el('span', { class: 'pk' }, c3ic('check', 'sm')),
128
- el('span', { class: 'rk', title: 'Confidence from the sources listed above' }, o.rec ? [c3ic('spark', 'sm'), 'Recommended · ' + c.confidence + '%'] : null),
131
+ // `noConf`: a real item has no confidence figure, so only the recommended option is marked, "Suggested", with no bar
132
+ noConf ? (o.rec ? el('span', { class: 'rk' }, c3ic('spark', 'sm'), 'Suggested') : null)
133
+ : el('span', { class: 'rk', title: 'Confidence from the sources listed above' }, o.rec ? [c3ic('spark', 'sm'), 'Recommended · ' + c.confidence + '%'] : null),
129
134
  el('b', null, o.label),
130
135
  fxList(o.effects),
131
136
  o.rec ? el('div', { class: 'conf' }, el('i', { style: 'width:' + c.confidence + '%' })) : null);
132
137
  }));
138
+ if (noConf) optsHost.querySelectorAll('.conf').forEach((n) => n.remove()); // no figure, no bar
133
139
  };
134
140
  let disposeHold = null;
135
141
  const drawSlide = () => {
@@ -164,7 +170,7 @@ export function renderCase(model, opts = {}) {
164
170
  if (opts.onPick) opts.onPick(id, c.options.find((o) => o.id === id));
165
171
  }
166
172
 
167
- if (c.quote) root.append(el('blockquote', { class: 'qd' }, c.quote));
173
+ if (c.quote) root.append(el('blockquote', { class: 'qd' + (opts.renderQuote ? ' qd-md' : '') }, opts.renderQuote ? opts.renderQuote(c.quote) : c.quote));
168
174
  root.append(el('p', { class: 'sit' }, c.situation));
169
175
  if (c.move) root.append(el('div', { class: 'c3ag' }, el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, c3ic('spark', 'sm'), ' Suggested move: ' + c.move)));
170
176
  if (c.evidence.length) {
@@ -405,6 +405,10 @@
405
405
  .c3 .cft .cm{display:inline-flex;gap:6px;align-items:center}
406
406
  .c3 .cft .cm .conf{width:56px}
407
407
  .c3 .qd{border-left:2px solid var(--ring);padding:2px 0 2px 10px;font-size:var(--fs-3);white-space:pre-wrap;margin:0}
408
+ .c3 .qd.qd-md{white-space:normal;max-height:28rem;overflow:auto}
409
+ .c3 .wk-agent-md{font-size:var(--fs-3);max-width:none}
410
+ .c3 .wk-agent-md h1{font-size:var(--fs-4)}
411
+ .c3 .wk-agent-md h2{font-size:var(--fs-4);margin:14px 0 4px}
408
412
  .c3 .fun{display:grid;gap:4px}
409
413
  .c3 .fun div{display:grid;grid-template-columns:62px 1fr 34px;gap:8px;align-items:center;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
410
414
  .c3 .fun i{display:block;height:8px;border-radius:4px;background:var(--ring);transform-origin:left;animation:c3-grow .7s backwards}
@@ -1697,7 +1701,8 @@
1697
1701
  .c3 .c3t-cv-panes .c3t-tw{flex:1 1 0;height:auto;min-height:0}
1698
1702
  .c3 .c3t-cv-list{display:none;border-right:1px solid var(--border);overflow:auto;padding:0 var(--sp-2) 0 0;margin-right:var(--sp-3);gap:2px;align-content:start}
1699
1703
  .c3 .c3t-cv-list .eyebrow{padding:var(--sp-2) 10px 4px;font:500 var(--fs-1) var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground)}
1700
- .c3 .c3t-cv-list button{display:grid;gap:2px;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);width:100%;min-width:0}
1704
+ .c3 .c3t-cv-list button{display:grid;gap:2px;justify-content:stretch;justify-items:start;align-content:start;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);line-height:1.3;width:100%;min-width:0;height:auto;min-height:0}
1705
+ .c3 .c3t-cv-list button>span{max-width:100%}
1701
1706
  .c3 .c3t-cv-list button .nm,.c3 .c3t-cv-list button .sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1702
1707
  .c3 .c3t-cv-list button .sub{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1703
1708
  .c3 .c3t-cv-list button[aria-current="true"]{background:color-mix(in oklab,var(--acc) 12%,transparent)}
@@ -1708,7 +1713,7 @@
1708
1713
  .c3 .c3t-cv-meta .tg[data-tone="muted"]{--tc:var(--muted-foreground)}
1709
1714
  .c3 .c3t-cv-meta .tg[data-tone="warning"]{--tc:var(--warning)}
1710
1715
  .c3 .c3t-cv-feed{flex:1 1 0;min-height:0;overflow:auto;padding:var(--sp-3) 2px}
1711
- .c3 .c3t-cv-feed>.wrap{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-2)}
1716
+ .c3 .c3t-cv-feed>.wrap{max-width:820px;margin:0;display:flex;flex-direction:column;gap:var(--sp-2)}
1712
1717
  .c3 .c3t-cv .m{padding:8px 14px;border-radius:8px;max-width:85%;overflow-wrap:anywhere;font-size:var(--fs-3);line-height:1.5}
1713
1718
  .c3 .c3t-cv .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent);white-space:pre-wrap}
1714
1719
  .c3 .c3t-cv .m.bot{align-self:flex-start;background:var(--muted)}
@@ -1951,6 +1956,7 @@
1951
1956
  .c3 .wk-agent-sum{margin:0}
1952
1957
  .c3 .wk-agent-body{--tc:var(--acc)}
1953
1958
  .c3 .wk-agent-body .sit{white-space:pre-line}
1959
+ .c3 .wk-agent-doc .wk-agent-md{font-size:var(--fs-4);max-width:72ch}
1954
1960
  .c3 .wk-agent-editbox{display:grid;gap:var(--sp-2)}
1955
1961
  .c3 .wk-agent-edit{width:100%;box-sizing:border-box;font:inherit;font-size:var(--fs-2);line-height:1.5;padding:8px 10px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px;resize:vertical}
1956
1962
  .c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
@@ -30,6 +30,7 @@ import { el } from './dom.js';
30
30
  import { button } from './primitives.js';
31
31
  import { pmGet, pmPost } from './http.js';
32
32
  import { renderCase } from './c3-case.js';
33
+ import { parseMd, toDom } from './c3-md.js';
33
34
  import { makeXp, attachExpand } from './c3-expand.js';
34
35
  import { attachSwipe } from './c3-swipe.js';
35
36
  import { toast } from './c3-toast.js';
@@ -822,6 +823,9 @@ function openAgentSession(id, item) {
822
823
  try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
823
824
  }
824
825
 
826
+ /* The agent's draft drawn the way a document is (c3-md.js, as the document sheet draws it): headings, lists, tables, bold. */
827
+ const draftMd = (text) => el('div', { class: 'ds-md wk-agent-md' }, toDom(parseMd(String(text || '')), document));
828
+
825
829
  /* The agent's draft in a sheet. A draft that waits for the PM can be edited there (amendment 13): Save keeps the PM's text
826
830
  * as the draft, so Post sends exactly that text. */
827
831
  function openDraft(item, inst, editing) {
@@ -842,7 +846,7 @@ function openDraft(item, inst, editing) {
842
846
  ? el('div', { class: 'wk-agent-editbox' }, area, el('div', { class: 'chips2' },
843
847
  button({ label: AG.saveDraft, variant: 'primary', size: 'sm', onClick: save, attrs: { 'data-pm-agent-save': item.id } }),
844
848
  button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
845
- : el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || ''));
849
+ : el('div', { class: 'wk-agent-doc' }, draftMd(d.body || ''));
846
850
  const body = el('div', { class: 'wk-agent-sheet' },
847
851
  el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : d.edited_at ? 'draft · edited by you' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
848
852
  d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
@@ -857,7 +861,8 @@ function openDraft(item, inst, editing) {
857
861
  function caseFile(inst, item, lens) {
858
862
  const { model, run } = itemCase(inst, item, lens);
859
863
  const root = renderCase(model, {
860
- picked: inst.picks.get(item.id), outbox: !!model.outbox,
864
+ picked: inst.picks.get(item.id), outbox: !!model.outbox, noConf: !item.mock_row,
865
+ renderQuote: item.agent && !item.mock_row ? draftMd : undefined,
861
866
  onPick: (id) => { inst.picks.set(item.id, id); const n = root.querySelector('[data-pm-agent-note]'); if (n) { n.hidden = id !== 'back'; if (!n.hidden) n.querySelector('input').focus(); } },
862
867
  onConfirm: ({ option }) => {
863
868
  const fn = run[option.id];
@@ -865,10 +870,6 @@ function caseFile(inst, item, lens) {
865
870
  },
866
871
  });
867
872
  root.setAttribute('data-pm-case', item.id);
868
- if (!item.mock_row) { // real items carry no confidence figure: never show a made-up percentage
869
- root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
870
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
871
- }
872
873
  const extras = [];
873
874
  const rs = inst.runs.get(item.id);
874
875
  const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
@@ -1226,10 +1227,9 @@ function jobCase(inst, j, close) {
1226
1227
  { id: 'mv', l: near ? 'Delete it and move its ' + its.length + ' item' + (its.length === 1 ? '' : 's') + ' to "' + nm(near) + '"' : 'Delete it', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('target', its.length + ' items change job', 'warn')] },
1227
1228
  { id: 'nj', l: 'Delete it and leave its items without a job', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('alert', 'The items have no job', '')] }] };
1228
1229
  const run = { keep: () => { say({ text: 'Kept.', timeout: 2500 }); close(); }, mv: del(near && near.id), nj: del(null) };
1229
- const root = renderCase(model, { onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
1230
+ // a job carries no confidence figure: never show a made-up percentage
1231
+ const root = renderCase(model, { noConf: true, onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
1230
1232
  root.setAttribute('data-pm-case', j.id);
1231
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); }); // a job carries no confidence figure: never show a made-up percentage
1232
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1233
1233
  const sit = root.querySelector('.sit');
1234
1234
  sit.after(el('div', { class: 'c3jf' }, j.mock_name ? jobEditField(inst, j, 'mock_name') : null, jobEditField(inst, j, 'when'), jobEditField(inst, j, 'want'), jobEditField(inst, j, 'so')), jobAll(inst, j));
1235
1235
  return root;
@@ -1246,16 +1246,12 @@ function jobNewCase(inst, st, close) {
1246
1246
  const model = { id: 'job:new', sit: 'The agent drafted this from your line. Change anything, then create it. It has no items yet.', conf: 70, cta: 'Slide to create',
1247
1247
  chg: [['spark', 'Drafted by the agent', 'assumption', 'warn']],
1248
1248
  opts: [{ id: 'mk', l: 'Create the job', rec: 1, fx: [fx('target', 'Items can now be filed under it', 'ok')] }, { id: 'no', l: 'Not now', rec: 0, fx: [] }] };
1249
- const root = renderCase(model, { onConfirm: async ({ option }) => {
1249
+ const root = renderCase(model, { noConf: !demo, onConfirm: async ({ option }) => {
1250
1250
  if (option.id === 'no') { say({ text: 'Dropped the draft.', timeout: 2500 }); close(); return; }
1251
1251
  const line = 'when ' + val('when') + ', I want ' + val('want') + ', so ' + val('so');
1252
1252
  const r = await press(inst, JOBS + '/add', nameIn ? { line, name: nameIn.value.trim() } : { line });
1253
1253
  if (r) { say({ text: 'Job created. Add work to it by saying which job it serves.', timeout: 3000 }); close(); await load(inst); }
1254
1254
  } });
1255
- if (!demo) {
1256
- root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
1257
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1258
- }
1259
1255
  root.querySelector('.sit').after(el('div', { class: 'c3jf' }, nameIn ? field('Job', nameIn) : null, ...['when', 'want', 'so'].map((k, i) => field(k === 'when' ? 'When' : k === 'want' ? 'I want' : 'So', inputs[i]))));
1260
1256
  return root;
1261
1257
  }