create-pmos 0.208.0 → 0.210.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.208.0",
3
+ "version": "0.210.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@709d321
1
+ kernel-source: pmos@97b9da7
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-14T01:38:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T01:55:32Z"}
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-14T01:38:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T01:55:32Z"}
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-14T01:38:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T01:55:32Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -98,6 +98,11 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
98
98
  }
99
99
  /* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
100
100
  ok('brief: the morning card counts what waits on you', await waitFor(page, () => /wait on you|waits on you/.test(document.querySelector('[data-c3ph-moment="morning"]')?.textContent || ''), null, 8000));
101
+ ok('brief: the morning shows the top three points of the Briefing deck', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="morning"]'); return !!c && c.querySelectorAll('[data-c3ph-point]').length === 3 && /things this morning/.test(c.textContent); }, null, 8000));
102
+ await page.click('[data-c3ph-moment="morning"] [data-c3ph-point]');
103
+ ok('brief: a point opens its details', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-c3ph-need]'), null, 5000));
104
+ await page.keyboard.press('Escape');
105
+ await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-c3ph-need]'), null, 3000);
101
106
  ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
102
107
  if (await page.$('[data-c3ph-all-open]')) {
103
108
  await page.click('[data-c3ph-all-open]');
@@ -102,6 +102,8 @@ Open an item that shows **Agent can:**. The case file shows **What the agent wil
102
102
 
103
103
  The offer is on Jira and WBS items with a suggestion, and on each item the morning brief suggests for you (for example a Slack reply or an email). On a brief item the offer is the reply or the note that the item asks for, and the draft goes back to the place the item came from, only when you post it.
104
104
 
105
+ An item you added yourself shows **Agent can draft a first version**. The agent drafts what the item asks for (a document, a plan or a recommendation), and the draft stays on the item. It has no place to post to: read it, keep it, or send it back with a note.
106
+
105
107
  1. To change the instruction, select **Change what the agent does** and type a new one.
106
108
  2. Select **Let the agent draft this** and hold to confirm. This adds one job to today's day run, with the instruction.
107
109
 
@@ -15,7 +15,7 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
15
15
 
16
16
  | Moment | When | What the card shows |
17
17
  |---|---|---|
18
- | Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of things that wait on you and how many are overdue, and **The brief of this meeting** |
18
+ | Your morning | Before noon, with no meeting in the next 30 minutes | Your briefing: the top three points of the desktop Briefing, each opens its details. Then how many things wait on you, how many drafts wait for your review, and the first meeting with **The brief of this meeting** |
19
19
  | Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it, the items to close first, each with **Done**, and **The brief of this meeting** |
20
20
  | In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**. Recording opens focus (see below). |
21
21
  | Just ended | A meeting ended less than 20 minutes ago | **What came out of it?** If you recorded the meeting, the card shows the summary, the decisions, the actions and the open questions from its notes, the same notes as on the Notes page. While PMOS writes the notes, the card says so. If you did not record it, **Tell PMOS** puts the meeting name in the bar so you can type the result. |
@@ -292,7 +292,14 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
292
292
  for (const o of Array.isArray(c.open) ? c.open : []) say(`open question: ${o}`);
293
293
  if (c.initiative) say(`initiative: ${c.initiative}`);
294
294
  say(`instruction from the PM (this is the ask): ${t.instruction}`);
295
- } else say('(the item is no longer in today\'s brief)');
295
+ } else if (!t.instruction) say('(the item is no longer in today\'s brief)');
296
+ else {
297
+ // an item the PM added in Work (initiative c3-work-agent, amendment 11): the PM's item is the ask
298
+ const c = t.context || {};
299
+ say(`from Work: ${t.title}${c.due ? ' · due ' + c.due : ''}${c.from ? ' · with ' + c.from : ''}`);
300
+ }
301
+ // the PM's ask, as Work or Your day gave it, including a changed ask (c3-work-agent amendments 10 and 11)
302
+ if (t.instruction && !t.my_work) say(`instruction from the PM (this is the ask): ${t.instruction}`);
296
303
  if (t.kind === 'approve') say('kind approve: deliver ONE draft; nothing goes out until the PM approves it.');
297
304
  else say('kind auto: deliver ONE draft; nothing leaves this machine.');
298
305
  if (t.draft) { say(`draft (${t.draft.at}): ${t.draft.summary}${t.draft.link ? ' · ' + t.draft.link : ''}`); say(t.draft.body); }
@@ -122,8 +122,8 @@ async function handle(req, res, url, ctx) {
122
122
  return true;
123
123
  }
124
124
 
125
- const bm = BRIEF_ITEM.exec(String((body && body.item) || ''));
126
- if (bm && p !== '/api/my-work/refresh') { briefPress(ctx, res, p.slice('/api/my-work/'.length), body, bm[1], now); return true; }
125
+ const agentItem = String((body && body.item) || '');
126
+ if ((BRIEF_ITEM.test(agentItem) || LOCAL_ITEM.test(agentItem)) && p !== '/api/my-work/refresh') { agentItemPress(ctx, res, p.slice('/api/my-work/'.length), body, agentItem, now); return true; }
127
127
 
128
128
  if (p === '/api/my-work/decide' || p === '/api/my-work/stop') { taskPress(ctx, res, p.slice('/api/my-work/'.length), body, now); return true; }
129
129
 
@@ -200,29 +200,62 @@ function taskPress(ctx, res, verb, body, now) {
200
200
  ctx.json(res, { ok: true, item: after });
201
201
  }
202
202
 
203
- /* ── the agent on an item the morning brief suggests for the PM (c3-work-agent amendment 10) ──────────────────────
204
- * Item `brief:<id>`; its task is `t-<id>` in today's day run, added as Your day's "Have the agent draft it" adds it. */
203
+ /* ── the agent on an item the morning brief suggests for the PM (c3-work-agent amendment 10) and on an item the PM added
204
+ * in Work (amendment 11). Item `brief:<id>`: its task is `t-<id>` in today's day run, added as Your day's "Have the agent
205
+ * draft it" adds it. Item `item-<hex>`: its task is `t-<hex>`, in the newest day run of the last 7 days that holds it. */
205
206
  const BRIEF_ITEM = /^brief:([0-9a-f]{12})$/;
207
+ const LOCAL_ITEM = /^item-[0-9a-f]{12}$/;
206
208
 
207
- function briefPress(ctx, res, verb, body, id, now) {
209
+ /** The item the press names, as the agent sees it: its task id, the offer, the spec a press adds and its agent field. */
210
+ function targetOf(ctx, itemId, now) {
208
211
  const today = localDate(now);
209
- let brief = null; try { brief = JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', today + '.json'), 'utf8')); } catch { brief = null; }
210
- const x = brief && brief.date === today ? [...(brief.priorities || []), ...(brief.items || [])].find((y) => y && y.id === id) : null;
211
- if (!x || x.owner !== 'pm') { ctx.json(res, { ok: false, error: 'no item of yours with that id in today\'s brief' }, 404); return; }
212
- const taskId = 't-' + id;
213
- const after = (task) => ctx.json(res, { ok: true, item: { id: 'brief:' + id, agent: assigned.briefAgent(x, ctx.readDayRun ? ctx.readDayRun(today) : null) }, task: task || null });
212
+ const bm = BRIEF_ITEM.exec(itemId);
213
+ if (bm) {
214
+ let brief = null; try { brief = JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', today + '.json'), 'utf8')); } catch { brief = null; }
215
+ const x = brief && brief.date === today ? [...(brief.priorities || []), ...(brief.items || [])].find((y) => y && y.id === bm[1]) : null;
216
+ if (!x || x.owner !== 'pm') return { error: 'no item of yours with that id in today\'s brief' };
217
+ const run = ctx.readDayRun ? ctx.readDayRun(today) : null;
218
+ return { postable: true, taskId: 't-' + x.id, suggest: assigned.briefSuggest(x), spec: (own) => assigned.briefDraftSpec(x, own), run, agent: (r) => assigned.briefAgent(x, r) };
219
+ }
220
+ // an item the PM added in Work (amendment 11): its draft is kept across days, so the newest run that holds its task decides
221
+ const pmItems = require('./pm-items.js');
222
+ const item = pmItems.fold(pmItems.readLines(path.join(ctx.runtimeDir, 'pm-items', 'items.jsonl'))).find((i) => i.id === itemId);
223
+ if (!item || !assigned.localOffer(item)) return { error: 'no open item of yours with that id' };
224
+ const taskId = assigned.localTaskId(item);
225
+ const run = assigned.runWith(assigned.recentRuns(ctx.readDayRun, now), taskId);
226
+ return { postable: false, taskId, suggest: assigned.localSuggest(item), spec: (own) => assigned.localDraftSpec(item, own), run, agent: (r) => assigned.localAgent(item, r) };
227
+ }
228
+
229
+ function agentItemPress(ctx, res, verb, body, itemId, now) {
230
+ const g = targetOf(ctx, itemId, now);
231
+ if (g.error) { ctx.json(res, { ok: false, error: g.error }, 404); return; }
232
+ const { taskId } = g;
233
+ const after = (run, task) => ctx.json(res, { ok: true, item: { id: itemId, agent: g.agent(run) }, task: task || null });
214
234
  if (verb === 'act') {
215
- if (body.action !== 'brief') { ctx.json(res, { ok: false, error: 'a brief item takes only action brief here — mark it done or set it aside from Work' }, 400); return; }
235
+ if (body.action !== 'brief') { ctx.json(res, { ok: false, error: 'this item takes only action brief here — mark it done or set it aside from Work' }, 400); return; }
236
+ const live = g.run && dayRun.findTask(g.run, taskId);
237
+ if (live && live.status !== 'failed' && g.run.date !== localDate(now)) { ctx.json(res, { ok: false, error: `the agent's task is ${live.status} — open the draft or stop it first` }, 409); return; }
216
238
  // the offer as shown ("Draft your reply: …") is not a change: the task then gets the full ask, written for the agent
217
- const own = typeof body.instruction === 'string' && body.instruction.trim() !== assigned.briefSuggest(x) ? body.instruction.trim().slice(0, 500) : '';
218
- const b = ctx.myWorkBrief(assigned.briefDraftSpec(x, own), String(body.ws || ''));
239
+ const own = typeof body.instruction === 'string' && body.instruction.trim() !== g.suggest ? body.instruction.trim().slice(0, 500) : '';
240
+ const b = ctx.myWorkBrief(g.spec(own), String(body.ws || ''));
219
241
  if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return; }
220
- after(b.task); return;
242
+ after(b.run, b.task); return;
221
243
  }
222
- const run = ctx.readDayRun ? ctx.readDayRun(today) : null;
244
+ const run = g.run;
223
245
  const t = run ? dayRun.findTask(run, taskId) : null;
224
246
  if (!t) { ctx.json(res, { ok: false, error: 'no agent works on this item — let the agent draft it first' }, 409); return; }
225
247
  let next = null;
248
+ if (verb === 'decide' && body.action === 'approve' && !g.postable) { ctx.json(res, { ok: false, error: 'an item you added has no place to post to — keep the draft or send it back' }, 400); return; }
249
+ if (verb === 'decide' && run.date !== localDate(now) && (body.action === 'revise' || body.action === 'retry')) {
250
+ // a draft from an earlier day: the job CLI reads today's run only, so the new round is a task of today's run
251
+ const note = typeof body.note === 'string' ? body.note.trim().slice(0, 500) : '';
252
+ if (body.action === 'revise' && !note) { ctx.json(res, { ok: false, error: 'say what to change: a revise needs a note' }, 400); return; }
253
+ const prev = t.draft && t.draft.body ? String(t.draft.body).slice(0, 4000) : '';
254
+ 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` : ''}` : '';
255
+ const b = ctx.myWorkBrief(g.spec(own), String(body.ws || run.ws || ''));
256
+ if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return; }
257
+ after(b.run, b.task); return;
258
+ }
226
259
  if (verb === 'decide') {
227
260
  const r = dayRun.decide(run, taskId, { action: body.action, note: body.note }, now);
228
261
  if (!r.ok) { ctx.json(res, { ok: false, error: r.error }, 400); return; }
@@ -237,7 +270,7 @@ function briefPress(ctx, res, verb, body, id, now) {
237
270
  next = w.run;
238
271
  }
239
272
  if (!ctx.writeDayRun || !ctx.writeDayRun(next)) { ctx.json(res, { ok: false, error: 'could not save — nothing changed' }, 500); return; }
240
- after(dayRun.findTask(next, taskId));
273
+ after(next, dayRun.findTask(next, taskId));
241
274
  }
242
275
 
243
276
  module.exports = { prefixes: ['/api/my-work'], handle, listFor, listForAsync };
@@ -344,6 +344,9 @@ module.exports = {
344
344
  const c = prepare(ctx);
345
345
  const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines, proofSources(ctx, c.itemLines), await assignedAsync(ctx, c.now));
346
346
  v.resolves_titles = resolvesTitles(v.items, ctx, url);
347
+ // an item the PM added: the agent can draft a first version of it (c3-work-agent amendment 11)
348
+ const runs = assigned.recentRuns(ctx.readDayRun, c.now.getTime());
349
+ for (const it of v.items) if (!it.agent && assigned.localOffer(it)) it.agent = assigned.localAgent(it, assigned.runWith(runs, assigned.localTaskId(it)));
347
350
  ctx.json(res, v);
348
351
  return true;
349
352
  }
@@ -70,17 +70,48 @@ function briefDraftSpec(x, instruction) {
70
70
  instruction: `${ask}${/[.!?]$/.test(ask) ? '' : '.'} Draft only. Send nothing: the PM approves it first.`,
71
71
  context: { from: x.who || null, where: x.where || null, about: x.about || null, ask: x.ask || null, how: x.how || null, why_now: x.why_now || null } };
72
72
  }
73
- const BRIEF_STATE = { waiting: 'queued', working: 'working', sending: 'sending', failed: 'failed', review: 'ready', done: 'ready', sent: 'sent' };
74
- function briefAgent(x, run) {
75
- const t = run && Array.isArray(run.tasks) ? run.tasks.find((y) => y && y.id === 't-' + x.id) || null : null;
76
- const base = { suggest: briefSuggest(x), instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null };
73
+ const TASK_STATE = { waiting: 'queued', working: 'working', sending: 'sending', failed: 'failed', review: 'ready', done: 'ready', sent: 'sent' };
74
+ /** The agent field of an item whose day task is `taskId` in `run` (or none yet: the offer `suggest`). */
75
+ function taskAgent(taskId, suggest, run) {
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
78
  if (!t) return { state: 'suggest', ...base };
78
- return { ...base, state: BRIEF_STATE[t.status] || 'queued', instruction: t.instruction || null,
79
+ return { ...base, state: TASK_STATE[t.status] || 'queued', instruction: t.instruction || null,
79
80
  draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null } : null,
80
81
  sent: t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null, error: t.error || null,
81
82
  session: t.stalled_session ? String(t.stalled_session) : null, asks: t.asks || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
82
83
  started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null };
83
84
  }
85
+ const briefAgent = (x, run) => taskAgent('t-' + x.id, briefSuggest(x), run);
86
+
87
+ /* An item the PM added in Work (amendment 11): the agent drafts a first version of what the item asks for, on the PM's
88
+ * press. Its task is `t-<hex>` for item `item-<hex>`; nothing goes anywhere, the draft stays in PMOS. */
89
+ const LOCAL_ID = /^item-([0-9a-f]{12})$/;
90
+ const localTaskId = (item) => { const m = LOCAL_ID.exec(String((item && item.id) || '')); return m ? 't-' + m[1] : null; };
91
+ /** Can the agent take this item? An open item the PM added, not offline work. */
92
+ const localOffer = (item) => !!(item && localTaskId(item) && (!item.source || item.source === 'local') && !item.done && item.kind !== 'offline' && String(item.title || '').trim());
93
+ function localSuggest(item) { return `Draft a first version: ${String(item.title).trim()}`.slice(0, 300); }
94
+ function localDraftSpec(item, instruction) {
95
+ const ask = String(instruction || '').trim() || `Draft a first version of the work this item of the PM's asks for: ${String(item.title).trim()}`;
96
+ return { id: localTaskId(item), item_id: null, title: String(item.title).slice(0, 250), source: 'work', link: null,
97
+ instruction: `${ask}${/[.!?]$/.test(ask) ? '' : '.'} Use what this repository and your connected tools hold. Deliver ONE draft (a document, a plan or a recommendation) for the PM to read. Draft only. Send nothing to anyone.`,
98
+ context: { about: item.title, due: item.due || null, from: item.person || null, ask: item.waiting_on ? 'waiting on ' + item.waiting_on : null } };
99
+ }
100
+ const localAgent = (item, run) => taskAgent(localTaskId(item), localSuggest(item), run);
101
+ /** The newest of `runs` (newest first) that holds task `taskId`, or null. An item the PM added keeps its draft across days. */
102
+ const runWith = (runs, taskId) => (runs || []).find((r) => r && Array.isArray(r.tasks) && r.tasks.some((t) => t && t.id === taskId)) || null;
103
+ /** The day runs of the last `days` days, newest first, through `readDayRun(date)`. */
104
+ function recentRuns(readDayRun, nowMs, days = 7) {
105
+ const out = [];
106
+ if (typeof readDayRun !== 'function') return out;
107
+ const pad = (n) => String(n).padStart(2, '0');
108
+ for (let i = 0; i < days; i++) {
109
+ const d = new Date(nowMs); d.setDate(d.getDate() - i);
110
+ let r = null; try { r = readDayRun(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`); } catch { r = null; }
111
+ if (r) out.push(r);
112
+ }
113
+ return out;
114
+ }
84
115
 
85
116
  function fromBrief(brief, todayIso, run = null) {
86
117
  if (!brief || brief.date !== todayIso) return [];
@@ -117,4 +148,4 @@ function overlay(item, byId) {
117
148
  return out;
118
149
  }
119
150
 
120
- module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, agentOf, briefAgent, briefSuggest, briefDraftSpec, AGENT };
151
+ module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, agentOf, briefAgent, briefSuggest, briefDraftSpec, taskAgent, localTaskId, localOffer, localSuggest, localDraftSpec, localAgent, runWith, recentRuns, AGENT };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.208.0",
3
+ "version": "0.210.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.208.0",
9
+ "version": "0.210.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.208.0",
3
+ "version": "0.210.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",
@@ -58,3 +58,4 @@
58
58
  .c3 .c3ph-stats div{display:grid;gap:2px}
59
59
  .c3 .c3ph-stats b{font-size:20px;font-variant-numeric:tabular-nums}
60
60
  .c3 .c3ph-stats span{font-size:11px;color:var(--muted-foreground);line-height:1.25}
61
+ .c3 .c3ph-pts{display:grid;gap:2px}
@@ -8,7 +8,8 @@
8
8
  * actions and questions picked out so far (dest-notes.js partsOf), as the desktop note shows them
9
9
  * before a meeting: when it starts, who, what is prepared, the "before you go" items with Done
10
10
  * just ended: tell PMOS what came out of it a free gap: the own items that fit, with Done and Later today
11
- * morning: the next meeting and how much waits
11
+ * morning ("Your morning"): the briefing, the top three cards of the desktop Briefing's deck (GET /api/briefing cards), each
12
+ * opening its details; how much waits; the drafts waiting for review; then the first meeting with its brief
12
13
  * working hours, and a free gap: "Finished for you", the agent's drafts of today (GET /api/phone-day finished), each opens
13
14
  * to read (c3-phone-detail.js openDraft); approving one waits for the desk
14
15
  * evening (recap, from 17:00): how today went (GET /api/phone-day recap), what moved, what is still open, one thing tonight
@@ -31,7 +32,7 @@
31
32
  * There is no way to the desktop pages on a phone.
32
33
  * The moment is worked out again every minute; when it changes, a short banner says so. ?at=HH:MM sets the clock in demo mode only. */
33
34
  import { el } from './dom.js';
34
- import { button, iconButton } from './primitives.js';
35
+ import { button, iconButton, ensureComponentsCss } from './primitives.js';
35
36
  import { pmPost, pmGet } from './http.js';
36
37
  import { ensureC3Css, c3ic } from './pm-work-view.js';
37
38
  import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
@@ -66,7 +67,7 @@ export function phoneWanted(store) {
66
67
  }
67
68
 
68
69
  export function mountPhone(root, ctx = {}) {
69
- ensureC3Css(); ensureCss();
70
+ ensureC3Css(); ensureComponentsCss(); ensureCss();
70
71
  const demo = !!ctx.demo;
71
72
  const q = new URLSearchParams(location.search);
72
73
  const at = demo && /^\d{1,2}:\d{2}$/.test(q.get('at') || '') ? q.get('at').split(':').map(Number) : null;
@@ -220,6 +221,15 @@ export function mountPhone(root, ctx = {}) {
220
221
  return out;
221
222
  }
222
223
  const decisions = () => waitRows().filter((r) => !r.pm).length;
224
+ /* the top points of the desktop Briefing (GET /api/briefing cards, the deck), at most three, each opening its details */
225
+ function briefPoints() {
226
+ const cards = (S.briefing && Array.isArray(S.briefing.cards) ? S.briefing.cards : []).filter((c) => c && c.headline);
227
+ return cards.slice(0, 3).map((c, i) => {
228
+ const v = cardView(c);
229
+ return { id: String(c.key || 'card' + i), pm: false, decide: !!(c.move && c.move.options), headline: v.headline,
230
+ label: v.tag + (v.deadline && v.deadline.text ? ' · ' + v.deadline.text : ''), why: c.why && c.why.text, url: c.url, urlLabel: c.url_label, facts: v.facts };
231
+ });
232
+ }
223
233
  const needOpts = { onDone: (r) => itemDone(r), onLater: (r) => itemLater(r, 'later-today') };
224
234
  const focus = (m) => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start, at: m.start, end: m.end || null, title: m.title }, date: String(m.start || '').slice(0, 10), phone: true }))
225
235
  .catch(() => toast('Focus did not open.'));
@@ -316,11 +326,18 @@ export function mountPhone(root, ctx = {}) {
316
326
  if (mo.kind === 'recap') return recapCard();
317
327
  if (mo.kind === 'night') return nightCard();
318
328
  if (mo.kind === 'morning') {
329
+ /* the morning is the briefing (mockup version 7, "Your morning"): the top points of the desktop Briefing's deck, then the first meeting */
319
330
  const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
320
- return card(true, { 'data-c3ph-moment': 'morning' }, why('Your day'), big(mo.next ? mo.next.title + ' at ' + hm(mo.next.start) : 'No meetings today'),
321
- line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
322
- w.length ? w.length + (w.length === 1 ? ' thing waits on you' : ' things wait on you') + (late ? ', ' + late + ' overdue.' : '.') : 'Nothing waits on you.'].filter(Boolean).join(' ')),
323
- mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
331
+ const pts = briefPoints();
332
+ const rv = S.briefing && Array.isArray(S.briefing.review) ? S.briefing.review.length : 0;
333
+ return card(true, { 'data-c3ph-moment': 'morning', 'data-c3ph-brief-points': String(pts.length) }, why('Your morning'),
334
+ big(!S.briefing ? 'Reading your briefing' : pts.length ? (pts.length === 1 ? 'One thing this morning' : ['', '', 'Two', 'Three'][pts.length] + ' things this morning') : 'Nothing pressing this morning'),
335
+ pts.length ? el('div', { class: 'c3ph-pts' }, ...pts.map((r) => row({ title: r.headline, meta: [r.label], compact: true, onOpen: () => openNeed(r, needOpts), openLabel: 'Open ' + r.headline, attrs: { 'data-c3ph-point': r.id } }))) : null,
336
+ line([w.length ? w.length + (w.length === 1 ? ' thing waits on you' : ' things wait on you') + (late ? ', ' + late + ' overdue.' : '.') : 'Nothing waits on you.',
337
+ rv ? (rv === 1 ? '1 draft waits' : rv + ' drafts wait') + ' for your review on your computer.' : null].filter(Boolean).join(' ')),
338
+ mo.next ? el('div', { class: 'c3ph-fit', 'data-c3ph-first': mo.next.id }, el('b', null, 'First up: ' + mo.next.title + ' at ' + hm(mo.next.start)),
339
+ line(inText((Date.parse(mo.next.start) - now().getTime()) / 60000).replace(/^in/, 'In') + (who(mo.next) ? ' · ' + who(mo.next) : '')),
340
+ acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next)))) : line('No meetings today.'));
324
341
  }
325
342
  return recapCard();
326
343
  }
@@ -706,11 +706,15 @@ const hhmm = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()
706
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
707
  // where a post goes: Jira for a ticket; for an item the morning brief suggests, the place it came from (amendment 10)
708
708
  const PLACE = { mail: 'email', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Drive', figma: 'Figma', calendar: 'the calendar', wispr: 'Wispr' };
709
- const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : PLACE[item.place] || 'its source');
709
+ const isMine = (item) => item.source === 'local' && !item.origin; // an item the PM added in Work (amendment 11): no place to post to
710
+ const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : isMine(item) ? 'anyone' : PLACE[item.place] || 'its source');
710
711
  const AG = {
711
712
  can: (t) => 'Agent can: ' + t, queued: 'Agent: queued', working: (ago) => 'Agent: working' + (ago ? ' · started ' + ago : ''), ready: 'Agent: draft ready',
712
713
  sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (asks) => (asks ? 'Agent: needs you' : 'Agent stopped'),
713
714
  sitSuggest: (w) => `The agent can draft this for you. You read it here first. Nothing goes to ${w} until you post it.`,
715
+ canMine: 'Agent can draft a first version',
716
+ sitMine: 'The agent can draft a first version of this for you. You read it here. Nothing leaves PMOS.',
717
+ handedMine: 'Handed to the agent. Its draft stays here in PMOS.', keptMine: 'Kept. The draft stays on this item.',
714
718
  sitQueued: 'Queued. The agent starts as soon as a session is free. You can close this page.',
715
719
  sitWorking: 'The agent is working on it. You can close this page: the item keeps its state.',
716
720
  sitReady: (at, w) => `Draft ready${at ? ' at ' + at : ''}. Nothing went to ${w}.`,
@@ -729,7 +733,7 @@ const AG = {
729
733
  function agentChip(item) {
730
734
  const a = item.agent; if (!a || item.mock_row) return null;
731
735
  const st = (cls, ic, text, spin) => el('span', { class: cls + ' wk-agent-st', 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id, title: text }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), el('span', { class: 'wk-agent-tx' }, text));
732
- if (a.state === 'suggest') return st('chip2', 'spark', AG.can(a.instruction || a.suggest || ''));
736
+ if (a.state === 'suggest') return st('chip2', 'spark', isMine(item) ? AG.canMine : AG.can(a.instruction || a.suggest || ''));
733
737
  if (a.state === 'queued') return st('st', 'clock', AG.queued);
734
738
  if (a.state === 'working') return st('st c3strip', 'spark', AG.working(minsAgo(a.started_at)), true);
735
739
  if (a.state === 'ready') return st('st ok', 'check', AG.ready);
@@ -758,9 +762,9 @@ function agentCase(inst, item, L) {
758
762
  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 + ' ',
759
763
  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))));
760
764
  if (a.state === 'suggest' || a.state === 'failed') {
761
- sit = a.state === 'failed' ? AG.sitFailed(a.error, a.asks) : AG.sitSuggest(w);
765
+ sit = a.state === 'failed' ? AG.sitFailed(a.error, a.asks) : isMine(item) ? AG.sitMine : AG.sitSuggest(w);
762
766
  if (a.state === 'failed' && a.session) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-session': a.session }, [c3ic('term', 'sm'), AG.openSession], () => openAgentSession(a.session, item))));
763
- const brief = () => agentPress(inst, '/api/my-work/act', { action: 'brief', item: item.id, instruction: ask(), ws: inst.ctx.ws }, AG.handed(w));
767
+ const brief = () => agentPress(inst, '/api/my-work/act', { action: 'brief', item: item.id, instruction: ask(), ws: inst.ctx.ws }, isMine(item) ? AG.handedMine : AG.handed(w));
764
768
  const retry = () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'retry' }, AG.retried);
765
769
  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];
766
770
  cta = 'Slide to let the agent draft';
@@ -787,10 +791,10 @@ function agentCase(inst, item, L) {
787
791
  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); };
788
792
  note.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) back(); });
789
793
  opts = [
790
- 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 }),
794
+ 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 }),
791
795
  o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
792
- o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: AG.kept, timeout: 3000 })),
793
- ];
796
+ o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: isMine(item) ? AG.keptMine : AG.kept, timeout: 3000 })),
797
+ ].filter(Boolean);
794
798
  const noteBox = el('div', { class: 'form wk-agent-ask', 'data-pm-agent-note': item.id }, note); noteBox.hidden = inst.picks.get(item.id) !== 'back';
795
799
  extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item))));
796
800
  after = noteBox;
@@ -859,7 +863,7 @@ function caseFile(inst, item, lens) {
859
863
  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'),
860
864
  c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
861
865
  }
862
- 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')));
866
+ const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent' && !isMine(item)) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
863
867
  if (showSteps) extras.push(stepsBlock(inst, item));
864
868
  const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
865
869
  if (anchor) anchor.after(...extras); else root.prepend(...extras);
@@ -22,9 +22,9 @@ Rules that always hold:
22
22
  7. YOU CANNOT FINISH WITHOUT A REPORT. End every run with exactly one of `day deliver` (do, revise), `day sent` (send) or `day fail`.
23
23
 
24
24
  Procedure:
25
- 1. Run `pmos proactive day show TASK`. It prints the task: the title, the kind (approve or auto), the source, the link, the goal, the item context (where, who, about, ask, how), the draft so far and the notes of the PM for each revision. Read all of it. A task from My work also prints "instruction from the PM (this is the ask)". The PM read the ticket and wrote that instruction. Do what it says and nothing more, and read the item context as background. The rules above still hold: one draft, nothing sent, no pull request, no commit. If the instruction asks for one of those, draft it and say in the summary which step approval would trigger. If the command says the task does not exist, or its status does not fit the mode, stop and run `pmos proactive day fail TASK --reason "<one sentence>"`.
25
+ 1. Run `pmos proactive day show TASK`. It prints the task: the title, the kind (approve or auto), the source, the link, the goal, the item context (where, who, about, ask, how), the draft so far and the notes of the PM for each revision. Read all of it. A task from My work, from Work (an item the PM added, or an item of the brief the PM gave to the agent) or from Your day also prints "instruction from the PM (this is the ask)". The PM read the ticket and wrote that instruction. Do what it says and nothing more, and read the item context as background. The rules above still hold: one draft, nothing sent, no pull request, no commit. If the instruction asks for one of those, draft it and say in the summary which step approval would trigger. If the command says the task does not exist, or its status does not fit the mode, stop and run `pmos proactive day fail TASK --reason "<one sentence>"`.
26
26
  2. If MODE is do:
27
- a. Read the sources the task names, read-only. Start from the link of the task. Read the conversation, the issue, the page or the file itself, and what it points to. Use `pmos proactive settings` to see which sources are on. If a source is off or you cannot reach it, say so in the summary.
27
+ a. Read the sources the task names, read-only. Start from the link of the task. A task with no link (an item the PM added in Work) starts from the instruction, this repository and the sources it names. Read the conversation, the issue, the page or the file itself, and what it points to. Use `pmos proactive settings` to see which sources are on. If a source is off or you cannot reach it, say so in the summary.
28
28
  b. Do the work locally. A reply, a comment, a summary, a description or a plan is a draft in a file in the scratch folder of this session. Write the text exactly as it would be sent.
29
29
  c. Check the draft against the item: does it answer the ask, is it for the right person and place, is it in the voice and the length the place expects. Fix it.
30
30
  d. Submit it. Run `pmos proactive day deliver TASK --summary "<at most 240 characters: what you did and where the draft would go>" --body-file <the draft file> [--link <URL of the thread, issue, page or file the draft is for>]`. The body is at most 8000 characters. The summary names the one outward step that approval will trigger, for example "Post this reply in the thread". For a task of kind auto, the summary says what you made and where it is.