create-pmos 0.217.0 → 0.219.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.217.0",
3
+ "version": "0.219.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@b12dcf9
1
+ kernel-source: pmos@16fef7c
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:36:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
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:36:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
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:36:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -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
@@ -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 };
@@ -681,7 +681,7 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null,
681
681
  sp.summarisedTickets = Number.isInteger(n.tickets) ? n.tickets : null;
682
682
  sp.closed = sp.state === 'closed' && n.closed ? { ...n.closed } : null;
683
683
  sp.frozen = sp.state === 'closed' && n.state === 'closed';
684
- sp.brief = sp.state !== 'closed' && n.brief ? n.brief : null;
684
+ sp.brief = sp.state === 'active' && n.brief ? n.brief : null; // the sprint brief is the active sprint's only
685
685
  sp.hit = sp.state === 'closed' && n.hit ? n.hit : null;
686
686
  sp.got = sp.state === 'closed' && n.got ? n.got : null;
687
687
  } else {
@@ -691,7 +691,10 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null,
691
691
  }
692
692
  }
693
693
  const order = { active: 0, future: 1, closed: 2 };
694
- const sprints = [...byName.values()].sort((a, b) => ((order[a.state] ?? 3) - (order[b.state] ?? 3)) || a.name.localeCompare(b.name));
694
+ // Two active sprints: the one Jira dated and gave an id comes first, then the larger one; the name only breaks a tie.
695
+ const real = (sp) => (sp.start || sp.end || wt.some((t) => t && t.sprint && t.sprint.id && String(t.sprint.name) === sp.name) ? 1 : 0);
696
+ const sprints = [...byName.values()].sort((a, b) => ((order[a.state] ?? 3) - (order[b.state] ?? 3))
697
+ || (a.state === 'active' ? (real(b) - real(a)) || (b.issues.length - a.issues.length) : 0) || a.name.localeCompare(b.name));
695
698
  for (const sp of sprints) sp.issues.sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }));
696
699
  // Counted by PMOS, never written by the agent: each sprint's points and status flow, the velocity of the last four closed
697
700
  // sprints, and the active sprint's pace and forecast (the sprint brief).
@@ -89,20 +89,24 @@ function fixVersionsOf(v) {
89
89
  return [...new Set(list.map((x) => clip(nameOf(x), 80)).filter(Boolean))];
90
90
  }
91
91
  const SPRINT_STATES = ['active', 'future', 'closed'];
92
- /** One sprint, from an object { name, state } or a bare name. */
92
+ /** A name that says "no sprint" ("Future", "Backlog", "No sprint"): a ticket planned for later, which is in no sprint. */
93
+ const NOT_A_SPRINT = /^(future|next|later|planned|unplanned|unscheduled|backlog|icebox|none|null|no sprint|not in a sprint|unassigned)( sprints?)?$/i;
94
+ /** One sprint, from an object { name, state } or a bare name. Only Jira's own state makes a sprint active: a bare name or an
95
+ * unknown state is a planned sprint, and a "Future" or "Backlog" with no Jira id is no sprint (the backlog). */
93
96
  function sprintOne(v) {
94
97
  if (!v) return null;
95
- if (isStr(v)) { const name = clip(oneLine(v), 80); return name ? { name, state: 'active' } : null; }
98
+ if (isStr(v)) { const name = clip(oneLine(v), 80); return name && !NOT_A_SPRINT.test(name) ? { name, state: 'future' } : null; }
96
99
  if (typeof v !== 'object') return null;
97
100
  const name = clip(oneLine(v.name), 80);
98
101
  const state = String(v.state || '').toLowerCase();
99
102
  if (!name) return null;
100
- const out = { name, state: SPRINT_STATES.includes(state) ? state : 'active' };
103
+ const out = { name, state: SPRINT_STATES.includes(state) ? state : 'future' };
101
104
  if ((Number.isInteger(v.id) && v.id > 0) || (isStr(v.id) && /^\d{1,12}$/.test(v.id))) out.id = String(v.id);
102
105
  for (const [k, from] of [['start', ['startDate', 'start']], ['end', ['endDate', 'end']], ['completed', ['completeDate', 'completed']]]) {
103
106
  const d = dayOf(from.map((f) => v[f]).find((x) => x != null));
104
107
  if (d) out[k] = d;
105
108
  }
109
+ if (!out.id && NOT_A_SPRINT.test(name)) return null;
106
110
  const goal = clip(oneLine(v.goal), 300); // Jira's sprint goal, often empty or broad (sprint-goal-summary)
107
111
  if (goal) out.goal = goal;
108
112
  return out;
@@ -235,7 +239,8 @@ function readSnapshot(root, initiative) {
235
239
  let data;
236
240
  try { data = JSON.parse(m[1]); } catch { return null; }
237
241
  const tickets = new Map();
238
- for (const r of Array.isArray(data.tickets) ? data.tickets : []) if (r && KEY_RE.test(String(r.key || ''))) tickets.set(r.key, r);
242
+ // The sprint is read again, so a reading saved before the "Future" fix is no longer an active sprint.
243
+ for (const r of Array.isArray(data.tickets) ? data.tickets : []) if (r && KEY_RE.test(String(r.key || ''))) tickets.set(r.key, { ...r, sprint: r.sprint ? sprintOne(r.sprint) : null });
239
244
  return { fetchedAt: isStr(data.fetched_at) ? data.fetched_at : null, query: isStr(data.query) ? data.query : null, tickets };
240
245
  }
241
246
 
@@ -415,7 +420,7 @@ function noteOne(x) {
415
420
  const note = { name, state, goal, summary, themes };
416
421
  // The sprint brief (sprint-goal-summary, mockup-c3-sprint-brief): where the sprint stands, the issues to resolve with a proposed
417
422
  // plan each, what is done and the asks for the PM. Written by the agent from the tickets; optional, so a note without one still loads.
418
- if (state !== 'closed' && x.brief && typeof x.brief === 'object') {
423
+ if (state === 'active' && x.brief && typeof x.brief === 'object') {
419
424
  const r = briefOf(x.brief, name);
420
425
  if (r.error) return r;
421
426
  note.brief = r.brief;
@@ -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
  }
@@ -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.217.0",
3
+ "version": "0.219.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.217.0",
9
+ "version": "0.219.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.217.0",
3
+ "version": "0.219.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",
@@ -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.
@@ -27,7 +27,7 @@ Procedure:
27
27
  - `type`: the issue type's name, for example `Epic`, `Story`, `Task` or `Bug`.
28
28
  - `parent`: the parent issue's key (the epic of a story), or `null`.
29
29
  - `category`: the status category, `new`, `indeterminate` or `done`.
30
- - `sprint`: `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}` for the active sprint, `"future"` for a planned one, or `null` when the issue is in the backlog. Copy `id`, `start`, `end`, `completed` (a closed sprint's completion date) and `goal` (the sprint goal, as Jira writes it) from Jira's sprint field; leave out any that Jira does not give.
30
+ - `sprint`: Jira's sprint, as `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}`. Copy `state` as Jira gives it: `"active"`, `"future"` (a planned sprint) or `"closed"`. Use `null` when the issue is in no sprint: never write a made-up sprint such as `"Future"` or `"Backlog"` for it. Copy `id`, `start`, `end`, `completed` (a closed sprint's completion date) and `goal` (the sprint goal, as Jira writes it) from Jira's sprint field; leave out any that Jira does not give.
31
31
  - `points`: the story points as a number, or `null` when none are set.
32
32
  - `resolved`: the resolution date as `YYYY-MM-DD`, or `null` when the issue is not resolved.
33
33
  - `subtasks`: `{"done": 2, "total": 5}`.