create-pmos 0.218.0 → 0.220.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.218.0",
3
+ "version": "0.220.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@e9a15ea
1
+ kernel-source: pmos@14bf375
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-14T04:40:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:03:17Z"}
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-14T04:40:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:03:17Z"}
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-14T04:40:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T05:03:17Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -236,6 +236,7 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
236
236
  ok('day: working hours show what the agent finished for you', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment] [data-c3ph-fin="t-0000000000a1"]'), null, 8000));
237
237
  await page.click('[data-c3ph-fin-open="t-0000000000a1"]');
238
238
  ok('day: Read it opens the draft, and approving it waits for the desk', await waitFor(page, () => { const d = document.querySelector('[data-c3ph-draft="t-0000000000a1"]'); return !!d && /31 Oct/.test(d.textContent) && /on your computer/.test(d.textContent); }, null, 5000));
239
+ ok('day: the draft reads as a document, not raw markdown (headings, bold, a list)', await page.evaluate(() => { const m = document.querySelector('[data-c3ph-draft="t-0000000000a1"] .ds-md'); return !!m && !!m.querySelector('h1') && !!m.querySelector('h2') && !!m.querySelector('strong') && m.querySelectorAll('li').length === 2 && !/(^|\s)(#|\*\*)/.test(m.textContent); }));
239
240
  await page.close();
240
241
  page = await open(pctx, '/c3.html?demo=1&at=17:40');
241
242
  ok('day: 17:40 is the evening recap, with the evening icon', (await moment(page)) === 'recap' && await page.evaluate(() => document.querySelector('[data-c3ph-part]')?.dataset.c3phPart === 'evening'), await moment(page));
@@ -121,15 +121,19 @@ 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, with its headings, lists and tables. 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 card, with its headings, lists and tables. Select **Open** to read all of it, or **Copy** to copy the text.
125
+
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
+
128
+ To ask the agent for changes, type in the box under the conversation and press Enter or select **Send to the agent**. Shift+Enter adds a line. The agent drafts it again, and the draft and the conversation stay on the item while it works. You can send a draft back as many times as you want.
129
+
130
+ Or choose one option:
125
131
 
126
132
  - **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
127
- - **Send it back with a note** returns the draft to the agent. Type what to change and press Enter.
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.
128
134
  - **Keep it as a draft** leaves the draft as it is.
129
135
  - **Mark it done** closes the item. Nothing is posted.
130
136
 
131
- To change the text yourself before you post it, select **Edit the draft**, change the text and select **Save the draft**. The case file then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
132
-
133
137
  The agent only drafts. It never posts to Jira until you approve the draft.
134
138
 
135
139
  ### Defer and flag
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: the phone companion"
4
4
  description: "At phone width the console shows a companion: one moment of your day, your items one at a time, Tell PMOS and Record."
5
- timestamp: 2026-10-13
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # Guide: the phone companion
@@ -20,7 +20,7 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
20
20
  | In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**. Recording opens focus (see below). |
21
21
  | Just ended | A meeting ended less than 20 minutes ago | **What came out of it?** If you recorded the meeting, the card shows the summary, the decisions, the actions and the open questions from its notes, the same notes as on the Notes page. While PMOS writes the notes, the card says so. If you did not record it, **Tell PMOS** puts the meeting name in the bar so you can type the result. |
22
22
  | Free time | 15 minutes or more before the next meeting | The items that fit in the time: overdue items first, then items due today, at most two. Each has **Done** and **Later today**. Under them, what the agent finished for you. |
23
- | Finished for you | After noon, before 17:00, with no meeting near | The drafts that the agent finished today, newest first. **Read it** shows the draft. Approve a draft, or send it back with a note, on your computer: nothing is sent from the phone. Under them, the items that fit. |
23
+ | Finished for you | After noon, before 17:00, with no meeting near | The drafts that the agent finished today, newest first. **Read it** shows the draft as a document, with its headings, lists and tables, the same as on your computer. Approve a draft, or send it back with a note, on your computer: nothing is sent from the phone. Under them, the items that fit. |
24
24
  | Evening recap | From 17:00 | How today went: the meetings, the decisions in their notes, the items you marked done and the agent's drafts. Then what moved, what is still open, and one thing for tonight with **Done** and **Tomorrow**. |
25
25
  | End of day | From 20:00, and before 05:00 | Today in one line, the meetings of the next working day (tap one to see its brief), and how many items carry over |
26
26
 
@@ -32,7 +32,6 @@
32
32
  */
33
33
 
34
34
  const MAX_PARALLEL = 5;
35
- const MAX_REVISIONS = 3;
36
35
  const MAX_CHECK_LOG = 20;
37
36
  const MAX_INTERVAL_MIN = 120;
38
37
  const BEFORE_MEETING_MIN = 15;
@@ -47,6 +46,10 @@ const iso = (ms) => new Date(ms).toISOString();
47
46
  const clone = (x) => JSON.parse(JSON.stringify(x));
48
47
  const isUrl = (v) => typeof v === 'string' && v.length <= 500 && /^https?:\/\/\S+$/.test(v);
49
48
  const text = (v) => (typeof v === 'string' ? v.trim() : '');
49
+ /* The conversation on a task (c3-work-agent amendment 15): the agent's drafts, the PM's notes, edits and post, in order, so
50
+ * Work can show every round. Each draft keeps its body, so an earlier round can be read again. At most MAX_LOG turns. */
51
+ const MAX_LOG = 30;
52
+ function logTurn(t, turn) { t.log = [...(Array.isArray(t.log) ? t.log : []), turn].slice(-MAX_LOG); }
50
53
  const pad = (n) => String(n).padStart(2, '0');
51
54
  const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
52
55
  /** ms of HH:MM on the local day of `ms`. */
@@ -129,7 +132,8 @@ function addBriefed(run, spec) {
129
132
  const next = clone(run);
130
133
  next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
131
134
  goal: null, my_work: s.my_work || null, brief_at: s.brief_at || null, instruction: text(s.instruction), context: s.context && typeof s.context === 'object' ? clone(s.context) : null,
132
- status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null });
135
+ status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null,
136
+ ...(Array.isArray(s.log) && s.log.length ? { log: clone(s.log).slice(-MAX_LOG) } : {}) });
133
137
  return { ok: true, run: next, added: s.id };
134
138
  }
135
139
 
@@ -201,6 +205,7 @@ function deliver(run, taskId, d, now) {
201
205
  if (body.length > 8000) return 'the body is over 8000 characters';
202
206
  if (b.link !== undefined && b.link !== null && b.link !== '' && !isUrl(b.link)) return 'the link must be an http(s) address of at most 500 characters';
203
207
  t.draft = { summary, body, link: b.link || null, at: iso(now) };
208
+ logTurn(t, { who: 'agent', kind: 'draft', at: iso(now), text: summary, body });
204
209
  t.status = t.kind === 'auto' ? 'done' : 'review';
205
210
  t.error = null;
206
211
  return null;
@@ -229,14 +234,16 @@ function decide(run, taskId, op, now) {
229
234
  if (t.status === 'review' && t.kind === 'approve') { t.status = 'sending'; keepSession(t); t.started_at = null; }
230
235
  else if (!(t.status === 'done' && t.kind === 'auto')) return `task ${taskId} is ${t.status} — only a draft waiting for you can be approved`;
231
236
  t.decided = { action: 'approve', at, ...(note ? { note: note.slice(0, 500) } : {}) };
237
+ logTurn(t, { who: 'pm', kind: 'post', at });
232
238
  return null;
233
239
  }
234
240
  if (o.action === 'revise') {
235
241
  if (t.status !== 'review' && t.status !== 'done') return `task ${taskId} is ${t.status} — only a delivered draft can be revised`;
236
242
  if (!note) return 'say what to change: a revise needs a note';
237
243
  if (note.length > 500) return 'the note is over 500 characters';
238
- if (t.revisions.length >= MAX_REVISIONS) return `task ${taskId} has had ${MAX_REVISIONS} revisions — at most ${MAX_REVISIONS}. Do the rest yourself.`;
244
+ // no cap on rounds: a conversation with the agent takes as many as the PM wants (c3-work-agent amendment 15; was 3)
239
245
  t.revisions.push({ note, at });
246
+ logTurn(t, { who: 'pm', kind: 'note', at, text: note });
240
247
  t.status = 'waiting'; keepSession(t); t.decided = { action: 'revise', at, note };
241
248
  return null;
242
249
  }
@@ -260,6 +267,7 @@ function edit(run, taskId, body, now) {
260
267
  if (b === t.draft.body) return null;
261
268
  if (t.draft.agent_body === undefined) t.draft.agent_body = t.draft.body;
262
269
  t.draft.body = b; t.draft.edited_at = iso(now);
270
+ logTurn(t, { who: 'pm', kind: 'edit', at: iso(now), body: b });
263
271
  return null;
264
272
  });
265
273
  }
@@ -274,6 +282,7 @@ function carry(run, task, now) {
274
282
  if (findTask(next, task.id)) return { ok: false, error: `task ${task.id} is already in today's run` };
275
283
  const t = clone(task);
276
284
  Object.assign(t, { status: 'sending', session_id: null, prev_session: null, started_at: null, error: null, decided: { action: 'approve', at: iso(now) } });
285
+ logTurn(t, { who: 'pm', kind: 'post', at: iso(now) });
277
286
  next.tasks.push(t);
278
287
  return { ok: true, run: next, task: task.id };
279
288
  }
@@ -441,6 +450,6 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
441
450
  return { ok: true, run: next };
442
451
  }
443
452
 
444
- module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
453
+ module.exports = { MAX_PARALLEL, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
445
454
  newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, edit, carry, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
446
455
  checkDue, checkStarted, checkDone };
@@ -196,7 +196,7 @@ function stateOf(item, presses, runs) {
196
196
  const t = taskOf(runs, last);
197
197
  const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null, edited_at: t.draft ? t.draft.edited_at || null : null,
198
198
  sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
199
- session: t.stalled_session || null, asks: t.asks || null } : null;
199
+ session: t.stalled_session || null, asks: t.asks || null, log: Array.isArray(t.log) ? t.log : null } : null;
200
200
  const base = { task, instruction: last.instruction, decided_at: last.at };
201
201
  if (!t) return { state: 'agent', sub: 'waiting', ...base };
202
202
  if (t.status === 'review') return { state: 'ready', sub: 'review', ...base };
@@ -279,7 +279,10 @@ function agentItemPress(ctx, res, verb, body, itemId, now) {
279
279
  if (body.action === 'revise' && !note) { ctx.json(res, { ok: false, error: 'say what to change: a revise needs a note' }, 400); return; }
280
280
  const prev = t.draft && t.draft.body ? String(t.draft.body).slice(0, 4000) : '';
281
281
  const own = body.action === 'revise' ? `Draft this item again. The PM's note on your earlier draft: ${note}${prev ? `\n\nYour earlier draft, to change:\n${prev}\n` : ''}` : '';
282
- const b = ctx.myWorkBrief(g.spec(own), String(body.ws || run.ws || ''));
282
+ // the conversation goes on in today's task: the earlier rounds and this note (amendment 15)
283
+ const prior = Array.isArray(t.log) && t.log.length ? t.log : t.draft ? [{ who: 'agent', kind: 'draft', at: t.draft.at || null, text: t.draft.summary || '', body: t.draft.body || '' }] : [];
284
+ const log = body.action === 'revise' ? [...prior, { who: 'pm', kind: 'note', at: new Date(now).toISOString(), text: note }] : prior;
285
+ const b = ctx.myWorkBrief({ ...g.spec(own), log }, String(body.ws || run.ws || ''));
283
286
  if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return; }
284
287
  after(b.run, b.task); return;
285
288
  }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * READ ONLY, and built only from what PMOS already holds. Nothing is invented. The companion reads it for three moments:
7
7
  * finished working hours, "Finished for you": the agent's tasks of today's day run (goals/<date>.run.json, lib/day-run.js) that have a
8
- * draft. { id, title, summary, body, at, status, link, source }; status 'review' (waits for the PM's approval at the desk),
8
+ * draft. { id, title, summary, body, at, status, link, source }; body keeps its lines (markdown, at most 8000 characters); status 'review' (waits for the PM's approval at the desk),
9
9
  * 'done' (an auto task) or 'sent'. Newest first, at most 5.
10
10
  * recap evening: how the day went. meetings = today's meetings that have ended; decided = the decisions in the final notes of today's
11
11
  * meetings; done = the PM's own items marked done today; finished = the agent's drafts of today. moved = at most 5 rows of
@@ -21,6 +21,8 @@ const { todayAgenda, notesAdapter } = require('./routes-brief-page.js');
21
21
  const pad = (n) => String(n).padStart(2, '0');
22
22
  const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
23
23
  const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
24
+ // the draft's text keeps its lines: the companion draws it as a document (markdown, c3-md.js)
25
+ const clipDoc = (s, n) => { const t = String(s || '').replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
24
26
  const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
25
27
  const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
26
28
  const FINISHED = ['review', 'done', 'sent'];
@@ -36,7 +38,7 @@ function nextWorkday(now) {
36
38
  function finishedOf(run) {
37
39
  const tasks = run && Array.isArray(run.tasks) ? run.tasks : [];
38
40
  return tasks.filter((t) => t && FINISHED.includes(t.status) && t.draft && t.draft.summary)
39
- .map((t) => ({ id: t.id, title: clip(t.title, 200), summary: clip(t.draft.summary, 300), body: clip(t.draft.body, 1200), at: t.draft.at || null, status: t.status, link: t.draft.link || t.link || null, source: t.source || null }))
41
+ .map((t) => ({ id: t.id, title: clip(t.title, 200), summary: clip(t.draft.summary, 300), body: clipDoc(t.draft.body, 8000), at: t.draft.at || null, status: t.status, link: t.draft.link || t.link || null, source: t.source || null }))
40
42
  .sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')));
41
43
  }
42
44
 
@@ -33,7 +33,7 @@ function agentOf(x) {
33
33
  state, suggest: x.suggest || null, instruction: x.instruction || null,
34
34
  draft: t && (t.draft || t.body) ? { summary: t.draft || null, body: t.body || null, edited_at: t.edited_at || null } : null,
35
35
  sent: t && t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null,
36
- error: t ? t.error || null : null, session: t && t.session ? String(t.session) : null, asks: t ? t.asks || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null,
36
+ error: t ? t.error || null : null, session: t && t.session ? String(t.session) : null, asks: t ? t.asks || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null, log: t ? t.log || null : null,
37
37
  };
38
38
  }
39
39
 
@@ -74,13 +74,13 @@ const TASK_STATE = { waiting: 'queued', working: 'working', sending: 'sending',
74
74
  /** The agent field of an item whose day task is `taskId` in `run` (or none yet: the offer `suggest`). */
75
75
  function taskAgent(taskId, suggest, run) {
76
76
  const t = run && Array.isArray(run.tasks) ? run.tasks.find((y) => y && y.id === taskId) || null : null;
77
- const base = { suggest, instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null };
77
+ const base = { suggest, instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null, log: null };
78
78
  if (!t) return { state: 'suggest', ...base };
79
79
  return { ...base, state: TASK_STATE[t.status] || 'queued', instruction: t.instruction || null,
80
80
  draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null, edited_at: t.draft.edited_at || null } : null,
81
81
  sent: t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null, error: t.error || null,
82
82
  session: t.stalled_session ? String(t.stalled_session) : null, asks: t.asks || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
83
- started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null };
83
+ started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null, log: Array.isArray(t.log) ? t.log : null };
84
84
  }
85
85
  const briefAgent = (x, run) => taskAgent('t-' + x.id, briefSuggest(x), run);
86
86
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.218.0",
3
+ "version": "0.220.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.218.0",
9
+ "version": "0.220.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.218.0",
3
+ "version": "0.220.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",
@@ -35,7 +35,7 @@ const at = (d, hh, mm) => new Date(2026, 9, d, hh, mm).toISOString();
35
35
  export const demoDay = () => ({
36
36
  ok: true, date: '2026-10-08',
37
37
  finished: [
38
- { id: 't-0000000000a1', title: 'Reply to Priya on the Checkout v2 date', summary: 'A reply that moves v2 to 31 Oct and keeps beta on 30 Oct, with the reason in one line.', body: 'Hi Priya,\nCheckout v2 moves to 31 Oct: the sandbox keys arrive two days late.\nBeta stays on 30 Oct for the two partners already set up.', at: at(8, 12, 52), status: 'review', link: null, source: 'my-work' },
38
+ { id: 't-0000000000a1', title: 'Reply to Priya on the Checkout v2 date', summary: 'A reply that moves v2 to 31 Oct and keeps beta on 30 Oct, with the reason in one line.', body: '# Reply to Priya\n\nHi Priya,\n\n**Checkout v2 moves to 31 Oct**: the sandbox keys arrive two days late.\n\n## What stays\n\n- Beta stays on 30 Oct for the two partners already set up.\n- The `checkout_v2` flag stays off until then.', at: at(8, 12, 52), status: 'review', link: null, source: 'my-work' },
39
39
  { id: 't-0000000000a2', title: 'Weekly status for leadership', summary: 'Green on Checkout v2 with the new date; amber on Returns, where the interview slipped a week.', body: '', at: at(8, 12, 40), status: 'done', link: null, source: 'brief' },
40
40
  ],
41
41
  recap: { meetings: 5, decided: 2, done: 1, finished: 2,
@@ -7,13 +7,15 @@
7
7
  * Done and Later today when the row is the PM's own Work item. opts: { onDone(r), onLater(r) }
8
8
  * openAll(rows, opts) every row that waits, as a list; a press opens that row (openNeed). opts as openNeed.
9
9
  * openDraft(t) a draft the agent finished today (GET /api/phone-day finished): what it is, its summary, its text and its
10
- * link. Approving or revising it waits for the desk, where the whole draft and its sources are.
10
+ * link. The text is drawn as a document (c3-md.js, as the desk's Work sheet and the document sheet draw it):
11
+ * headings, lists, tables, bold. Approving or revising it waits for the desk, where the whole draft and its sources are.
11
12
  * Nothing here writes: the buttons call back into c3-phone.js, which owns the writes and their Undo. */
12
13
  import { el } from './dom.js';
13
14
  import { button, row } from './primitives.js';
14
15
  import { pmGet } from './http.js';
15
16
  import { c3ic } from './pm-work-view.js';
16
17
  import { openSheet, closeSheet } from './c3-sheet.js';
18
+ import { parseMd, toDom } from './c3-md.js';
17
19
 
18
20
  const p2 = (n) => String(n).padStart(2, '0');
19
21
  const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
@@ -88,7 +90,7 @@ export function openDraft(t) {
88
90
  const body = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-draft': t.id },
89
91
  el('span', { class: 'c3ph-step' }, (t.status === 'review' ? 'Waits for you' : t.status === 'sent' ? 'Sent' : 'Finished').toUpperCase() + (t.at ? ' · ' + hm(t.at) : '')),
90
92
  line(t.summary),
91
- ...String(t.body || '').split(/\n+/).map((x) => x.trim()).filter(Boolean).slice(0, 12).map((x) => el('div', { class: 'c3ph-prep' }, x)),
93
+ t.body ? el('div', { class: 'ds-md wk-agent-md c3ph-md' }, toDom(parseMd(String(t.body)), document)) : null,
92
94
  t.link ? el('div', { class: 'chips2' }, link(t.link, 'Open the source')) : null,
93
95
  line(t.status === 'review' ? 'Approve it, or send it back with a note, on your computer. Nothing is sent from the phone.' : 'Nothing more is needed from you.'));
94
96
  return openSheet({ title: t.title, icon: 'file-text', body });
@@ -59,3 +59,8 @@
59
59
  .c3 .c3ph-stats b{font-size:20px;font-variant-numeric:tabular-nums}
60
60
  .c3 .c3ph-stats span{font-size:11px;color:var(--muted-foreground);line-height:1.25}
61
61
  .c3 .c3ph-pts{display:grid;gap:2px}
62
+ /* the agent's draft in its sheet, drawn as a document (c3-md.js): its headings, lists and tables, at the phone's reading size */
63
+ .c3 .c3ph-md{max-width:none;padding-top:8px;border-top:1px solid var(--border);font-size:var(--fs-3);line-height:1.55}
64
+ .c3 .c3ph-md h1,.c3 .c3ph-md h2{font-size:var(--fs-4);margin:12px 0 4px}
65
+ .c3 .c3ph-md p{margin:6px 0}
66
+ .c3 .c3ph-md .tw{max-width:100%}
@@ -1957,6 +1957,27 @@
1957
1957
  .c3 .wk-agent-body{--tc:var(--acc)}
1958
1958
  .c3 .wk-agent-body .sit{white-space:pre-line}
1959
1959
  .c3 .wk-agent-doc .wk-agent-md{font-size:var(--fs-4);max-width:72ch}
1960
+ .c3 .wk-doc{display:grid;gap:var(--sp-2);box-shadow:none}
1961
+ .c3 .wk-doc-h{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:space-between}
1962
+ .c3 .wk-doc-to{font-size:var(--fs-3);font-weight:600}
1963
+ .c3 .wk-doc .ds-md h3{display:block;color:var(--foreground);text-transform:none;letter-spacing:0}
1964
+ .c3 .wk-doc-clip{max-height:22rem;overflow:hidden;-webkit-mask-image:linear-gradient(to bottom,var(--foreground) 65%,transparent);mask-image:linear-gradient(to bottom,var(--foreground) 65%,transparent)}
1965
+ .c3 .wk-doc>.chip2{justify-self:start}
1966
+ .c3 .wk-conv{display:grid;gap:var(--sp-2)}
1967
+ .c3 .wk-conv-h{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}
1968
+ .c3 .wk-conv-h>.dim{display:inline-flex;gap:6px;align-items:center;font-size:var(--fs-2)}
1969
+ .c3 .wk-thr li{grid-template-columns:auto minmax(0,1fr);align-items:start}
1970
+ .c3 .wk-thr li>.ic{margin-top:3px}
1971
+ .c3 .wk-thr li.ag>.ic{color:var(--acc)}
1972
+ .c3 .wk-turn{display:grid;gap:4px;min-width:0}
1973
+ .c3 .wk-turn p{margin:0}
1974
+ .c3 .wk-who{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;font-size:var(--fs-2)}
1975
+ .c3 .wk-reply{display:grid;gap:6px}
1976
+ .c3 .wk-reply textarea{width:100%;box-sizing:border-box;font:inherit;font-size:var(--fs-3);line-height:1.45;padding:8px 10px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:8px;resize:vertical;min-height:3.2em}
1977
+ .c3 .wk-reply textarea:focus-visible{outline:2px solid var(--ring);outline-offset:1px}
1978
+ .c3 .wk-reply-f{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:space-between}
1979
+ .c3 .wk-reply-f>.dim{display:inline-flex;gap:6px;align-items:center;font-size:var(--fs-2)}
1980
+ .c3 .case.wk-ag>.wkq{border-top:1px solid var(--border);padding-top:var(--sp-3)}
1960
1981
  .c3 .wk-agent-editbox{display:grid;gap:var(--sp-2)}
1961
1982
  .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}
1962
1983
  .c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
@@ -725,6 +725,11 @@ const AG = {
725
725
  openSession: 'Open the session',
726
726
  willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
727
727
  draftIt: 'Let the agent draft this', tryAgain: 'Try again', post: (k) => 'Post it to ' + k, back: 'Send it back with a note', keepDraft: 'Keep it as a draft',
728
+ sitRedraft: (w) => `The agent is redrafting with your note. Nothing went to ${w}.`, sitRedraftMine: 'The agent is redrafting with your note. The draft stays here in PMOS.',
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
+ send: 'Send to the agent', replyWhat: (w) => `The agent drafts it again. ${w ? `Nothing goes to ${w} or to anyone.` : 'Nothing leaves PMOS.'}`,
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',
728
733
  noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
729
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',
730
735
  doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
@@ -760,7 +765,7 @@ function agentCase(inst, item, L) {
760
765
  const o = (id, l, f, run, extra) => ({ id, l, rec: 0, fx: f, run, ...(extra || {}) });
761
766
  const own = L.opts.slice(0, 2).map((x) => ({ ...x, rec: 0 }));
762
767
  const ask = () => inst.asks.get(item.id) || a.instruction || a.suggest || '';
763
- let sit = ''; let opts = []; let quote = ''; const chg = []; let outbox = false; let cta = ''; let agentAsk = null; let after = null;
768
+ let sit = ''; let opts = []; const quote = ''; const chg = []; let outbox = false; let cta = ''; let agentAsk = null; let before = null;
764
769
  const extras = [];
765
770
  const strip = (spin, ic, text) => el('div', { class: 'c3ag', 'data-pm-agent-strip': item.id }, el('div', { class: 'fold a', role: 'status' }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), ' ' + text + ' ',
766
771
  c3btn('chip2 lk', { 'data-pm-agent-stop': item.id }, [c3ic('stop', 'sm'), AG.stop], () => agentPress(inst, '/api/my-work/stop', { item: item.id }, AG.stopped))));
@@ -780,45 +785,117 @@ function agentCase(inst, item, L) {
780
785
  editing ? el('div', { class: 'form wk-agent-ask' }, input, button({ label: COPY.save, variant: 'outline', size: 'sm', onClick: save })) : null));
781
786
  if (!editing) agentAsk = c3btn('chip2 lk', { 'data-pm-agent-change': item.id }, [c3ic('edit', 'sm'), AG.change], () => { inst.editing.add('ask:' + item.id); draw(inst); });
782
787
  }
783
- } else if (a.state === 'queued') {
784
- sit = AG.sitQueued; extras.push(strip(false, 'clock', 'Queued' + (a.decided_at ? ' at ' + hhmm(a.decided_at) : '')));
785
- } else if (a.state === 'working') {
786
- sit = AG.sitWorking; extras.push(strip(true, 'spark', 'Working' + (a.started_at ? ' · started ' + minsAgo(a.started_at) : '')));
788
+ } else if (a.state === 'queued' || a.state === 'working') {
789
+ // a redraft after the PM's note shows the draft and the conversation above the strip (amendment 15)
790
+ const redraft = !!(a.draft && a.draft.body);
791
+ sit = redraft ? (isMine(item) ? AG.sitRedraftMine : AG.sitRedraft(w)) : a.state === 'queued' ? AG.sitQueued : AG.sitWorking;
792
+ if (redraft) extras.push(draftCard(inst, item, k, true), conversation(inst, item, true));
793
+ extras.push(a.state === 'queued' ? strip(false, 'clock', 'Queued' + (a.decided_at ? ' at ' + hhmm(a.decided_at) : ''))
794
+ : strip(true, 'spark', (redraft ? 'Redrafting with your note' : 'Working') + (a.started_at ? ' · started ' + minsAgo(a.started_at) : '')));
787
795
  } else if (a.state === 'ready') {
788
- const d = a.draft || {};
789
- quote = d.body || ''; sit = AG.sitReady('', w);
790
- if (d.summary) chg.push(['doc', d.summary, 'draft']);
791
- if (d.edited_at) chg.push(['edit', AG.edited, '']);
792
- if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
796
+ sit = AG.sitReady('', w);
793
797
  outbox = true; cta = 'Hold to post';
794
- const note = el('input', { type: 'text', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id, autocomplete: 'off' });
795
- const back = () => { const v = note.value.trim(); if (!v) { say({ text: AG.needNote, timeout: 3500 }); note.focus(); return; } agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'revise', note: v }, AG.sentBack); };
796
- note.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) back(); });
798
+ const mine = isMine(item);
797
799
  opts = [
798
- isMine(item) ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
799
- o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
800
- o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: isMine(item) ? AG.keptMine : AG.kept, timeout: 3000 })),
801
- o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
800
+ mine ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
801
+ mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)) : null,
802
+ o('edit', AG.editDraft, [fx('edit', mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
803
+ o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: mine ? AG.keptMine : AG.kept, timeout: 3000 })),
804
+ mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
802
805
  ].filter(Boolean);
803
- const noteBox = el('div', { class: 'form wk-agent-ask', 'data-pm-agent-note': item.id }, note); noteBox.hidden = inst.picks.get(item.id) !== 'back';
804
- extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
805
- c3btn('chip2 lk', { 'data-pm-agent-edit': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))));
806
- after = noteBox;
806
+ extras.push(draftCard(inst, item, k, false), conversation(inst, item, false, w));
807
+ before = el('div', { class: 'wk-conv-h', 'data-pm-agent-decide': item.id }, el('span', { class: 'dim mono' }, c3ic('check', 'sm'), AG.orDecide));
807
808
  } else if (a.state === 'sending') {
808
809
  sit = AG.sitSending(k);
810
+ if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
809
811
  } else if (a.state === 'sent') {
810
812
  sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
811
813
  opts = L.opts.map((x) => ({ ...x }));
812
- if (a.draft && a.draft.body) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
813
- a.sent && a.sent.link ? el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link) : null));
814
+ if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
815
+ if (a.sent && a.sent.link) extras.push(el('div', { class: 'chips2' }, el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link)));
814
816
  }
815
817
  if (opts.length) opts[0].rec = 1;
816
818
  const model = { id: 'task:' + item.id, sit, quote, chg, conf: 0, cta, opts: opts.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: !!x.outward })), why: '', outbox,
817
- outboxReason: OUT_REASON, agentAsk, agentExtras: () => extras, agentAfterOpts: after };
819
+ outboxReason: OUT_REASON, agentAsk, agentExtras: () => extras, agentBeforeOpts: before };
818
820
  const run = {}; opts.forEach((x) => { if (x.run) run[x.id] = x.run; });
819
821
  return { model, run };
820
822
  }
821
823
 
824
+ /* ── the draft and the conversation with the agent (c3-work-agent amendment 15, mockup "Agent Draft Conversation") ──────── */
825
+
826
+ /** The turns on an item's task, oldest first: the PM's ask, then the log (drafts, notes, edits, the post). A task from before
827
+ * the log has its current draft only. Each draft or edit carries its round label and body. */
828
+ function turnsOf(item) {
829
+ const a = item.agent || {}; const d = a.draft || {};
830
+ let log = Array.isArray(a.log) && a.log.length ? a.log.slice() : d.body ? [{ who: 'agent', kind: 'draft', at: null, text: d.summary || '', body: d.body }] : [];
831
+ // an edit made before the log, or the draft body that the log does not hold: the current text is the last version
832
+ const last = [...log].reverse().find((t) => t.body);
833
+ if (d.body && last && last.body !== d.body) log.push({ who: 'pm', kind: 'edit', at: d.edited_at || null, body: d.body });
834
+ const ask = a.instruction || a.suggest || '';
835
+ const out = ask ? [{ who: 'pm', kind: 'ask', at: null, text: ask }] : [];
836
+ let round = 0;
837
+ for (const t of log) out.push(t.body ? { ...t, label: t.who === 'agent' ? AG.round(++round) : AG.yours, round: t.who === 'agent' ? round : 0 } : t);
838
+ return out;
839
+ }
840
+
841
+ const copyText = (text) => { try { navigator.clipboard.writeText(String(text || '')).then(() => say({ text: AG.copied, timeout: 2000 }), () => {}); } catch { /* no clipboard */ } };
842
+
843
+ /** The draft as a document card: what it is, which version, Open and Copy, the document clipped with Show all. */
844
+ function draftCard(inst, item, k, busy) {
845
+ const a = item.agent; const d = a.draft || {};
846
+ const cur = [...turnsOf(item)].reverse().find((t) => t.body) || { label: AG.round(1), at: d.at || null };
847
+ const all = inst.editing.has('all:' + item.id);
848
+ const long = String(d.body || '').length > 900 || String(d.body || '').split('\n').length > 16;
849
+ const body = el('div', { class: 'ds-md wk-agent-md' + (long && !all ? ' wk-doc-clip' : ''), 'data-pm-agent-doc-body': item.id }, toDom(parseMd(String(d.body || '')), document));
850
+ return el('section', { class: 'card wk-doc', 'data-pm-agent-doc': item.id, 'aria-label': 'The draft' },
851
+ el('div', { class: 'wk-doc-h' },
852
+ el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : 'draft'), busy ? el('span', { class: 'tag' }, AG.beingRedrafted) : null,
853
+ el('b', { class: 'wk-doc-to' }, isMine(item) ? 'Draft for you' : AG.draftSheet(k)),
854
+ el('span', { class: 'chip2', 'data-pm-agent-version': item.id }, c3ic(cur.kind === 'edit' ? 'edit' : 'spark', 'sm'), cur.label + (cur.at ? ' · ' + hhmm(cur.at) : ''))),
855
+ el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.open], () => openDraft(item, inst)),
856
+ c3btn('chip2 lk', { 'data-pm-agent-copy': item.id }, [c3ic('copy', 'sm'), AG.copy], () => copyText(d.body)))),
857
+ body,
858
+ long ? c3btn('chip2 lk', { 'data-pm-agent-all': item.id, 'aria-expanded': String(all) }, [c3ic(all ? 'zout' : 'zin', 'sm'), all ? AG.showLess : AG.showAll],
859
+ () => { if (all) inst.editing.delete('all:' + item.id); else inst.editing.add('all:' + item.id); draw(inst); }) : null);
860
+ }
861
+
862
+ /** The conversation: one line per turn, then the reply box that sends the draft back (only while a draft waits). */
863
+ function conversation(inst, item, busy, w) {
864
+ const a = item.agent; const turns = turnsOf(item); const n = turns.length;
865
+ const only = n <= 2 && !busy; const show = !only || inst.editing.has('thr:' + item.id);
866
+ const cur = [...turns].reverse().find((t) => t.body);
867
+ const lead = { ask: isMine(item) ? 'added' : 'asked', note: 'sent it back', edit: 'edited the draft', post: 'posted it' };
868
+ const line = (t) => {
869
+ const you = t.who !== 'agent';
870
+ const text = t.kind === 'note' ? el('blockquote', { class: 'qd' }, t.text || '') : t.kind === 'draft' ? el('p', null, t.text || 'Drafted it') : t.kind === 'ask' ? el('p', null, t.text) : null;
871
+ const ver = !t.body ? null : t === cur ? el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : AG.theDraft)
872
+ : c3btn('chip2 lk', { 'data-pm-agent-round': String(t.round || 0) }, [c3ic('eye', 'sm'), t.kind === 'edit' ? AG.viewEdit : AG.viewRound(t.round)], () => openDraft(item, inst, false, t));
873
+ return el('li', { class: you ? 'you' : 'ag' }, c3ic(you ? 'user' : 'spark', 'sm'),
874
+ el('div', { class: 'wk-turn' }, el('div', { class: 'wk-who' }, el('b', null, you ? 'You' : 'Agent'), el('span', { class: 'dim mono' }, [t.at ? hhmm(t.at) : '', lead[t.kind] || ''].filter(Boolean).join(' · '))),
875
+ text, ver ? el('div', { class: 'chips2' }, ver) : null));
876
+ };
877
+ const head = el('div', { class: 'wk-conv-h' }, el('span', { class: 'dim mono' }, c3ic('reply', 'sm'), AG.conv), el('span', { class: 'tag' }, String(n)),
878
+ only ? c3btn('chip2 lk', { 'data-pm-agent-thread': item.id, 'aria-expanded': String(show) }, [c3ic(show ? 'zout' : 'zin', 'sm'), show ? AG.hide : AG.show],
879
+ () => { if (show) inst.editing.delete('thr:' + item.id); else inst.editing.add('thr:' + item.id); draw(inst); }) : null);
880
+ let reply = null;
881
+ if (a.state === 'ready') {
882
+ const ta = el('textarea', { rows: '2', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id });
883
+ ta.value = inst.asks.get('note:' + item.id) || '';
884
+ ta.addEventListener('input', () => inst.asks.set('note:' + item.id, ta.value));
885
+ const send = () => {
886
+ const v = ta.value.trim();
887
+ if (!v) { say({ text: AG.needNote, timeout: 3500 }); ta.focus(); return; }
888
+ inst.asks.delete('note:' + item.id);
889
+ agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'revise', note: v }, AG.sentBack);
890
+ };
891
+ ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } });
892
+ reply = el('div', { class: 'wk-reply', 'data-pm-agent-note': item.id }, ta,
893
+ el('div', { class: 'wk-reply-f' }, el('span', { class: 'dim' }, c3ic('lock', 'sm'), ' ' + AG.replyWhat(isMine(item) ? '' : w)),
894
+ button({ label: AG.send, variant: 'primary', size: 'sm', onClick: send, attrs: { 'data-pm-agent-send': item.id } })));
895
+ }
896
+ return el('div', { class: 'wk-conv', 'data-pm-agent-conv': item.id }, head, show ? el('ol', { class: 'chgl wk-thr', role: 'list' }, ...turns.map(line)) : null, reply);
897
+ }
898
+
822
899
  function openAgentSession(id, item) {
823
900
  try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
824
901
  }
@@ -828,9 +905,9 @@ const draftMd = (text) => el('div', { class: 'ds-md wk-agent-md' }, toDom(parseM
828
905
 
829
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
830
907
  * as the draft, so Post sends exactly that text. */
831
- function openDraft(item, inst, editing) {
832
- const a = item.agent || {}; const d = a.draft || {};
833
- const canEdit = !!inst && a.state === 'ready';
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;
834
911
  const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
835
912
  area.value = d.body || '';
836
913
  const save = async () => {
@@ -848,12 +925,12 @@ function openDraft(item, inst, editing) {
848
925
  button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
849
926
  : el('div', { class: 'wk-agent-doc' }, draftMd(d.body || ''));
850
927
  const body = el('div', { class: 'wk-agent-sheet' },
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),
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),
852
929
  d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
853
930
  text,
854
- el('div', { class: 'fold' }, c3ic('info', 'sm'), ' ' + (a.state === 'sent' ? 'This is the text that was posted.' : 'Nothing is in ' + whereOf(item) + ' yet. Post it from the item, after you read it.')));
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.')));
855
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;
856
- openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
933
+ openSheet({ title: ver ? ver.label + (ver.at ? ' · ' + hhmm(ver.at) : '') : AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
857
934
  if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
858
935
  }
859
936
 
@@ -863,7 +940,7 @@ function caseFile(inst, item, lens) {
863
940
  const root = renderCase(model, {
864
941
  picked: inst.picks.get(item.id), outbox: !!model.outbox, noConf: !item.mock_row,
865
942
  renderQuote: item.agent && !item.mock_row ? draftMd : undefined,
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(); } },
943
+ onPick: (id) => { inst.picks.set(item.id, id); },
867
944
  onConfirm: ({ option }) => {
868
945
  const fn = run[option.id];
869
946
  if (fn) fn(rowOf(inst, item));
@@ -875,7 +952,8 @@ function caseFile(inst, item, lens) {
875
952
  const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
876
953
  if (item.kind === 'offline') extras.push(offlineBlock(inst, item));
877
954
  if (model.agentExtras) extras.push(...model.agentExtras());
878
- if (model.agentAfterOpts) { const sl = root.querySelector('.c3slide'); if (sl) sl.after(model.agentAfterOpts); }
955
+ if (model.agentBeforeOpts) { const op = root.querySelector('.opts'); if (op) op.before(model.agentBeforeOpts); }
956
+ if (item.agent && !item.mock_row && ['ready', 'sending', 'sent', 'queued', 'working'].includes(item.agent.state)) root.classList.add('wk-ag');
879
957
  if (rs && rs.phase === 'work') {
880
958
  extras.push(el('div', { class: 'c3ag', 'data-ag': item.id }, el('div', { class: 'fold a', role: 'status' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), rs.steps[Math.min(rs.i, rs.steps.length - 1)] + ' · ' + (Math.min(rs.i, rs.steps.length - 1) + 1) + ' of ' + rs.steps.length,
881
959
  el('button', { type: 'button', class: 'chip2 lk', 'data-pm-agent-stop': item.id, onclick: () => { inst.runs.delete(item.id); say({ text: 'Stopped. Nothing was kept.', timeout: 2500 }); draw(inst); } }, c3ic('stop', 'sm'), 'stop'))));
@@ -253,8 +253,7 @@ export const COPY = {
253
253
  sendNote: 'Run it again with this note',
254
254
  cancel: 'Cancel',
255
255
  noteNeeded: 'Write what should change, in 1 to 500 characters.',
256
- revisions: (n, max) => `${n} of ${max} revisions used`,
257
- noMoreRevisions: 'This job used all its revisions. Do the rest yourself.',
256
+ revisions: (n) => `Sent back ${n} time${n === 1 ? '' : 's'}`,
258
257
  openConversation: 'Open the conversation ↗',
259
258
  openDraft: 'Open the draft ↗',
260
259
  openSent: 'Open what was sent ↗',
@@ -40,7 +40,6 @@ const D = COPY.day;
40
40
  const A = COPY.act;
41
41
  const SLOW_MS = 30000;
42
42
  const FAST_MS = 4000;
43
- const MAX_REVISIONS = 3;
44
43
  const SCOPED = ['slack', 'jira', 'confluence'];
45
44
  const NARROW = 860; // below this width of the section the detail goes under the list (components.css @container td-day)
46
45
 
@@ -999,7 +998,7 @@ function jobCard(ctx, j) {
999
998
  t.draft.summary ? el('p', null, t.draft.summary) : null,
1000
999
  t.draft.body ? el('pre', { class: 'td-day__draft' }, t.draft.body) : null) : null;
1001
1000
  const acts = [];
1002
- const canRevise = t && (j.status === 'review' || j.status === 'done') && revs < MAX_REVISIONS;
1001
+ const canRevise = t && (j.status === 'review' || j.status === 'done') ;
1003
1002
  if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: tagged(D.approve, 'you'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
1004
1003
  if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: tagged(D.approveAuto, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
1005
1004
  if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: tagged(D.revise, 'agent'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
@@ -1029,7 +1028,7 @@ function jobCard(ctx, j) {
1029
1028
  t && j.status === 'failed' && t.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'alert' }, D.failedWhy(t.error)) : null,
1030
1029
  j.status === 'sent' && t && ((t.sent && t.sent.at) || (t.decided && t.decided.at)) ? note(D.sentAt(hhmm((t.sent && t.sent.at) || t.decided.at))) : null,
1031
1030
  draft,
1032
- revs ? note(D.revisions(revs, MAX_REVISIONS) + (revs >= MAX_REVISIONS ? ' ' + D.noMoreRevisions : '')) : null,
1031
+ revs ? note(D.revisions(revs)) : null,
1033
1032
  el('div', { class: 'td-day__acts' }, ...acts.filter(Boolean)),
1034
1033
  form);
1035
1034
  }
@@ -32,7 +32,7 @@ Procedure:
32
32
  3. If MODE is revise:
33
33
  a. Read the draft so far and the LAST note of the PM in the output of `day show`. The note says what to change. Earlier notes still hold unless the last one cancels them.
34
34
  b. Change the draft to address the note, and only what the note asks. Read a source again only if the note needs it.
35
- c. Submit the new draft as in step 2d. State in the summary what changed. The workspace keeps at most 3 revisions. Make this one count.
35
+ c. Submit the new draft as in step 2d. State in the summary what changed. The PM can send the draft back as often as they want. Make this one count.
36
36
  4. If MODE is send:
37
37
  a. The task is approved. Read the approved draft and its link in the output of `day show`. The approved draft is the only text you may send. Do not read it as a draft to improve.
38
38
  A draft whose summary starts "The PM's own" was written by the PM in the Your day card. Post it exactly as written, as the PM, to the place the summary and the link name. Do not add a greeting, a signature or a line of your own.