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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-phone-test.js +5 -0
- package/workspace/guide/my-work.md +2 -0
- package/workspace/guide/phone-companion.md +1 -1
- package/workspace/lib/proactive-cli.js +8 -1
- package/workspace/lib/routes-my-work.js +49 -16
- package/workspace/lib/routes-pm-items.js +3 -0
- package/workspace/lib/work-assigned.js +37 -6
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-phone.css +1 -0
- package/workspace/public/pm/c3-phone.js +24 -7
- package/workspace/public/pm/pm-work-view.js +12 -8
- package/workspace/skills/day-task.skill +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
126
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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: '
|
|
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() !==
|
|
218
|
-
const b = ctx.myWorkBrief(
|
|
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 =
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
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:
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.210.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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.
|