create-pmos 0.220.0 → 0.222.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.220.0",
3
+ "version": "0.222.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@14bf375
1
+ kernel-source: pmos@8951b06
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-14T05:03:17Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:33:59Z"}
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-14T05:03:17Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:33:59Z"}
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-14T05:03:17Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:33:59Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -141,6 +141,10 @@ const { harness } = require('./c3-ini-harness');
141
141
  await page.click('[data-ini="edit-workflow"]');
142
142
  await until(() => st(() => !!document.querySelector('[data-ini="wf-editor"]')));
143
143
  ok('W1 the editor offers only the templates the board offers', await st(() => !document.querySelector('[data-wf-tpl="fix"]').disabled && document.querySelector('[data-wf-tpl="project"]').disabled));
144
+ const wfRow = await st(() => { const r = document.querySelector('[data-wf-row="0"]'); const top = (s) => Math.round(r.querySelector(s).getBoundingClientRect().top); const nb = r.querySelector('.nb');
145
+ return { grip: top('[data-wf-drag]'), name: top('[data-wf-name]'), del: top('[data-wf-del]'), dup: [...nb.querySelectorAll('.chips > *')].some((c) => c.textContent.trim() === document.querySelector('[data-wf-cat="0"]').value),
146
+ mono: getComputedStyle(document.querySelector('[data-ini="wf-editor"] [role="group"] > span')).fontFamily !== getComputedStyle(document.querySelector('[data-ini="wf-editor"]')).fontFamily }; });
147
+ ok('W1b each stage is one row (grip, name and remove on one line); no chip repeats the category; the template label is not mono', Math.abs(wfRow.grip - wfRow.del) < 8 && Math.abs(wfRow.name - wfRow.del) < 8 && !wfRow.dup && !wfRow.mono, JSON.stringify(wfRow));
144
148
  await page.focus('[data-wf-down="0"]'); await page.keyboard.press('Enter');
145
149
  const order = await st(() => [...document.querySelectorAll('[data-wf-name]')].map((i) => i.value).join(','));
146
150
  ok('W2 a stage moves down from the keyboard', order.startsWith('Plan,Discover'), order);
@@ -86,7 +86,7 @@ Press the gear in the board header to open the board settings.
86
86
 
87
87
  ## An initiative
88
88
 
89
- Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip next to the status, at the right of the breadcrumb, for example "Skipped Plan" or "No spec yet". Press the gear next to the stepper to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes.
89
+ Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip next to the status, at the right of the breadcrumb, for example "Skipped Plan" or "No spec yet". Press the gear next to the stepper to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes. In the editor, each stage is one row: drag handle, category colour, name, category, then move up, move down and remove. A short note under the name shows only when it adds something, for example **current stage**, **new**, or the old name of a renamed stage.
90
90
 
91
91
  Press **Tools**, at the right of the stepper, to open a tool. The menu lists the tools of the current stage first, then the tools of the other stages. The **…** menu at the right of the breadcrumb opens the Dashboard and the Jira epics of the initiative, and holds Status and Tidy up. The tabs are **Overview**, **Breakdown**, **Docs**, **Tickets**, **Timeline** and **History**.
92
92
 
@@ -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 shows it as a document card, with its headings, lists and tables. Select **Open** to read all of it, or **Copy** to copy the text.
124
+ When the draft is ready, the case file shows it as a document card, with its headings, lists and tables. Select **Open** to read it in the document sheet, the same sheet as your other documents: **Read**, **Edit**, **Proposals** (send it back with a note) and **Edit log** (the conversation with the agent). Select **Copy** to copy the text.
125
125
 
126
126
  Under the draft is the conversation with the agent: your ask, each draft, each note you sent back and each edit. Select **View round** on an earlier draft to read that version. The conversation stays on the item on the next days too.
127
127
 
@@ -130,7 +130,7 @@ To ask the agent for changes, type in the box under the conversation and press E
130
130
  Or choose one option:
131
131
 
132
132
  - **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
133
- - **Edit the draft** opens the text to change. Select **Save the draft**. The card then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
133
+ - **Edit the draft** opens the draft in the document sheet's editor. Select **Save**. The card then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
134
134
  - **Keep it as a draft** leaves the draft as it is.
135
135
  - **Mark it done** closes the item. Nothing is posted.
136
136
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.220.0",
3
+ "version": "0.222.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.220.0",
9
+ "version": "0.222.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.220.0",
3
+ "version": "0.222.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",
@@ -31,6 +31,10 @@
31
31
  *
32
32
  * Two options for a report, which is a document with no stored file yet (report-families-view.js):
33
33
  * generated = { text, why, health } Read shows the generated text; Edit and Proposals say why they need a stored document; Health shows `health`.
34
+ * An agent's draft on a Work item (initiative c3-work-agent, amendment 16) is a generated document with three more hooks:
35
+ * generated.save(text, why) → { ok, error } Edit works: Save hands the text to the caller (the reason is optional) instead of the edit route.
36
+ * generated.ask(note) → { ok, error } Proposals is the note that sends the draft back to the agent.
37
+ * generated.log = [{ who, what, at, text }] Edit log lists these turns (the conversation with the agent).
34
38
  * secExtra(name) a node under each section in Read: the report's claims and where they come from.
35
39
  * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections,
36
40
  * copyKindOf, citesIn, outlineOf, hoodOf. */
@@ -738,7 +742,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
738
742
 
739
743
  /* ── Edit ── */
740
744
  function startEdit() {
741
- if (generated || frozen() || S.text == null) { S.ed = null; return; }
745
+ if ((generated && !generated.save) || frozen() || S.text == null) { S.ed = null; return; }
742
746
  S.ed = { text: S.text, base: splitDoc(S.text), front: {}, kind: 'rich', why: '', whyBad: false, node: null, ta: null, range: null, link: false };
743
747
  }
744
748
  function edNode() {
@@ -785,7 +789,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
785
789
  const s = ui.body.querySelector('[data-doc-savesum]'); if (s) s.replaceChildren(...saveSum(ch).filter(Boolean));
786
790
  }
787
791
  function saveSum(ch) {
788
- if (!ch.length) return [c3ic('info', 'sm'), 'No changes yet. Health checks the document again after you save.'];
792
+ if (!ch.length) return [c3ic('info', 'sm'), generated ? 'No changes yet.' : 'No changes yet. Health checks the document again after you save.'];
789
793
  const cl = [...new Set(ch.map((c) => clashOf(c.name)).filter(Boolean))];
790
794
  return [c3ic('edit', 'sm'), plural(ch.length, 'section') + ' changed: ' + ch.map((c) => c.name).join(', ') + '.',
791
795
  cl.length ? el('span', { class: 'ds-warn' }, ' ' + plural(cl.length, 'open proposal') + ' also change' + (cl.length === 1 ? 's' : '') + ' them; accepting it after your save needs a new draft.') : null];
@@ -835,7 +839,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
835
839
  drawEdit();
836
840
  }
837
841
  function editBody() {
838
- if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why);
842
+ if (generated && !generated.save) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'edit' }, c3ic('lock', 'sm'), generated.why);
839
843
  if (frozen()) return el('div', { class: 'ds', 'data-doc-frozen': '' }, el('div', { class: 'fold w', role: 'status' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to change it.'),
840
844
  el('div', { class: 'ds-acts' }, btn('ds-btn pri', { 'data-doc-next': '' }, [c3ic('plus', 'sm'), 'Start the next cycle'], () => nextCycle())));
841
845
  if (S.text == null) return readBody();
@@ -845,12 +849,12 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
845
849
  let area;
846
850
  if (E.kind === 'rich') { if (!E.node) E.node = edNode(); area = E.node; E.ta = null; }
847
851
  else { E.ta = E.ta || el('textarea', { class: 'ds-src', id: 'dsrc', 'aria-label': 'Markdown source', spellcheck: 'false', 'data-doc-mdsrc': '', onInput: () => { clearTimeout(E.t); E.t = setTimeout(mark, 250); } }, E.text); area = E.ta; E.node = null; }
848
- const why = el('input', { type: 'text', id: 'dwhy', class: E.whyBad ? 'bad' : '', placeholder: 'Why are you changing it? Kept in the edit log.', 'aria-label': 'Why', 'data-doc-why': '', value: E.why,
852
+ const why = el('input', { type: 'text', id: 'dwhy', class: E.whyBad ? 'bad' : '', placeholder: generated ? 'What did you change? Optional.' : 'Why are you changing it? Kept in the edit log.', 'aria-label': 'Why', 'data-doc-why': '', value: E.why,
849
853
  onInput: (e) => { E.why = e.target.value; if (E.whyBad && E.why.trim()) { E.whyBad = false; e.target.classList.remove('bad'); const a = ui.body.querySelector('[data-doc-whybad]'); if (a) a.remove(); } } });
850
854
  const save = el('div', { class: 'ds-save' }, el('div', { class: 'sum', 'data-doc-savesum': '' }, saveSum([])), why,
851
855
  E.whyBad ? el('div', { class: 'fold w', role: 'alert', 'data-doc-whybad': '' }, c3ic('alert', 'sm'), 'Say why the document changes. The reason is kept in the edit log.') : null,
852
856
  S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : null,
853
- el('div', { class: 'row' }, el('span', { class: 'ds-mono' }, c3ic('user', 'sm'), ' Saved by you' + (nextV(doc.v) ? ' as ' + nextV(doc.v) : '')),
857
+ el('div', { class: 'row' }, el('span', { class: 'ds-mono' }, c3ic('user', 'sm'), generated ? ' Saved by you as the draft' : ' Saved by you' + (nextV(doc.v) ? ' as ' + nextV(doc.v) : '')),
854
858
  el('span', { class: 'ds-acts' }, btn('ds-btn', { 'data-doc-cancel': '' }, 'Cancel', () => { S.ed = null; S.err = ''; go('read'); }),
855
859
  btn('ds-btn pri', { 'data-doc-save': '' }, [c3ic('check', 'sm'), 'Save' + (nextV(doc.v) ? ' as ' + nextV(doc.v) : '')], () => saveDoc()))));
856
860
  return el('div', { class: 'ds', 'data-doc-edit': '' }, staleBanner(), propsBlock(secs, true), area, save);
@@ -860,6 +864,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
860
864
  const E = S.ed; if (!E || S.busy) return;
861
865
  const after = draftText(); const ch = changedSections(S.text, after);
862
866
  if (after === S.text || !ch.length) { say('Nothing changed.'); S.ed = null; S.err = ''; go('read'); return; }
867
+ if (generated) { await saveGenerated(after, E.why.trim()); return; }
863
868
  if (!E.why.trim()) { E.whyBad = true; E.text = after; if (E.kind === 'rich') { E.base = splitDoc(after); E.node = null; } else if (E.ta) E.ta.value = after; drawEdit(); const w = ui.body.querySelector('#dwhy'); if (w) w.focus(); return; }
864
869
  const before = S.text; S.busy = true;
865
870
  const r = await pmPost('/api/doc-sheet/edit', { ...base, text: after, why: E.why.trim() }).catch(() => null);
@@ -874,6 +879,15 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
874
879
  });
875
880
  }
876
881
 
882
+ async function saveGenerated(after, why) {
883
+ const E = S.ed; S.busy = true;
884
+ const r = await Promise.resolve(generated.save(after, why)).catch((e) => ({ ok: false, error: e && e.message }));
885
+ S.busy = false;
886
+ if (!r || !r.ok) { S.err = (r && r.error) || 'The draft was not saved.'; E.text = after; if (E.kind === 'rich') { E.base = splitDoc(after); E.node = null; } drawEdit(); return; }
887
+ S.text = after; generated.text = after; S.err = ''; S.ed = null; S.changed = true; go('read');
888
+ say(generated.saved || 'Saved as the draft.');
889
+ }
890
+
877
891
  /* ── Proposals ── */
878
892
  function askBox() {
879
893
  if (frozen()) return el('div', { class: 'fold w', role: 'status', 'data-doc-frozen': '' }, c3ic('lock', 'sm'), 'Issued reports are frozen. Start the next cycle to ask for a change.');
@@ -966,6 +980,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
966
980
  S.rejFor = null; S.rejWhy = ''; S.log = null; await loadProps(); draw(); say('Rejected. The reason is in the edit log.');
967
981
  }
968
982
  function propsBody() {
983
+ if (generated && generated.ask) return askGenerated();
969
984
  if (generated) return el('div', { class: 'fold w', role: 'note', 'data-doc-nostore': 'ask' }, c3ic('lock', 'sm'), generated.why);
970
985
  const op = openProps(); const closed = (S.props || []).filter((p) => p.state !== 'proposed');
971
986
  const list = S.props === null ? el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the proposals.')
@@ -1054,7 +1069,30 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
1054
1069
  if (!j || !j.ok) { say((j && j.error) || 'The reason was not added.'); return; }
1055
1070
  S.whyFor = null; await loadLog(); draw(); say('Reason added. The revision itself is unchanged.');
1056
1071
  }
1072
+ /* a draft on a Work item: the note sends it back to the agent, which drafts it again on the item */
1073
+ function askGenerated() {
1074
+ const ta = el('textarea', { id: 'dpr', placeholder: 'Tell the agent what to change', 'aria-label': 'Note to the agent', 'data-doc-prompt': '', onInput: (e) => { S.prompt = e.target.value; } }, S.prompt);
1075
+ const send = async () => {
1076
+ const v = String(S.prompt || '').trim(); if (!v) { say('Tell the agent what to change'); ta.focus(); return; }
1077
+ S.busy = true; draw();
1078
+ const r = await Promise.resolve(generated.ask(v)).catch((e) => ({ ok: false, error: e && e.message }));
1079
+ S.busy = false;
1080
+ if (!r || !r.ok) { S.err = (r && r.error) || 'The draft was not sent back.'; draw(); return; }
1081
+ S.prompt = ''; S.err = ''; ui.close();
1082
+ };
1083
+ return el('div', { class: 'ds', 'data-doc-proposals': '' }, el('div', { class: 'ds-ask' }, el('label', { class: 'ds-h', for: 'dpr' }, c3ic('spark', 'sm'), 'Send it back to the agent'), ta,
1084
+ el('div', { class: 'ds-acts' }, btn('ds-btn ag', { 'data-doc-ask': '', disabled: S.busy, 'aria-disabled': S.busy ? 'true' : null }, [c3ic('reply', 'sm'), 'Send to the agent'], send)),
1085
+ S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : null,
1086
+ el('p', { class: 'ds-note', 'data-doc-ask-why': '' }, generated.askWhy || 'The agent drafts it again with your note. Nothing is sent anywhere.')));
1087
+ }
1057
1088
  function logBody() {
1089
+ if (generated && Array.isArray(generated.log)) {
1090
+ return el('div', { class: 'ds', 'data-doc-log': '' }, el('h3', { class: 'ds-h' }, 'Conversation with the agent · ' + generated.log.length),
1091
+ el('ul', { class: 'ds-closed hx', 'data-doc-genlog': '' }, generated.log.map((t, i) => el('li', { 'data-doc-genturn': String(i) },
1092
+ el('span', { class: 'chip2' + (t.who === 'agent' ? ' ds-ag' : '') }, c3ic(t.who === 'agent' ? 'spark' : 'user', 'sm'), t.who === 'agent' ? 'Agent' : 'You'),
1093
+ el('span', null, el('b', null, t.what), t.text ? el('small', null, short(t.text)) : null),
1094
+ el('span', { class: 'w' }, t.at ? whenOf(t.at) : '')))));
1095
+ }
1058
1096
  if (generated) return el('div', { class: 'fold', 'data-doc-nostore': 'hist' }, c3ic('info', 'sm'), generated.why);
1059
1097
  if (!S.log) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the edit log.');
1060
1098
  const F = [['all', 'All'], ['people', 'People'], ['agent', 'Agent'], ['outside', 'Outside PMOS']];
@@ -125,6 +125,17 @@
125
125
  .c3 .ini-kb .nrow.ini-dragging{opacity:.4}
126
126
  .c3 [data-ini-col].over,.c3 [data-wf-row].over{outline:2px dashed var(--acc);outline-offset:-2px}
127
127
  .c3 .ini-grip{cursor:grab}
128
+ /* the workflow editor's stage rows (sheet body is a .brief, whose rows have two tracks): one line each, grip, swatch, name, category, up, down, remove */
129
+ .c3 [data-ini="wf-list"] .nrow{grid-template-columns:auto minmax(0,1fr) auto;align-items:start;cursor:default;padding-block:6px}
130
+ .c3 [data-ini="wf-list"] .nrow .nb{padding-right:0}
131
+ .c3 [data-ini="wf-list"] .nrow>.acts{display:flex;gap:6px}
132
+ .c3 [data-ini="wf-list"] .nrow .form{flex-wrap:nowrap}
133
+ .c3 [data-ini="wf-list"] .nrow .form input[type=text]{flex:1 1 auto;width:auto;min-width:0}
134
+ .c3 [data-ini="wf-list"] .nrow .form select{flex:none}
135
+ .c3 [data-ini="wf-list"] .nrow .nb>.chips{padding-left:14px}
136
+ .c3 [data-ini="wf-list"] .nrow .nb>.chips>:nth-child(n+3){display:inline-flex !important}
137
+ .c3.ph [data-ini="wf-list"] .nrow{grid-template-columns:auto minmax(0,1fr)}
138
+ .c3.ph [data-ini="wf-list"] .nrow>.acts{grid-column:2}
128
139
 
129
140
  /* ══ the timeline (Portfolio, Board, Initiative) ══ */
130
141
  .c3 .ini-tl{position:relative;--lw:240px;display:grid;gap:var(--sp-2);min-width:0}
@@ -609,7 +609,7 @@ export function mountInitiatives(pane, opts) {
609
609
  const m = mv();
610
610
  put(host,
611
611
  note('The ordered stages of ' + row.id + ' only. Other initiatives on ' + b.name + ' keep their own. Each stage sits in one category so the portfolio can compare. No conditions or validators, by design: moving work is your call.'),
612
- el('div', { class: 'chips2', role: 'group', 'aria-label': 'Start from a template' }, mono('Start from a template ' + b.name + ' offers'),
612
+ el('div', { class: 'chips2', role: 'group', 'aria-label': 'Start from a template' }, el('span', { class: 'dim' }, 'Start from a template'),
613
613
  ...Object.keys(TEMPLATES).map((k) => pill({ 'aria-pressed': String(W.tpl === k), 'data-wf-tpl': k, disabled: !offer.includes(k), title: offer.includes(k) ? TEMPLATES[k].use : templateBuilds(k) && b.build === false ? 'Needs Build and deliver on the board' : 'Not offered on this board',
614
614
  onclick: () => { W.tpl = k; W.draft = TEMPLATES[k].stages.map((s) => ({ key: 'k' + (++seq), name: s.name, cat: s.cat, was: now.some((o) => o.name === s.name) ? s.name : null })); W.confirm = false; W.err = ''; render('[data-wf-tpl="' + k + '"]'); } }, TEMPLATES[k].name))),
615
615
  b.build === false ? note(b.name + ' has Build and deliver off, so Build and Deliver stages are not offered. Turn it on in the board settings to use them.') : null,
@@ -643,7 +643,8 @@ export function mountInitiatives(pane, opts) {
643
643
  render('[data-wf-name="0"]');
644
644
  }
645
645
 
646
- /** The stage rows of an editor: grip (drag to reorder), name, category, chips, up, down, remove. o.render is set by the caller. */
646
+ /** The stage rows of an editor, one line each: grip (drag to reorder), category swatch, name, category, up, down, remove; under the name only
647
+ * the chips that add something (current stage, was, new, counts, a Build warning). o.render is set by the caller. */
647
648
  function stageRows(W, o) {
648
649
  let dragFrom = null;
649
650
  const api = { render: () => {} };
@@ -652,10 +653,10 @@ export function mountInitiatives(pane, opts) {
652
653
  grip.addEventListener('dragstart', (e) => { dragFrom = k; try { e.dataTransfer.setData('text/plain', 'wf'); e.dataTransfer.effectAllowed = 'move'; const r = grip.closest('[data-wf-row]'); if (r) e.dataTransfer.setDragImage(r, 10, 10); } catch { /* no data transfer */ } });
653
654
  const li = el('li', { class: 'nrow', 'data-wf-row': String(k) }, grip,
654
655
  el('div', { class: 'nb' },
655
- el('div', { class: 'form' },
656
+ el('div', { class: 'form' }, sw(s.cat),
656
657
  el('input', { type: 'text', value: s.name, 'aria-label': 'Stage ' + (k + 1) + ' name', 'data-wf-name': String(k), oninput: (e) => { s.name = e.target.value; } }),
657
658
  el('select', { 'aria-label': 'Stage ' + (k + 1) + ' category', 'data-wf-cat': String(k), onchange: (e) => { s.cat = e.target.value; o.onChange(); } }, ...CATS.map((c) => el('option', { value: c, selected: c === s.cat }, c)))),
658
- el('div', { class: 'chips' }, chip(sw(s.cat), s.cat), o.current && s.was === o.current ? st('ok', null, 'current stage') : null,
659
+ chipsOf(o.current && s.was === o.current ? chip('current stage') : null,
659
660
  s.was ? (s.was !== s.name.trim() ? chip('was ' + s.was) : null) : tag('new'),
660
661
  o.counts && s.was ? chip(plural(Number(o.counts[s.was]) || 0, 'initiative')) : null,
661
662
  !o.build && isBuildStage(s) ? st('warn', 'alert', 'needs Build and deliver') : null)),
@@ -669,6 +670,7 @@ export function mountInitiatives(pane, opts) {
669
670
  li.addEventListener('dragend', () => { dragFrom = null; li.classList.remove('over'); });
670
671
  return li;
671
672
  }), { 'data-ini': 'wf-list' });
673
+ const chipsOf = (...c) => (c.some(Boolean) ? el('div', { class: 'chips' }, ...c) : null);
672
674
  const focusIn = (a, b2) => { const sheetEl = document.querySelector('[data-ini="wf-editor"]'); if (!sheetEl) return; const f = sheetEl.querySelector(a); if (f && !f.disabled) f.focus(); else { const g = sheetEl.querySelector(b2); if (g) g.focus(); } };
673
675
  return api;
674
676
  }
@@ -692,7 +694,7 @@ export function mountInitiatives(pane, opts) {
692
694
  const gone = removed().filter((n) => Number(counts[n]) > 0);
693
695
  put(host,
694
696
  note('The stages that ' + b.name + '\'s initiatives run when they have no workflow of their own. Each initiative can still pick its own from the templates the board offers.'),
695
- el('div', { class: 'chips2', role: 'group', 'aria-label': 'Start from a template' }, mono('Start from a template'),
697
+ el('div', { class: 'chips2', role: 'group', 'aria-label': 'Start from a template' }, el('span', { class: 'dim' }, 'Start from a template'),
696
698
  ...BOARD_TEMPLATES.map((k) => pill({ 'aria-pressed': String(W.tpl === k), 'data-wf-tpl': k, disabled: b.build === false && templateBuilds(k), onclick: () => { W.tpl = k; W.draft = TEMPLATES[k].stages.map((s) => ({ key: 'k' + (++seq), name: s.name, cat: s.cat, hint: '', was: old.includes(s.name) ? s.name : null })); render('[data-wf-tpl="' + k + '"]'); } }, TEMPLATES[k].name))),
697
699
  rowsUi.list(),
698
700
  el('div', { class: 'chips2' }, pbtn('outline', 'Add stage', 'plus', { 'data-wf-add': '', onclick: () => { W.draft.push({ key: 'k' + (++seq), name: 'New stage', cat: 'Do', hint: '', was: null }); render('[data-wf-name="' + (W.draft.length - 1) + '"]'); } })),
@@ -34,7 +34,7 @@ import { parseMd, toDom } from './c3-md.js';
34
34
  import { makeXp, attachExpand } from './c3-expand.js';
35
35
  import { attachSwipe } from './c3-swipe.js';
36
36
  import { toast } from './c3-toast.js';
37
- import { openSheet, closeSheet } from './c3-sheet.js';
37
+ import { openSheet } from './c3-sheet.js';
38
38
  import { c3btn } from './page-agent.js';
39
39
  import { onBriefing, loadBriefing } from './shell-ticker.js';
40
40
  import { reviewSection } from './c3-brief.js';
@@ -729,9 +729,9 @@ const AG = {
729
729
  orDecide: 'Or decide', conv: 'Conversation with the agent', show: 'Show', hide: 'Hide', showAll: 'Show all', showLess: 'Show less', open: 'Open', copy: 'Copy', copied: 'Copied.',
730
730
  send: 'Send to the agent', replyWhat: (w) => `The agent drafts it again. ${w ? `Nothing goes to ${w} or to anyone.` : 'Nothing leaves PMOS.'}`,
731
731
  theDraft: 'the draft above', beingRedrafted: 'being redrafted',
732
- round: (n) => 'Agent draft, round ' + n, yours: 'Edited by you', viewRound: (n) => 'View round ' + n, viewEdit: 'View your edit', earlier: 'earlier version',
732
+ round: (n) => 'Agent draft, round ' + n, yours: 'Edited by you', viewRound: (n) => 'View round ' + n, viewEdit: 'View your edit',
733
733
  noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
734
- saveDraft: 'Save the draft', cancel: 'Cancel', savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.', bodyLabel: 'The draft',
734
+ savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.',
735
735
  doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
736
736
  handed: (w) => `Handed to the agent. Nothing goes to ${w} until you post it.`, stopped: 'Stopped. Nothing was kept.', kept: 'Kept as a draft. Post it when you are ready.',
737
737
  posting: 'Approved. A session posts exactly this text.', sentBack: 'Sent back with your note. The agent drafts it again.', retried: 'The agent tries again.',
@@ -903,35 +903,41 @@ function openAgentSession(id, item) {
903
903
  /* The agent's draft drawn the way a document is (c3-md.js, as the document sheet draws it): headings, lists, tables, bold. */
904
904
  const draftMd = (text) => el('div', { class: 'ds-md wk-agent-md' }, toDom(parseMd(String(text || '')), document));
905
905
 
906
- /* 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
907
- * as the draft, so Post sends exactly that text. */
908
- function openDraft(item, inst, editing, ver) {
909
- const a = item.agent || {}; const d = ver ? { body: ver.body, summary: ver.kind === 'draft' ? ver.text : '' } : a.draft || {};
910
- const canEdit = !!inst && a.state === 'ready' && !ver;
911
- const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
912
- area.value = d.body || '';
913
- const save = async () => {
914
- const v = area.value.trim();
915
- if (!v) { say({ text: 'The draft cannot be empty. Send it back with a note instead.', timeout: 3500 }); area.focus(); return; }
916
- const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'edit', body: v });
917
- if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return; }
918
- closeSheet();
919
- say({ text: isMine(item) ? AG.savedMine : AG.savedDraft(whereOf(item)), timeout: 4000 });
920
- await load(inst);
921
- };
922
- const text = editing && canEdit
923
- ? el('div', { class: 'wk-agent-editbox' }, area, el('div', { class: 'chips2' },
924
- button({ label: AG.saveDraft, variant: 'primary', size: 'sm', onClick: save, attrs: { 'data-pm-agent-save': item.id } }),
925
- button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
926
- : el('div', { class: 'wk-agent-doc' }, draftMd(d.body || ''));
927
- const body = el('div', { class: 'wk-agent-sheet' },
928
- el('div', { class: 'chips' }, el('span', { class: 'tag' }, ver ? AG.earlier : 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),
929
- d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
930
- text,
931
- el('div', { class: 'fold' }, c3ic('info', 'sm'), ' ' + (ver ? 'This is an earlier version. The draft on the item is the newest one.' : a.state === 'sent' ? 'This is the text that was posted.' : isMine(item) ? 'This stays here in PMOS.' : 'Nothing is in ' + whereOf(item) + ' yet. Post it from the item, after you read it.')));
932
- const actions = canEdit && !editing ? [c3btn('chip2 lk', { 'data-pm-agent-edit-sheet': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))] : null;
933
- openSheet({ title: ver ? ver.label + (ver.at ? ' · ' + hhmm(ver.at) : '') : AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
934
- if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
906
+ /* The agent's draft opens in the document sheet, the one every other document opens in (amendment 16): Read with its outline,
907
+ * Edit (Save keeps the PM's text as the draft, so Post sends exactly that text: amendment 13), Proposals (a note sends the draft
908
+ * back to the agent), Edit log (the conversation with the agent) and Health. An earlier version (ver) opens read-only. */
909
+ async function openDraft(item, inst, editing, ver) {
910
+ const a = item.agent || {}; const d = a.draft || {};
911
+ const live = !!inst && a.state === 'ready' && !ver;
912
+ const w = whereOf(item); const mine = isMine(item);
913
+ const what = { ask: mine ? 'added the item' : 'asked', draft: 'drafted it', note: 'sent it back', edit: 'edited the draft', post: 'posted it' };
914
+ const log = turnsOf(item).map((t) => ({ who: t.who === 'agent' ? 'agent' : 'pm', what: (t.body ? t.label + ': ' : '') + (what[t.kind] || t.kind), at: t.at || null, text: t.kind === 'draft' ? t.text : t.kind === 'note' || t.kind === 'ask' ? t.text : '' }));
915
+ const why = ver ? 'This is an earlier version. The draft on the item is the newest one.'
916
+ : a.state === 'sent' ? 'This is the text that was posted. It is kept on the item, not as a file.'
917
+ : 'This draft is kept on the item, not as a file. ' + (mine ? 'It stays here in PMOS.' : 'Nothing is in ' + w + ' yet. Post it from the item, after you read it.');
918
+ const generated = { text: ver ? ver.body || '' : d.body || '', why, log, health: { passed: 0, total: 0, checks: [], err: 'Health checks run on stored documents. This draft is kept on the item.' } };
919
+ if (live) {
920
+ generated.save = async (text) => {
921
+ const v = String(text || '').trim();
922
+ if (!v) return { ok: false, error: 'The draft cannot be empty. Send it back with a note instead.' };
923
+ const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'edit', body: v });
924
+ if (r && r.ok) load(inst);
925
+ return r || { ok: false, error: COPY.saveError };
926
+ };
927
+ generated.saved = mine ? AG.savedMine : AG.savedDraft(w);
928
+ generated.ask = async (note) => {
929
+ const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'revise', note });
930
+ if (r && r.ok) { say({ text: AG.sentBack, timeout: 3500 }); load(inst); }
931
+ return r || { ok: false, error: COPY.saveError };
932
+ };
933
+ generated.askWhy = 'The agent drafts it again with your note, on the item. ' + AG.replyWhat(mine ? '' : w);
934
+ }
935
+ const title = (ver ? ver.label + (ver.at ? ' · ' + hhmm(ver.at) : '') + ' · ' : '') + (mine ? 'Draft: ' + (item.title || '') : AG.draftSheet(item.key || item.title));
936
+ const relations = () => el('ul', { class: 'ds-rel', 'data-pm-agent-rel': item.id }, el('li', null, c3ic(item.url ? 'out' : 'doc', 'sm'),
937
+ item.url ? el('a', { class: 'ds-rlk', href: item.url, target: '_blank', rel: 'noopener noreferrer' }, item.key || item.title, el('small', null, 'the item this draft is for')) : el('span', { class: 'ds-rlk' }, item.title || '', el('small', null, 'the item this draft is for'))));
938
+ const { openDocSheet } = await import('./c3-doc-sheet.js');
939
+ return openDocSheet({ ws: inst && inst.ctx ? inst.ctx.ws : '', doc: { path: '', name: title, ty: 'Draft', v: ver ? '' : (d.edited_at ? 'edited by you' : ''), st: a.state === 'sent' ? 'issued' : 'draft', ini: '', iniName: mine ? 'Work' : w },
940
+ mode: editing && live ? 'edit' : 'read', generated, relations });
935
941
  }
936
942
 
937
943
  /** The case file element of an item: c3-case.js plus what is Work's own (offline work, the shared actions, the steps, the agent tray, the edit fields). */