create-pmos 0.184.0 → 0.186.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.184.0",
3
+ "version": "0.186.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@14635e7
1
+ kernel-source: pmos@4d636bc
2
2
  assembled: 2026-10-13
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-13T04:43:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
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-13T04:43:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
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-13T04:43:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -88,6 +88,7 @@ const TAB = '[data-rs-tab]';
88
88
  await page.click('[data-rs-card] [data-rs-act="setup"]');
89
89
  ok('Set up posts ONE setup with the ask and the proposal', !!(await until(() => sent('/api/report-setups/setup').length === 1)) && sent('/api/report-setups/setup')[0].body.ask && sent('/api/report-setups/setup')[0].body.proposal && /pricing/i.test(sent('/api/report-setups/setup')[0].body.proposal.name), JSON.stringify(sent('/api/report-setups/setup')).slice(0, 300));
90
90
  ok('the set-up appears marked New with its first draft in Documents', !!(await until(async () => (await page.$$('[data-rs-reg]')).length === 4)) && /New/.test(await text('[data-rs-setups]')) && (await page.$$('[data-rs-doc]')).length === 7);
91
+ ok('the Set up toast says the first draft is ready and offers Open', /first draft is ready/.test(await text('[data-c3-toast]')) && !!(await page.$('[data-c3-toast-action]')), await text('[data-c3-toast]'));
91
92
  ok('nothing else was posted: no copy, no issue', sent('/api/docs/create-copy').length === 0 && sent('/api/report/issue').length === 0);
92
93
 
93
94
  /* ── found: Set up, Not a report with Undo ── */
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "My work: every Jira ticket and WBS item assigned to you, ranked, briefed to an agent, approved in Agenda"
4
- timestamp: 2026-10-12
4
+ timestamp: 2026-10-13
5
5
  ---
6
6
 
7
7
  # My work
@@ -77,7 +77,7 @@ The Work page holds all the work that is yours:
77
77
  - every open Jira ticket and WBS item assigned to you, from the same read as My work;
78
78
  - the items today's morning brief suggests for you. They are flagged, so they are in Today.
79
79
 
80
- A row of assigned work shows its Jira key (a link to the ticket) or **WBS** and the repository, and the agent's suggestion when the read has one. A suggested row shows **Suggested** and the reason. An agent at work on a ticket puts it in Waiting; a ticket in progress, or with a draft ready for you, is flagged. A ticket that is done in Jira leaves the page.
80
+ A row of assigned work shows its Jira key (a link to the ticket) or **WBS** and the repository. When the read has a suggestion, the first chip on the row shows **Agent can:** and the suggestion. This chip is only a suggestion. No agent works on the item until you start it. A suggested row shows **Suggested** and the reason. An agent at work on a ticket puts it in Waiting; a ticket in progress, or with a draft ready for you, is flagged. A ticket that is done in Jira leaves the page.
81
81
 
82
82
  Every action works on these rows. The first action writes the item to your own list, with its source. **Drop** takes it off your Work page only; Jira and the Board keep it, and it does not come back.
83
83
 
@@ -96,6 +96,34 @@ The six views follow the OmniFocus style. The **Work** tab badge counts Inbox pl
96
96
 
97
97
  An item goes to the first view that fits these rules, in this order. Parked is Someday. Waiting on someone is Waiting. A defer date after today is Upcoming. Flagged is Today. Due today or earlier is Today. A later due date is Upcoming. An undated item whose defer date has arrived is Today. No date is Inbox.
98
98
 
99
+ ### Let the agent draft an item
100
+
101
+ Open an assigned item that shows **Agent can:**. The case file shows **What the agent will do**.
102
+
103
+ 1. To change the instruction, select **Change what the agent does** and type a new one.
104
+ 2. Select **Let the agent draft this** and hold to confirm. This adds one job to today's day run, with the instruction.
105
+
106
+ The row chip then shows the agent's state:
107
+
108
+ | Chip | What it means |
109
+ |---|---|
110
+ | **Agent: queued** | The job waits for a free place in the day run. |
111
+ | **Agent: working** | An agent session drafts the item. Select **stop** in the case file to end it. |
112
+ | **Agent: draft ready** | The draft waits for you. |
113
+ | **Agent: posting** | You approved the draft. A session posts exactly that text. |
114
+ | **Posted to** the item | The draft went out. Select **Open what was posted** to see it. |
115
+ | **Agent stopped** | The job failed or you stopped it. The case file gives the reason. Select **Try again** to run it again. |
116
+
117
+ The page reads the state again every 15 seconds while an agent is queued, working or posting.
118
+
119
+ When the draft is ready, the case file quotes it. Select **Open the draft** to read all of it. Then choose one option:
120
+
121
+ - **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
122
+ - **Send it back with a note** returns the draft to the agent. Type what to change and press Enter.
123
+ - **Keep it as a draft** leaves the draft as it is.
124
+
125
+ The agent only drafts. It never posts to Jira until you approve the draft.
126
+
99
127
  ### Defer and flag
100
128
 
101
129
  A defer date means "not before this day". The item stays in Upcoming and shows **Not before** with the date. On that day it moves to Today by itself. A due date is when the work must be done. A defer date is when you start.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Set up a report by asking, and the report finder"
4
4
  description: "How to ask PMOS to draft a report you send, how the report finder finds reports you already send, how to set one up, and how a cycle becomes a document you issue."
5
- timestamp: 2026-10-12
5
+ timestamp: 2026-10-13
6
6
  ---
7
7
 
8
8
  # Set up a report by asking, and the report finder
@@ -17,6 +17,7 @@ Open the **Reports** tab of the console. It has four parts: Ask PMOS, Found in y
17
17
  2. Press **Set up**. PMOS reads your earlier copies and learns the sections. This takes a minute. You can leave the page. **Cancel** stops it.
18
18
  3. PMOS shows its proposal: what it found, the proposed report, what it learned from, and the fixes it will apply.
19
19
  4. Press **Set up** to accept the proposal. Press **Change** to say what to change, for example "Draft it on Tuesday instead". PMOS revises the proposal. Press **Cancel** to drop it.
20
+ 5. PMOS writes the first draft at once. Press **Open** in the message to read it. You do not wait for the day of the schedule.
20
21
 
21
22
  A proposal shows the name, the purpose, the audience, the output, the schedule, the sources and the sections in order. Change the output with the buttons: Google Slides, Google Doc, Google Sheets, Confluence or PDF. PMOS shows the report in the same format in the sheet.
22
23
 
@@ -39,6 +40,7 @@ The most likely finding also shows on the Briefing as **Report found**. Press **
39
40
  **Set up** has one row for each report: its source, output, schedule, next draft and last issue. Each row has these buttons:
40
41
 
41
42
  - **Open** opens the current draft.
43
+ - **Draft it now** shows in place of Open when the report has no draft. PMOS writes the draft of the next cycle and opens it.
42
44
  - **All cycles** lists every cycle of the report.
43
45
  - **Pause** stops the drafts. **Resume** starts them again.
44
46
 
@@ -46,7 +48,7 @@ The most likely finding also shows on the Briefing as **Report found**. Press **
46
48
 
47
49
  ## What PMOS does and does not do
48
50
 
49
- - PMOS drafts on the day of the schedule. It sends nothing.
51
+ - PMOS drafts the first cycle when you press Set up, and each next cycle on the day of the schedule. It sends nothing.
50
52
  - PMOS writes to Google or to Confluence only when you press **Create the copy** or **Update the copy**.
51
53
  - A download happens only when you press a download item.
52
54
  - You issue a report. An issued report is frozen.
@@ -17,7 +17,7 @@
17
17
  * addBriefed(run, spec) one task the PM briefed in My work (initiative my-work, PRD R5)
18
18
  * addSend(run, spec, now) the PM's own text, written in the card, to post as written (today-act-prepare)
19
19
  * toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
20
- * started · deliver · decide · sent · fail · sessionEnded · sessionStalled
20
+ * started · deliver · decide · sent · fail · stop · sessionEnded · sessionStalled
21
21
  * stage(run, brief) reading | working | review | done
22
22
  * checkDue(run, {settings, now, events}) · checkStarted · checkDone
23
23
  *
@@ -272,6 +272,17 @@ function fail(run, taskId, reason, now) {
272
272
  });
273
273
  }
274
274
 
275
+ /** The PM stopped a task that has not delivered (initiative c3-work-agent): waiting or working → failed with `reason`, so it
276
+ * never starts and frees its place. The caller ends its session. A retry runs it again in a NEW session. */
277
+ function stop(run, taskId, reason, now) {
278
+ return withTask(run, taskId, (t) => {
279
+ if (t.status !== 'waiting' && t.status !== 'working') return `task ${taskId} is ${t.status} — only a waiting or working task can be stopped`;
280
+ const why = text(reason) || 'Stopped by the PM.';
281
+ t.status = 'failed'; t.error = why.slice(0, 240); t.failed_at = iso(now); keepSession(t); t.prev_session = null;
282
+ return null;
283
+ });
284
+ }
285
+
275
286
  /** A session ended. A task it was working on without delivering, or sending without reporting, has failed. */
276
287
  function sessionEnded(run, sessionId, now) {
277
288
  if (!run) return { ok: false, error: 'no day run today' };
@@ -388,5 +399,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
388
399
  }
389
400
 
390
401
  module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
391
- newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
402
+ newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, stop, sessionEnded, sessionStalled, stage, intervalMin,
392
403
  checkDue, checkStarted, checkDone };
@@ -194,7 +194,8 @@ function stateOf(item, presses, runs) {
194
194
  if (!last || last.action === 'reopen') return { state: 'brief', sub: null, task: null, instruction: null };
195
195
  if (last.action === 'handle') return { state: 'yours', sub: null, task: null, instruction: null, decided_at: last.at };
196
196
  const t = taskOf(runs, last);
197
- const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null } : null;
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,
198
+ sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0 } : null;
198
199
  const base = { task, instruction: last.instruction, decided_at: last.at };
199
200
  if (!t) return { state: 'agent', sub: 'waiting', ...base };
200
201
  if (t.status === 'review') return { state: 'ready', sub: 'review', ...base };
@@ -7,6 +7,10 @@
7
7
  * POST /api/my-work/act { action: brief|handle|reopen, item, instruction?, ws }
8
8
  * → { ok, item, task? } the PM's press; a brief adds one day task (R5)
9
9
  * POST /api/my-work/refresh { ws } → { ok, session } Read again: starts the my-work-read skill
10
+ * POST /api/my-work/decide { item, action: approve|revise|retry, note? }
11
+ * → { ok, item, task } the PM's decision on the agent's draft (initiative c3-work-agent):
12
+ * approve posts exactly the draft, revise sends it back with the note, retry runs a failed task again
13
+ * POST /api/my-work/stop { item } → { ok, item } ends the item's working or queued task and offers the suggestion again
10
14
  *
11
15
  * STATE. The latest read is <runtime>/my-work/read.json and the presses are <runtime>/my-work/actions.jsonl, append-only.
12
16
  * Both stay in the per-user runtime state and never land in a repository: they hold paraphrases of Jira comments. The
@@ -23,6 +27,7 @@ const guard = require('./pm-guard.js');
23
27
  const myWork = require('./my-work.js');
24
28
  const boardFold = require('./board-fold.js');
25
29
  const offloop = require('./offloop.js');
30
+ const dayRun = require('./day-run.js');
26
31
  const { localDate } = require('./brief.js');
27
32
 
28
33
  const dirOf = (ctx) => path.join(ctx.runtimeDir, 'my-work');
@@ -92,7 +97,7 @@ async function handle(req, res, url, ctx) {
92
97
  ctx.json(res, await listForAsync(ctx, now));
93
98
  return true;
94
99
  }
95
- if (!['/api/my-work/read', '/api/my-work/act', '/api/my-work/refresh'].includes(p)) return false;
100
+ if (!['/api/my-work/read', '/api/my-work/act', '/api/my-work/refresh', '/api/my-work/decide', '/api/my-work/stop'].includes(p)) return false;
96
101
  if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
97
102
  const got = await ctx.readJson(req);
98
103
  if (!got.ok) { ctx.json(res, { ok: false, error: 'invalid JSON body' }, 400); return true; }
@@ -116,6 +121,8 @@ async function handle(req, res, url, ctx) {
116
121
  return true;
117
122
  }
118
123
 
124
+ if (p === '/api/my-work/decide' || p === '/api/my-work/stop') { taskPress(ctx, res, p.slice('/api/my-work/'.length), body, now); return true; }
125
+
119
126
  // act
120
127
  const c = myWork.checkAct(body, now);
121
128
  if (!c.ok) { ctx.json(res, { ok: false, error: c.error }, 400); return true; }
@@ -137,4 +144,56 @@ async function handle(req, res, url, ctx) {
137
144
  return true;
138
145
  }
139
146
 
147
+ /* ── the agent's task of an item (initiative c3-work-agent) ─────────────────────────────────────────────────────── */
148
+
149
+ const DAY_MS = 86400000;
150
+ const nextDay = (d) => new Date(Date.parse(d + 'T00:00:00Z') + DAY_MS).toISOString().slice(0, 10);
151
+
152
+ /** The day run that holds the task of the item's last brief: the brief's day (UTC, as the press records it) up to today. */
153
+ function runOfTask(ctx, brief, today) {
154
+ const id = myWork.taskSpec({ id: brief.item }, brief).id;
155
+ for (let d = String(brief.at).slice(0, 10), n = 0; d <= today && n < 400; d = nextDay(d), n++) {
156
+ let run = null; try { run = ctx.readDayRun ? ctx.readDayRun(d) : null; } catch { run = null; }
157
+ if (run && dayRun.findTask(run, id)) return { run, id };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /** decide (approve | revise | retry) or stop, on the task of the item's last brief. */
163
+ function taskPress(ctx, res, verb, body, now) {
164
+ const item = String((body && body.item) || '');
165
+ if (!myWork.ID_RE.test(item)) { ctx.json(res, { ok: false, error: 'item must be an item id like jira:ATL-491' }, 400); return; }
166
+ const list = listFor(ctx, now);
167
+ const it = list.items.find((x) => x.id === item);
168
+ if (!it) { ctx.json(res, { ok: false, error: `no item ${item} is assigned to you` }, 404); return; }
169
+ const brief = loadActions(ctx).filter((a) => a.item === item).pop();
170
+ if (!brief || brief.action !== 'brief') { ctx.json(res, { ok: false, error: 'no agent works on this item — let the agent draft it first' }, 409); return; }
171
+ const found = runOfTask(ctx, brief, list.today);
172
+ if (verb === 'decide') {
173
+ if (!found) { ctx.json(res, { ok: false, error: 'the agent has not started this item yet' }, 409); return; }
174
+ const r = dayRun.decide(found.run, found.id, { action: body.action, note: body.note }, now);
175
+ if (!r.ok) { ctx.json(res, { ok: false, error: r.error }, 400); return; }
176
+ const run = ctx.startDayTasks ? ctx.startDayTasks(r.run, now) : r.run;
177
+ if (!ctx.writeDayRun || !ctx.writeDayRun(run)) { ctx.json(res, { ok: false, error: 'could not save — nothing changed' }, 500); return; }
178
+ const after = listFor(ctx, Date.now()).items.find((x) => x.id === item) || null;
179
+ ctx.json(res, { ok: true, item: after, task: dayRun.findTask(run, found.id) });
180
+ return;
181
+ }
182
+ // stop: only a task that has not delivered yet; its session ends and the item offers the suggestion again
183
+ if (!(it.state === 'agent' && ['waiting', 'working', 'failed'].includes(it.sub))) { ctx.json(res, { ok: false, error: `the item is ${it.state === 'ready' ? 'a draft waiting for you' : it.state} — there is nothing to stop` }, 409); return; }
184
+ if (found) {
185
+ const t = dayRun.findTask(found.run, found.id);
186
+ if (t && (t.status === 'waiting' || t.status === 'working')) {
187
+ if (t.session_id && ctx.endSession) { try { ctx.endSession(t.session_id); } catch { /* the session already ended */ } }
188
+ const f = dayRun.stop(found.run, found.id, 'You stopped it.', now);
189
+ if (!f.ok) { ctx.json(res, { ok: false, error: f.error }, 409); return; }
190
+ if (!ctx.writeDayRun || !ctx.writeDayRun(f.run)) { ctx.json(res, { ok: false, error: 'could not save — nothing changed' }, 500); return; }
191
+ }
192
+ }
193
+ try { fs.mkdirSync(dirOf(ctx), { recursive: true }); fs.appendFileSync(actionsFile(ctx), JSON.stringify({ action: 'reopen', item, at: new Date(now).toISOString() }) + '\n'); }
194
+ catch { ctx.json(res, { ok: false, error: 'could not save your press' }, 500); return; }
195
+ const after = listFor(ctx, Date.now()).items.find((x) => x.id === item) || null;
196
+ ctx.json(res, { ok: true, item: after });
197
+ }
198
+
140
199
  module.exports = { prefixes: ['/api/my-work'], handle, listFor, listForAsync };
@@ -9,7 +9,8 @@
9
9
  * POST /api/report-setups/ask { ws, text } → { ok, id } starts the report-setup run (headless)
10
10
  * POST /api/report-setups/change { ws, id, text } → { ok } a correction to a proposal; a second run revises it
11
11
  * POST /api/report-setups/cancel { ws, id } → { ok }
12
- * POST /api/report-setups/setup { ws, ask? | finding?, proposal? } → { ok, setup, first_draft } THE press that sets a report up
12
+ * POST /api/report-setups/setup { ws, ask? | finding?, proposal? } → { ok, setup, first_draft, drafted } THE press that sets a report up; drafts the first cycle
13
+ * POST /api/report-setups/draft { ws, id } → { ok, drafted: { slot, path }, id } Draft it now (the next cycle)
13
14
  * POST /api/report-setups/dismiss { ws, finding } → { ok } "Not a report"
14
15
  * POST /api/report-setups/pause | resume { ws, id } → { ok, setup }
15
16
  * POST /api/report-setups/from-dashboard { ws, key, name, output, schedule, audience } → { ok, setup }
@@ -39,13 +40,14 @@ const RN = require('./report-needs.js');
39
40
  const RC = require('./report-checks.js');
40
41
  const RX = require('./report-export.js');
41
42
  const RF = require('./report-finder.js');
43
+ const RD = require('./report-draft.js');
42
44
  const logic = require('./schedule-logic.js');
43
45
  const docSync = require('./doc-sync.js');
44
46
  const headless = require('./headless.js');
45
47
 
46
48
  const text = (v) => (typeof v === 'string' ? v : '');
47
49
  const bad = (status, error) => ({ status, body: { ok: false, error } });
48
- const POSTS = ['ask', 'change', 'cancel', 'setup', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder'];
50
+ const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder'];
49
51
  const MADE_BY = ['schedule', 'you', 'agent'];
50
52
 
51
53
  const runs = new Map(); // ask id → session id, for the report-setup runs this server started
@@ -331,14 +333,30 @@ async function handle(req, res, url, ctx) {
331
333
  if (!proposal) return send(bad(400, 'say which request or finding to set up, or give the proposal'));
332
334
  const r = RS.setUp(root, { ...proposal, ...(from ? { from } : {}) }, opts);
333
335
  if (!r.ok) return fail(r);
334
- const docsOf = { reports: [] };
335
- return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), docsOf, new Map()), first_draft: r.first_draft } });
336
+ // The first cycle is drafted now, at the slot of its first scheduled draft, so the PM has a document to read the moment it is set up
337
+ // (the approved mockup's "Sample draft made when you set up"). The schedule later redrafts the same slot. A failed draft leaves the set-up
338
+ // in place and says why; Draft it now on the row tries again.
339
+ const drafted = firstDraft(root, r, g.actor);
340
+ if (drafted.path) ctx.invalidateNeedsAgg && ctx.invalidateNeedsAgg();
341
+ return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), documentsOf(ctx, w), copyIndex(root)), first_draft: r.first_draft, drafted } });
336
342
  }
337
343
  if (sub === 'dismiss') {
338
344
  const r = RS.dismissFinding(file, { id: text(body.finding), by: g.actor });
339
345
  if (!r.ok) return fail(r);
340
346
  return send({ status: 200, body: { ok: true } });
341
347
  }
348
+ if (sub === 'draft') {
349
+ // Draft it now: the next cycle of a set-up report, written at the slot of its next scheduled draft. A page write, like Draft now on the report.
350
+ const s = RS.fold(RS.readLines(file)).setups.get(text(body.id));
351
+ if (!s || s.status === 'removed') return send(bad(404, 'no such report'));
352
+ if (s.source !== 'work') return send(bad(400, 'a dashboard report is drafted from its dashboard on its day'));
353
+ let next = null; try { next = RS.nextDraftAt(s.schedule, s.last_run, new Date()); } catch { next = null; }
354
+ const day = next ? `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}-${String(next.getDate()).padStart(2, '0')}` : null; // the local day, as setUp names a slot
355
+ const d = draftCycle(root, s.id, day, g.actor);
356
+ if (d.error) return send(bad(500, 'the draft was not written: ' + d.error));
357
+ if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
358
+ return send({ status: 200, body: { ok: true, drafted: d, id: s.id + '/' + d.slot } });
359
+ }
342
360
  if (sub === 'pause' || sub === 'resume') {
343
361
  const r = RS[sub](root, text(body.id), opts);
344
362
  if (!r.ok) return fail(r);
@@ -365,4 +383,19 @@ async function handle(req, res, url, ctx) {
365
383
  return send(bad(404, 'no such report route'));
366
384
  }
367
385
 
386
+ /** Draft a report's cycle now through the one writer (report-draft.js writeDraft). → { slot, path } | { error } */
387
+ function draftCycle(root, cadence, slot, actor) {
388
+ const d = RD.draftOf(root, { cadence, slot: slot || undefined });
389
+ if (!d || d.ok === false) return { error: (d && d.error) || 'the drafter answered nothing' };
390
+ // A draft already at that slot is the answer: open it, never overwrite its notes.
391
+ try { if (d.path && fs.statSync(path.join(path.resolve(String(root)), d.path)).isFile()) return { slot: d.slot, path: d.path, existing: true }; } catch { /* not there yet */ }
392
+ const w = RD.writeDraft(root, d, { actor, fresh: true });
393
+ if (!w || w.ok === false) return { error: (w && w.error) || 'the writer answered nothing' };
394
+ return { slot: d.slot, path: w.path };
395
+ }
396
+ function firstDraft(root, r, actor) {
397
+ if (!r.setup || r.setup.source !== 'work') return { error: 'a dashboard report is drafted from its dashboard on its day' };
398
+ return draftCycle(root, r.setup.id, r.first_draft && r.first_draft.slot, actor);
399
+ }
400
+
368
401
  module.exports = { prefixes: ['/api/report-setups', '/api/report-documents', '/api/report-export'], handle, tick, ensureMigrated, documentsOf, listAnswer, copyOf };
@@ -6,6 +6,7 @@
6
6
  * addable(virtuals, itemLines) → [item] the ones the PM's own ledger has never held (not added, not dropped)
7
7
  * createdEvent(v) → event the `created` line that puts one in the PM's ledger on its first press
8
8
  * overlay(item, byId) → item a ledger copy with the source's current status, suggestion and state
9
+ * agentOf(x) → agent what the agent does on a my-work item (initiative c3-work-agent), or null
9
10
  *
10
11
  * PURE: no file, process or clock. A virtual item has the Work item's fields (title, due, priority, waiting_on, flagged) and
11
12
  * says where it came from: origin jira | wbs | brief, plus key, url, status, suggest, why, my_work_state, repo. Its `source`
@@ -13,17 +14,40 @@
13
14
 
14
15
  const TIER_PRIORITY = { P1: 'P1', P2: 'P2', P3: 'P3' };
15
16
  const AGENT = 'an agent';
16
- const SHOWN = ['origin', 'key', 'url', 'status', 'suggest', 'why', 'my_work_state', 'repo', 'initiative'];
17
+ const SHOWN = ['origin', 'key', 'url', 'status', 'suggest', 'why', 'my_work_state', 'repo', 'initiative', 'agent'];
18
+
19
+ /* The agent on a my-work item (initiative c3-work-agent): its state, read from lib/my-work.js stateOf.
20
+ * suggest nothing started (the suggestion, when there is one, is the offer) queued · working · sending · failed the day task
21
+ * ready the draft waits for the PM sent the PM posted it null the PM took the item, or there is nothing to offer */
22
+ const SUB_STATE = { waiting: 'queued', working: 'working', sending: 'sending', failed: 'failed' };
23
+ function agentOf(x) {
24
+ if (!x) return null;
25
+ const t = x.task || null;
26
+ let state = null;
27
+ if (x.state === 'brief') state = x.suggest ? 'suggest' : null;
28
+ else if (x.state === 'agent') state = SUB_STATE[x.sub] || 'queued';
29
+ else if (x.state === 'ready') state = 'ready';
30
+ else if (x.state === 'done' && x.sub === 'sent') state = 'sent';
31
+ if (!state) return null;
32
+ return {
33
+ state, suggest: x.suggest || null, instruction: x.instruction || null,
34
+ draft: t && (t.draft || t.body) ? { summary: t.draft || null, body: t.body || null } : null,
35
+ sent: t && t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null,
36
+ error: t ? t.error || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null,
37
+ };
38
+ }
17
39
 
18
40
  function fromMyWork(items) {
19
41
  const out = [];
20
42
  for (const x of items || []) {
21
- if (!x || !x.id || !x.title || x.state === 'done' || x.category === 'done') continue;
43
+ if (!x || !x.id || !x.title) continue;
44
+ // a posted item is done in my-work: it adds no new row, but the PM's own copy of it still shows that it was posted
45
+ if (x.state === 'done' || x.category === 'done') { if (x.state === 'done' && x.sub === 'sent') out.push({ id: String(x.id), overlay_only: true, agent: agentOf(x) }); continue; }
22
46
  const v = {
23
47
  id: String(x.id), title: String(x.title), source: x.source === 'wbs' ? 'wbs' : 'jira', origin: x.source === 'wbs' ? 'wbs' : 'jira', assigned: true,
24
48
  due: x.due || null, priority: TIER_PRIORITY[x.wsjf && x.wsjf.tier] || null, waiting_on: null, job: null,
25
49
  key: x.key || null, url: x.url || null, status: x.status || null, suggest: x.suggest || null, my_work_state: x.state || null,
26
- repo: x.repo || null, initiative: x.initiative || null,
50
+ repo: x.repo || null, initiative: x.initiative || null, agent: agentOf(x),
27
51
  };
28
52
  if (x.state === 'agent') v.waiting_on = AGENT;
29
53
  if (x.state === 'ready' || x.category === 'doing') v.flagged = true;
@@ -49,11 +73,11 @@ function fromBrief(brief, todayIso) {
49
73
  /** The virtual items whose id the ledger never held: an item the PM added, pressed or dropped is the ledger's from then on. */
50
74
  function addable(virtuals, itemLines) {
51
75
  const held = new Set((itemLines || []).filter((l) => l && l.id).map((l) => l.id));
52
- return (virtuals || []).filter((v) => !held.has(v.id));
76
+ return (virtuals || []).filter((v) => !v.overlay_only && !held.has(v.id));
53
77
  }
54
78
 
55
79
  function createdEvent(v) {
56
- const { id, source, ...fields } = v;
80
+ const { id, source, agent, ...fields } = v; // the agent state is read on every list, never stored
57
81
  return { event: 'created', id, fields, source };
58
82
  }
59
83
 
@@ -66,4 +90,4 @@ function overlay(item, byId) {
66
90
  return out;
67
91
  }
68
92
 
69
- module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, AGENT };
93
+ module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, agentOf, AGENT };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.184.0",
3
+ "version": "0.186.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.184.0",
9
+ "version": "0.186.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.184.0",
3
+ "version": "0.186.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",
@@ -186,7 +186,8 @@ function setupFrom(b) {
186
186
  startDocs(s, F);
187
187
  if (F) findings = findings.filter((f) => f.id !== F.id);
188
188
  if (askRec) asks = asks.filter((a) => a.id !== askRec.id);
189
- return json({ ok: true, setup: setupView(s), first_draft: { slot: next || '', at: next ? next + 'T08:00:00.000Z' : null, day: next || '' } });
189
+ const first = DOCS.filter((d) => d.setup === id && d.state === 'draft')[0];
190
+ return json({ ok: true, setup: setupView(s), first_draft: { slot: next || '', at: next ? next + 'T08:00:00.000Z' : null, day: next || '' }, drafted: first ? { slot: first.slot, path: first.path } : { error: 'nothing to draft' } });
190
191
  }
191
192
  function post(path, b) {
192
193
  const k = path.replace('/api/report-setups/', '');
@@ -198,6 +199,7 @@ function post(path, b) {
198
199
  }
199
200
  if (k === 'cancel') { asks = asks.filter((a) => a.id !== b.id); return json({ ok: true }); }
200
201
  if (k === 'setup') return setupFrom(b);
202
+ if (k === 'draft') { const s = SETUPS.find((x) => x.id === b.id); if (!s) return json({ ok: false, error: 'no such report' }, 404); const d = startDocs(s, null)[0]; return json({ ok: true, drafted: { slot: d.slot, path: d.path }, id: s.id + '/' + d.slot }); }
201
203
  if (k === 'dismiss') { findings = findings.filter((f) => f.id !== b.finding); dismissed.add(b.finding); return json({ ok: true }); }
202
204
  if (k === 'pause' || k === 'resume') { const s = SETUPS.find((x) => x.id === b.id); if (!s) return json({ ok: false, error: 'no such set-up' }, 404); s.status = k === 'pause' ? 'paused' : 'active'; return json({ ok: true, setup: setupView(s) }); }
203
205
  if (k === 'from-dashboard') {
@@ -14,7 +14,7 @@ import { openSheet, closeSheet } from './c3-sheet.js';
14
14
  import { openDocSheet } from './c3-doc-sheet.js';
15
15
  import { toast } from './c3-toast.js';
16
16
  import { FORMATS, OUTPUTS, formatOf, ensureReportCss } from './c3-doc-formats.js';
17
- import { RS, onReports, loadReports, askPmos, changeIt, cancelAsk, setupFrom, dismissLater, pauseResume, fromDashboard, goTo, evidenceKind, normProposal, scheduleLabel, nextDraftDate, dayText, docsOf, outChip, copyChip, srcChip } from './c3-report-store.js';
17
+ import { RS, onReports, loadReports, askPmos, changeIt, cancelAsk, setupFrom, draftNow, dismissLater, pauseResume, fromDashboard, goTo, evidenceKind, normProposal, scheduleLabel, nextDraftDate, dayText, docsOf, outChip, copyChip, srcChip } from './c3-report-store.js';
18
18
 
19
19
  const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
20
20
  /** 'Wed 21 Oct 2026' for a day, 'Oct 2026' for a month, any other slot as it is. */
@@ -122,6 +122,7 @@ function askCard(a, redo) {
122
122
  }
123
123
 
124
124
  /* ── a report document in the document sheet ── */
125
+ RS.openDoc = (d) => openReportDoc(d); // the store's toasts open a document through this
125
126
  export function openReportDoc(d, mode) {
126
127
  ensureReportCss();
127
128
  const setup = RS.setups.find((x) => x.id === d.setup) || null; const cyc = slotText(d.slot);
@@ -205,7 +206,10 @@ export function mountReportSetup(host, ctx) {
205
206
  el('span', { class: 'chips2' }, stag(d), copyChip(d.output, d.copy), pbtn('ghost', 'Open', 'out', { onclick: () => openReportDoc(d) })))));
206
207
  function openSetup(s) {
207
208
  const L = docsOf(RS.docs, s.id); const d = L.find((x) => x.state === 'draft') || L[0];
208
- if (d) openReportDoc(d); else toast('No draft yet. The first draft is ' + (s.next_draft ? dayText(s.next_draft) : 'when you confirm the day') + '.');
209
+ if (d) { openReportDoc(d); return; }
210
+ if (s.source !== 'work' || s.status !== 'active') { toast('No draft yet. The first draft is ' + (s.next_draft ? dayText(s.next_draft) : 'when you confirm the day') + '.'); return; }
211
+ toast('Drafting ' + s.name + ' now. Nothing is sent.');
212
+ draftNow(s).then((nd) => { if (nd) openReportDoc(nd); });
209
213
  }
210
214
  function setupRow(s) {
211
215
  const paused = s.status === 'paused'; const cy = docsOf(RS.docs, s.id); const n = s.cycles != null ? s.cycles : cy.length; const open = RS.cyc.has(s.id);
@@ -215,7 +219,8 @@ export function mountReportSetup(host, ctx) {
215
219
  chip(icon('cal', 'sm'), s.label || scheduleLabel(s.schedule)), paused ? null : chip(icon('clock', 'sm'), 'Next draft ' + (s.next_draft ? dayText(s.next_draft) : 'not set yet')),
216
220
  chip(icon('mail', 'sm'), 'Last issued ' + (s.last_issued ? dayText(s.last_issued) : 'not yet'))),
217
221
  open ? cycRows(s) : null),
218
- el('div', { class: 'acts' }, pbtn('outline', 'Open', 'out', { 'data-rs-act': 'open-reg', onclick: () => openSetup(s) }),
222
+ el('div', { class: 'acts' }, cy.length || paused || s.source !== 'work' ? pbtn('outline', 'Open', 'out', { 'data-rs-act': 'open-reg', onclick: () => openSetup(s) })
223
+ : pbtn('outline', 'Draft it now', 'spark', { 'data-rs-act': 'draft-now', onclick: () => openSetup(s) }),
219
224
  pbtn('ghost', 'All cycles (' + n + ')', 'fold', { 'data-rs-act': 'cyc', 'aria-expanded': String(open), onclick: () => { if (RS.cyc.has(s.id)) RS.cyc.delete(s.id); else RS.cyc.add(s.id); drawSetups(); } }),
220
225
  pbtn('ghost', paused ? 'Resume' : 'Pause', paused ? 'play' : 'pause', { 'data-rs-act': 'pause', onclick: () => pauseResume(s, !paused) })));
221
226
  }
@@ -114,9 +114,22 @@ export async function setupFrom(body) {
114
114
  if (r.setup && r.setup.id) RS.newIds.add(r.setup.id);
115
115
  await loadReports(RS.ws, true);
116
116
  const fd = r.first_draft; const first = (r.setup && r.setup.next_draft) || (fd && typeof fd === 'object' ? fd.day || fd.slot : fd);
117
- toast('Set up. First draft ' + (first ? dayText(first) : 'once you confirm the day') + '. Nothing is sent.');
117
+ if (r.drafted && r.drafted.path) openDraftToast(r.setup.id, 'Set up. The first draft is ready to read. Nothing is sent.');
118
+ else toast('Set up. First draft ' + (first ? dayText(first) : 'once you confirm the day') + '. Nothing is sent.' + (r.drafted && r.drafted.error ? ' It could not be drafted yet: ' + r.drafted.error + '.' : ''));
118
119
  return r;
119
120
  }
121
+ /* the page that draws documents sets RS.openDoc, so a toast here can open one without this store importing the page */
122
+ function openDraftToast(id, text) {
123
+ const d = (RS.docs || []).filter((x) => x.setup === id && x.state === 'draft').sort((a, b) => (a.slot < b.slot ? 1 : -1))[0];
124
+ if (d && typeof RS.openDoc === 'function') toast({ text, actionLabel: 'Open', timeout: 8000, onAction: () => RS.openDoc(d) });
125
+ else toast(text);
126
+ }
127
+ /** Draft it now: the next cycle of a set-up report. → the new draft document, or null. */
128
+ export async function draftNow(setup) {
129
+ const r = await post('draft', { id: setup.id }); if (!r || !r.ok) { fail(r, 'It was not drafted'); return null; }
130
+ await loadReports(RS.ws, true);
131
+ return (RS.docs || []).find((x) => x.setup === setup.id && x.slot === r.drafted.slot) || null;
132
+ }
120
133
  /** Not a report: the finding leaves the list at once, the POST waits five seconds, and Undo cancels it. */
121
134
  export function dismissLater(id, onChange) {
122
135
  RS.hidden.add(id); emit(); if (onChange) onChange();
@@ -1879,6 +1879,15 @@
1879
1879
  .c3 .wkq{display:flex;flex-wrap:wrap;gap:var(--sp-2);align-items:center;margin-top:var(--sp-2)}
1880
1880
  .c3 .wkq-f{display:inline-flex;gap:6px;align-items:center;font-size:var(--fs-2)}
1881
1881
  .c3 .wkq input[type=date],.c3 .wkq-f input[type=text]{font:inherit;font-size:var(--fs-2);padding:4px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px;min-width:0;max-width:150px}
1882
+ /* c3-work-agent: what the agent will do, the note when the PM sends a draft back, the draft sheet */
1883
+ .c3 .wk-agent-ask{display:flex;gap:6px;align-items:center;margin-top:6px}
1884
+ .c3 .wk-agent-ask input{flex:1;min-width:0;font:inherit;font-size:var(--fs-2);padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px}
1885
+ .c3 .wk-agent-ask[hidden]{display:none}
1886
+ .c3 .wk-agent-sheet{display:grid;gap:var(--sp-3)}
1887
+ .c3 .wk-agent-will{font-size:var(--fs-2)}
1888
+ .c3 .wk-agent-sum{margin:0}
1889
+ .c3 .wk-agent-body{--tc:var(--acc)}
1890
+ .c3 .wk-agent-body .sit{white-space:pre-line}
1882
1891
  .c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
1883
1892
  .c3 .tag.wk-nb{background:var(--muted);color:var(--foreground)}
1884
1893
  .c3 .tag.wk-on{background:var(--acc);color:var(--background)}
@@ -34,6 +34,7 @@ import { makeXp, attachExpand } from './c3-expand.js';
34
34
  import { attachSwipe } from './c3-swipe.js';
35
35
  import { toast } from './c3-toast.js';
36
36
  import { openSheet } from './c3-sheet.js';
37
+ import { c3btn } from './page-agent.js';
37
38
  import { onBriefing, loadBriefing } from './shell-ticker.js';
38
39
  import { reviewSection } from './c3-brief.js';
39
40
  import { VIEWS, REVIEW_DAYS, workPlace, arrives, buildWork, addIso, daysBetween, untouchedDays } from './work-place.js';
@@ -165,7 +166,7 @@ export const COPY = Object.freeze({
165
166
  outcome: 'What came out of it (optional)', notYet: 'Not yet:', laterToday: 'Later today', tomorrow: 'Tomorrow', nextWeek: 'Next week',
166
167
  looksDone: 'Looks done?', confirm: 'Confirm done', flag: 'Flag', unflag: 'Unflag', notBefore: 'Not before', someday: 'Someday', bringBack: 'Bring back',
167
168
  waitingField: 'Waiting on', waitingWho: 'Who?', stopWaiting: 'Stop waiting', flagged: 'Flagged',
168
- notBeforeChip: (d) => 'Not before ' + d, advances: (t) => 'Advances team task ' + t, suggested: 'Suggested', agentSuggests: (t) => 'Agent: ' + t, moved: (n) => `Moved ${n}×`,
169
+ notBeforeChip: (d) => 'Not before ' + d, advances: (t) => 'Advances team task ' + t, suggested: 'Suggested', agentSuggests: (t) => 'Agent can: ' + t, moved: (n) => `Moved ${n}×`,
169
170
  overdue: (n) => (n === 1 ? 'Overdue since yesterday' : `${n} days overdue`), dueToday: 'Due today', dueOn: (d) => 'Due ' + d,
170
171
  waitingSince: (w, since, ago) => `Waiting for ${w}` + (since ? ` since ${since}` : '') + (ago ? ` (${ago})` : ''), repeats: (r) => 'Repeats ' + String(r).toLowerCase(),
171
172
  /* Review (PRD R7) */
@@ -284,7 +285,7 @@ export function mountPmWork(host, ctx) {
284
285
  if (ctx && ctx.FIXTURE) return;
285
286
  ensureC3Css();
286
287
  inst = { host, ctx: ctx || {}, root: el('div', { class: 'c3 calwrap wk', 'data-pm-work': '' }), data: null, base: null, ov: newOv(), lens: 'today',
287
- opens: new Set(), picks: new Map(), editing: new Set(), planning: new Set(), runs: new Map(), trays: new Set(), cases: new Set(), timers: new Set(), hidden: new Set(),
288
+ opens: new Set(), picks: new Map(), asks: new Map(), editing: new Set(), planning: new Set(), runs: new Map(), trays: new Set(), cases: new Set(), timers: new Set(), hidden: new Set(),
288
289
  focEdit: new Set(), rv: { ids: null, handled: new Set(), changing: false }, decisions: 0, drafts: null, draftsSig: '', draftsHost: el('div', { 'data-pm-review-drafts': '' }) };
289
290
  inst.body = el('div', { class: 'calbody pmw-body' });
290
291
  inst.bar = el('div', { class: 'calbar' });
@@ -328,6 +329,9 @@ async function load(inst) {
328
329
  inst.data = inst.base && DEMO() ? applyOv(inst.base, inst.ov) : inst.base;
329
330
  if (!inst.data) say({ text: (r && r.error) || COPY.readError, tone: 'error' });
330
331
  if (insts.get(inst.host) === inst) draw(inst);
332
+ /* an agent that is queued, working or posting changes on its own: read again until it stops (c3-work-agent) */
333
+ const busy = inst.data && !DEMO() && (inst.data.items || []).some((i) => i.agent && AGENT_BUSY.includes(i.agent.state));
334
+ if (busy && !inst.agentPoll) { inst.agentPoll = true; later(inst, () => { inst.agentPoll = false; load(inst); }, AGENT_POLL_MS); }
331
335
  }
332
336
 
333
337
  async function press(inst, path, body, { quiet } = {}) {
@@ -628,6 +632,7 @@ function itemCase(inst, item, lens) {
628
632
  const ix = item.ix || {}; const mock = item.mock_row === true;
629
633
  const j = item.job && jobById(inst, item.job);
630
634
  const L = lensOptions(inst, item, lens);
635
+ if (!mock && item.agent && item.kind !== 'offline') return agentCase(inst, item, L);
631
636
  const jm = j && (j.mock_name || j.want);
632
637
  const evidence = ix.chg ? ix.chg.slice() : (() => {
633
638
  const c = [];
@@ -691,12 +696,131 @@ function startRun(inst, item, intent) {
691
696
  }
692
697
 
693
698
 
699
+ /* ── the agent on a Jira or WBS item (initiative c3-work-agent, planning/prd/c3-work-agent-prd.md) ───────────────────
700
+ * item.agent = { state: suggest|queued|working|ready|sending|sent|failed, suggest, instruction, draft: { summary, body }, sent, error, … }
701
+ * from lib/work-assigned.js. The suggestion is an option the PM picks; the day task does the work; nothing goes outward
702
+ * until the PM holds Post (POST /api/my-work/decide approve). Stop ends a task that has not delivered (POST /api/my-work/stop). */
703
+ const AGENT_BUSY = ['queued', 'working', 'sending'];
704
+ const AGENT_POLL_MS = 15000;
705
+ const hhmm = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '' : String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); };
706
+ const minsAgo = (iso) => { const t = Date.parse(iso); if (!Number.isFinite(t)) return ''; const m = Math.max(0, Math.round((Date.now() - t) / 60000)); return m < 1 ? 'just now' : m < 60 ? m + ' min ago' : Math.floor(m / 60) + ' h ago'; };
707
+ const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : 'its source');
708
+ const AG = {
709
+ can: (t) => 'Agent can: ' + t, queued: 'Agent: queued', working: (ago) => 'Agent: working' + (ago ? ' · started ' + ago : ''), ready: 'Agent: draft ready',
710
+ sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (why) => 'Agent stopped' + (why ? ': ' + why : ''),
711
+ sitSuggest: (w) => `The agent can draft this for you. You read it here first. Nothing goes to ${w} until you post it.`,
712
+ sitQueued: 'Queued. The agent starts as soon as a session is free. You can close this page.',
713
+ sitWorking: 'The agent is working on it. You can close this page: the item keeps its state.',
714
+ sitReady: (at, w) => `Draft ready${at ? ' at ' + at : ''}. Nothing went to ${w}.`,
715
+ sitSending: (k) => `Posting to ${k}. A session does exactly the step you approved and nothing else.`,
716
+ sitSent: (at, w) => `You posted it to ${w}${at ? ' at ' + at : ''}.`,
717
+ sitFailed: (why) => `The agent stopped${why ? ': ' + why : '.'} Nothing was sent.`,
718
+ willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
719
+ 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',
720
+ noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
721
+ handed: (w) => `Handed to the agent. Nothing goes to ${w} until you post it.`, stopped: 'Stopped. Nothing was kept.', kept: 'Kept as a draft. Post it when you are ready.',
722
+ posting: 'Approved. A session posts exactly this text.', sentBack: 'Sent back with your note. The agent drafts it again.', retried: 'The agent tries again.',
723
+ needNote: 'Say what to change first: type a note, then send it back.', link: 'Open what was posted',
724
+ };
725
+
726
+ function agentChip(item) {
727
+ const a = item.agent; if (!a || item.mock_row) return null;
728
+ const st = (cls, ic, text, spin) => el('span', { class: cls, 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), text);
729
+ if (a.state === 'suggest') return st('chip2', 'spark', AG.can(a.instruction || a.suggest || ''));
730
+ if (a.state === 'queued') return st('st', 'clock', AG.queued);
731
+ if (a.state === 'working') return st('st c3strip', 'spark', AG.working(minsAgo(a.started_at)), true);
732
+ if (a.state === 'ready') return st('st ok', 'check', AG.ready);
733
+ if (a.state === 'sending') return st('st c3strip', 'out', AG.sending, true);
734
+ if (a.state === 'sent') return st('st ok', 'out', AG.sent(whereOf(item), a.sent && a.sent.at ? hhmm(a.sent.at) : ''));
735
+ if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.error));
736
+ return null;
737
+ }
738
+
739
+ async function agentPress(inst, path, body, text) {
740
+ const r = await pmPost(path, body);
741
+ if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return false; }
742
+ inst.picks.delete(body.item);
743
+ say({ text, timeout: 4000 });
744
+ await load(inst);
745
+ return true;
746
+ }
747
+
748
+ function agentCase(inst, item, L) {
749
+ const a = item.agent; const w = whereOf(item); const k = item.key || item.title;
750
+ const o = (id, l, f, run, extra) => ({ id, l, rec: 0, fx: f, run, ...(extra || {}) });
751
+ const own = L.opts.slice(0, 2).map((x) => ({ ...x, rec: 0 }));
752
+ const ask = () => inst.asks.get(item.id) || a.instruction || a.suggest || '';
753
+ let sit = ''; let opts = []; let quote = ''; const chg = []; let outbox = false; let cta = ''; let agentAsk = null; let after = null;
754
+ const extras = [];
755
+ 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 + ' ',
756
+ 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))));
757
+ if (a.state === 'suggest' || a.state === 'failed') {
758
+ sit = a.state === 'failed' ? AG.sitFailed(a.error) : AG.sitSuggest(w);
759
+ const brief = () => agentPress(inst, '/api/my-work/act', { action: 'brief', item: item.id, instruction: ask(), ws: inst.ctx.ws }, AG.handed(w));
760
+ const retry = () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'retry' }, AG.retried);
761
+ opts = [o('agent', a.state === 'failed' ? AG.tryAgain : AG.draftIt, [fx('spark', 'Drafts it here in PMOS', 'ok'), fx('lock', 'Nothing goes to ' + w, 'ok')], a.state === 'failed' ? retry : brief), ...own];
762
+ cta = 'Slide to let the agent draft';
763
+ if (a.state === 'suggest') {
764
+ const editing = inst.editing.has('ask:' + item.id);
765
+ const input = el('input', { type: 'text', value: ask(), 'aria-label': AG.askLabel, 'data-pm-agent-ask': item.id, autocomplete: 'off' });
766
+ const save = () => { const v = input.value.trim(); if (v) inst.asks.set(item.id, v); inst.editing.delete('ask:' + item.id); draw(inst); };
767
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) save(); });
768
+ extras.push(el('div', { class: 'c3ag', 'data-pm-agent-will': item.id }, el('div', { class: 'dim mono wk-agent-will' }, c3ic('spark', 'sm'), ' ' + AG.willDo + (editing ? '' : ask())),
769
+ editing ? el('div', { class: 'form wk-agent-ask' }, input, button({ label: COPY.save, variant: 'outline', size: 'sm', onClick: save })) : null));
770
+ 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); });
771
+ }
772
+ } else if (a.state === 'queued') {
773
+ sit = AG.sitQueued; extras.push(strip(false, 'clock', 'Queued' + (a.decided_at ? ' at ' + hhmm(a.decided_at) : '')));
774
+ } else if (a.state === 'working') {
775
+ sit = AG.sitWorking; extras.push(strip(true, 'spark', 'Working' + (a.started_at ? ' · started ' + minsAgo(a.started_at) : '')));
776
+ } else if (a.state === 'ready') {
777
+ const d = a.draft || {};
778
+ quote = d.body || ''; sit = AG.sitReady('', w);
779
+ if (d.summary) chg.push(['doc', d.summary, 'draft']);
780
+ if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
781
+ outbox = true; cta = 'Hold to post';
782
+ const note = el('input', { type: 'text', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id, autocomplete: 'off' });
783
+ 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); };
784
+ note.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) back(); });
785
+ opts = [
786
+ 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 }),
787
+ o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
788
+ o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: AG.kept, timeout: 3000 })),
789
+ ];
790
+ const noteBox = el('div', { class: 'form wk-agent-ask', 'data-pm-agent-note': item.id }, note); noteBox.hidden = inst.picks.get(item.id) !== 'back';
791
+ extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item))));
792
+ after = noteBox;
793
+ } else if (a.state === 'sending') {
794
+ sit = AG.sitSending(k);
795
+ } else if (a.state === 'sent') {
796
+ sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
797
+ opts = L.opts.map((x) => ({ ...x }));
798
+ 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)),
799
+ 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));
800
+ }
801
+ if (opts.length) opts[0].rec = 1;
802
+ 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,
803
+ outboxReason: OUT_REASON, agentAsk, agentExtras: () => extras, agentAfterOpts: after };
804
+ const run = {}; opts.forEach((x) => { if (x.run) run[x.id] = x.run; });
805
+ return { model, run };
806
+ }
807
+
808
+ function openDraft(item) {
809
+ const a = item.agent || {}; const d = a.draft || {};
810
+ const body = el('div', { class: 'wk-agent-sheet' },
811
+ el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
812
+ d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
813
+ el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || '')),
814
+ 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.')));
815
+ openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body });
816
+ }
817
+
694
818
  /** The case file element of an item: c3-case.js plus what is Work's own (offline work, the shared actions, the steps, the agent tray, the edit fields). */
695
819
  function caseFile(inst, item, lens) {
696
820
  const { model, run } = itemCase(inst, item, lens);
697
821
  const root = renderCase(model, {
698
- picked: inst.picks.get(item.id), outbox: false,
699
- onPick: (id) => inst.picks.set(item.id, id),
822
+ picked: inst.picks.get(item.id), outbox: !!model.outbox,
823
+ 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(); } },
700
824
  onConfirm: ({ option }) => {
701
825
  const fn = run[option.id];
702
826
  if (fn) fn(rowOf(inst, item));
@@ -711,6 +835,8 @@ function caseFile(inst, item, lens) {
711
835
  const rs = inst.runs.get(item.id);
712
836
  const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
713
837
  if (item.kind === 'offline') extras.push(offlineBlock(inst, item));
838
+ if (model.agentExtras) extras.push(...model.agentExtras());
839
+ if (model.agentAfterOpts) { const sl = root.querySelector('.c3slide'); if (sl) sl.after(model.agentAfterOpts); }
714
840
  if (rs && rs.phase === 'work') {
715
841
  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,
716
842
  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'))));
@@ -725,7 +851,7 @@ function caseFile(inst, item, lens) {
725
851
  c3ib('Speak is off on this line: nothing on this page records. Type the reply here.', 'mic', { 'data-off': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the reply here.', timeout: 4200 }) }, 'sm'),
726
852
  c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
727
853
  }
728
- const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
854
+ const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent') && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
729
855
  if (showSteps) extras.push(stepsBlock(inst, item));
730
856
  const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
731
857
  if (anchor) anchor.after(...extras); else root.prepend(...extras);
@@ -734,8 +860,9 @@ function caseFile(inst, item, lens) {
734
860
  const form = editForm(inst, item); form.hidden = !inst.editing.has(item.id);
735
861
  const edit = el('button', { type: 'button', class: 'chip2 lk', 'aria-expanded': String(inst.editing.has(item.id)), 'data-pm-edit-toggle': item.id,
736
862
  onclick: () => { const on = !inst.editing.has(item.id); if (on) inst.editing.add(item.id); else inst.editing.delete(item.id); form.hidden = !on; edit.setAttribute('aria-expanded', String(on)); } }, c3ic('edit', 'sm'), 'Edit details');
737
- const ft = root.querySelector('.cft');
738
- if (ft) ft.append(edit); else root.append(el('div', { class: 'cft' }, edit));
863
+ const ft = root.querySelector('.cft') || root.appendChild(el('div', { class: 'cft' }));
864
+ if (model.agentAsk) ft.append(model.agentAsk);
865
+ ft.append(edit);
739
866
  root.append(form);
740
867
  return root;
741
868
  }
@@ -805,7 +932,8 @@ function workChips(inst, item, o = {}) {
805
932
  const label = item.origin === 'jira' ? 'Jira ' + (item.key || '') : 'WBS' + (item.repo ? ' ' + item.repo : '') + (item.key ? ' ' + item.key : '');
806
933
  const attrs = { class: 'tag', 'data-pm-origin': item.origin, title: item.status || '' };
807
934
  c.push(item.url ? el('a', { ...attrs, href: item.url, target: '_blank', rel: 'noopener', onclick: (e) => e.stopPropagation() }, label.trim()) : el('span', attrs, label.trim()));
808
- if (item.suggest) c.push(el('span', { class: 'chip2', 'data-pm-suggest': item.id }, c3ic('spark', 'sm'), COPY.agentSuggests(item.suggest)));
935
+ // the agent's state is the row's first chip (itemRow), so quiet pages keep it in sight
936
+ if (item.suggest && !item.agent) c.push(el('span', { class: 'chip2', 'data-pm-suggest': item.id }, c3ic('spark', 'sm'), COPY.agentSuggests(item.suggest)));
809
937
  } else if (item.origin === 'brief') {
810
938
  c.push(el('span', { class: 'tag', 'data-pm-origin': 'brief', title: item.why || '' }, c3ic('spark', 'sm'), COPY.suggested));
811
939
  if (item.why) c.push(el('span', { class: 'chip2', 'data-pm-why': item.id }, item.why));
@@ -833,7 +961,9 @@ function itemRow(inst, item, lens, o = {}) {
833
961
  if (rs && rs.phase === 'work') chips.push(el('span', { class: 'st c3strip', 'data-sxs': item.id }, 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));
834
962
  else if (rs && rs.phase === 'done') chips.push(el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'agent: result ready'));
835
963
  else if (inst.planning.has(item.id)) chips.push(el('span', { class: 'st' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), 'planning the steps'));
836
- if (item.waiting_on) {
964
+ const ac = !mock && agentChip(item);
965
+ if (ac) chips.push(ac);
966
+ if (item.waiting_on && !(ac && item.waiting_on === 'an agent')) {
837
967
  const since = item.waiting_since ? String(item.waiting_since).slice(0, 10) : '';
838
968
  chips.push(el('span', { class: 'chip2', 'data-pm-waiting-chip': item.id }, c3ic('wait', 'sm'), COPY.waitingSince(item.waiting_on, since ? dayLabel(since) : '', since ? ago(since, today) : '')));
839
969
  }
@@ -8988,6 +8988,12 @@ async function handleRequest(req, res) {
8988
8988
  headlessBusy: (id) => { const x = sessions.get(String(id || '')); return !!(x && x.headless && x.child); },
8989
8989
  // initiative my-work: every registered repository, the day runs, and the two presses that start sessions
8990
8990
  workspaceList: () => [...workspaces.values()], readDayRun, myWorkBrief, myWorkRead,
8991
+ // c3-work-agent: the PM's decision on the agent's draft and Stop, on the day run that holds the item's task
8992
+ writeDayRun, startDayTasks, endSession: (id) => {
8993
+ const x = sessions.get(String(id || '')); if (!x) return;
8994
+ if (x.child) stopHeadless(x);
8995
+ else if (x.pty) { if (x.tmux) tmuxKillSession(x.tmux); try { x.pty.kill(); } catch { /* already ended */ } }
8996
+ },
8991
8997
  // workspace-data-freshness: the source table, the facts rebuild and Refresh now's skill sessions
8992
8998
  freshness, factsRefresh: (root) => factsAsync.refresh(root, { force: true }), startSkill: startSkillSession,
8993
8999
  // c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Initiative c3-work-agent: the agent on a Jira or WBS item in Work (the suggestion as an option, queued, working, the draft, Send back, Stop, Post).",
3
+ "modules": ["work-agent.test.js"],
4
+ "panels": ["pm-c3-work-agent-test.js"],
5
+ "seconds": { "work-agent.test.js": 8, "pm-c3-work-agent-test.js": 30 }
6
+ }