create-pmos 0.73.0 → 0.74.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.73.0",
3
+ "version": "0.74.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@a99e09f
1
+ kernel-source: pmos@fb515e5
2
2
  assembled: 2026-10-07
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-07T05:41:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
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-07T05:41:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
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-07T05:41:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T07:25:37Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -82,12 +82,33 @@ Each job has one of three kinds.
82
82
 
83
83
  | Kind | Who does it | What you do |
84
84
  |---|---|---|
85
- | you | Only you. | Do it. Nothing runs for it. |
85
+ | you | Only you. | Do it, then mark it. See [Jobs you do and meetings you prepare for](#jobs-you-do-and-meetings-you-prepare-for). |
86
86
  | approve | An agent prepares one draft. | Read the draft. Press **Approve** to let the one outward step happen, or **Revise** with a note. |
87
87
  | auto | An agent does it, and nothing leaves the computer. | Read the result. Press **Revise** if it is wrong. |
88
88
 
89
89
  A draft that waits for you shows its summary, its text and the link of the place it is for. **Approve** sends only that text, to that place. **Revise** takes a note of at most 500 characters and asks for a new draft. You can revise a job 3 times. A fourth revise is refused, so do the job yourself or drop it. A failed job shows its reason and **Retry**.
90
90
 
91
+ ### Jobs you do and meetings you prepare for
92
+
93
+ A job that only you can do starts with a verb, such as **Reply**, **Review** or **Sign off**, and one sentence that says what to do. Under the sentence are who asked, why it is for today, and the steps. Check each step when you finish it. The first button opens the place where you do the job, for example **Reply in Slack**. **Have the agent draft it** gives the job to an agent. The draft then waits for your approval, as for any approve job.
94
+
95
+ A job card holds what you need to do the job without opening another file. **The conversation** shows the last messages, up to 4. A step that reads a source shows what the source says, in a few lines, under the step. **What the sources say** shows the other sources. **Open the source** opens the source itself when you want more.
96
+
97
+ When the job is a reply or a review, you can finish it in the card. Write your answer in the box, or press a suggestion to start from it. For a review, choose **Approve** or **Ask for changes**. Then press **Send to …**. An agent posts exactly your text, as you, to the place the button names. Nothing else is sent, and nothing is sent before you press. The job then shows **Sending**, and **Sent** when it is posted. If the send fails, the job shows the reason and **Retry**, which sends the same text again.
98
+
99
+ A meeting that needs prep is a row of **Needs you**, with the verb **Prepare**. Its detail shows the prep, how many steps are ready, the free time before the meeting, and the steps before it. A step that reads a source shows what the source says under it. A step that asks for your view, such as **Write your recommendation**, has a box for your note. The note is saved when you leave the box. It stays with the meeting on this computer and is not sent. A step that the brief already marked done stays checked.
100
+
101
+ When you finish or put off a job or a meeting, mark it:
102
+
103
+ | Press | What it does |
104
+ |---|---|
105
+ | **Mark done** (a meeting: **Mark ready**) | Moves it to **Done today**. |
106
+ | **Snooze** (a meeting: **Remind me**) | Moves it to **Snoozed** until a time later today. At that time it comes back to **Needs you**. |
107
+ | **Waiting on…** | Puts a job on hold. Write who or what you wait on, and when to ask again today. It shows in **Waiting on others** until that time. |
108
+ | **Not mine** (a meeting: **Skip prep**) | Takes it off **Needs you**, with a reason. |
109
+
110
+ Each press shows **Undo**. **Bring it back now** or **Reopen** puts a marked item back in **Needs you**. A mark lasts for the day. A mark does not send anything and does not change the source. Only **Send to …** sends, and only your text. **Snoozed**, **Waiting on others** and **Done today** are folded. Press a group to open it.
111
+
91
112
  The page also shows which sources were read and how many items each gave, and the next daytime check. **Check now** starts a check at once.
92
113
 
93
114
  ## What waits for you
@@ -41,6 +41,8 @@ const EFFORTS = ['minutes', 'an-hour', 'half-day', 'day'];
41
41
  const KINDS = ['you', 'approve', 'auto'];
42
42
  const DIGEST_SOURCES = ['slack', 'jira', 'confluence'];
43
43
  const STEP_WHEN = ['before', 'during', 'after'];
44
+ // The verb of an item the PM acts on (initiative today-act-prepare, PRD A2): the chip Your day shows first.
45
+ const VERBS = ['reply', 'review', 'decide', 'sign-off', 'read', 'do'];
44
46
  const WORTH_ASKING = 0.5;
45
47
  const MAX_ITEMS = 30;
46
48
  const MAX_EVENTS = 20;
@@ -95,6 +97,10 @@ function eventExtras(e, at, sources) {
95
97
  const err = optText(e[k], n, `${at}: ${k}`); if (err) return { error: err };
96
98
  if (typeof e[k] === 'string' && e[k].trim()) f[k] = clip(e[k], n);
97
99
  }
100
+ // The sources the PM reads to prepare, each in a few lines, so no other file needs opening (PRD X2).
101
+ const refs = refList(e.refs, at);
102
+ if (refs.error) return refs;
103
+ if (refs.refs.length) f.refs = refs.refs;
98
104
  if (e.steps !== undefined && e.steps !== null) {
99
105
  if (typeof e.steps !== 'object' || Array.isArray(e.steps)) return { error: `${at}: steps must be {before, during, after}` };
100
106
  for (const k of Object.keys(e.steps)) if (!STEP_WHEN.includes(k)) return { error: `${at}: steps has "${k}" — only ${STEP_WHEN.join(', ')}` };
@@ -112,7 +118,10 @@ function eventExtras(e, at, sources) {
112
118
  if (tooLong(st.t, 140)) return { error: `${sa}: t is over 140 characters` };
113
119
  if (!OWNERS.includes(st.who)) return { error: `${sa}: who must be one of ${OWNERS.join(', ')}` };
114
120
  if (st.done !== undefined && typeof st.done !== 'boolean') return { error: `${sa}: done must be true or false` };
115
- steps[k].push({ t: clip(st.t, 140), who: st.who, done: st.done === true });
121
+ if (st.ref !== undefined && st.ref !== null && !(Number.isInteger(st.ref) && st.ref >= 0 && st.ref < refs.refs.length)) return { error: `${sa}: ref must name one of the refs` };
122
+ if (st.note !== undefined && typeof st.note !== 'boolean') return { error: `${sa}: note must be true or false` };
123
+ if (st.note && (k !== 'before' || st.who !== 'pm')) return { error: `${sa}: note is for a step the PM does before the meeting` };
124
+ steps[k].push({ t: clip(st.t, 140), who: st.who, done: st.done === true, ...(Number.isInteger(st.ref) ? { ref: st.ref } : {}), ...(st.note ? { note: true } : {}) });
116
125
  }
117
126
  }
118
127
  f.steps = steps;
@@ -177,9 +186,86 @@ function itemExtras(x, at) {
177
186
  if (!KINDS.includes(kind)) return { error: `${at}: kind must be one of ${KINDS.join(', ')}` };
178
187
  if (x.owner === 'pm' && kind !== 'you') return { error: `${at}: an item the PM owns has kind "you" — "${kind}" is for an item the agent owns` };
179
188
  f.kind = kind;
189
+ // What the PM does, said once (PRD A2): a verb, one sentence, a due time and at most 4 steps. All optional.
190
+ if (x.verb !== undefined && x.verb !== null) {
191
+ if (!VERBS.includes(x.verb)) return { error: `${at}: verb must be one of ${VERBS.join(', ')}` };
192
+ f.verb = x.verb;
193
+ }
194
+ const terr = optText(x.todo, 200, `${at}: todo`);
195
+ if (terr) return { error: terr };
196
+ if (typeof x.todo === 'string' && x.todo.trim()) f.todo = clip(x.todo, 200);
197
+ // The context the PM reads in the card, so no other file needs opening (PRD X1, X2), and where the PM's own answer goes
198
+ // (PRD F1). All optional.
199
+ const ctx = cardContext(x, at);
200
+ if (ctx.error) return { error: ctx.error };
201
+ Object.assign(f, ctx.fields);
202
+ if (x.due !== undefined && x.due !== null) {
203
+ if (typeof x.due !== 'string' || Number.isNaN(Date.parse(x.due))) return { error: `${at}: due must be a date and time` };
204
+ f.due = x.due;
205
+ }
206
+ const st = stepList(x.steps, 4, f.refs ? f.refs.length : 0, at);
207
+ if (st.error) return { error: st.error };
208
+ if (st.steps.length) f.steps = st.steps;
180
209
  return { fields: f };
181
210
  }
182
211
 
212
+ /* The messages of the conversation (at most 4) and the sources it points to, each read in a few lines (at most 3). */
213
+ function cardContext(x, at) {
214
+ const f = {};
215
+ if (x.thread !== undefined && x.thread !== null) {
216
+ if (!Array.isArray(x.thread) || x.thread.length > 4) return { error: `${at}: thread must be a list of at most 4 messages` };
217
+ const thread = [];
218
+ for (const [i, m] of x.thread.entries()) {
219
+ const w = `${at}: thread message ${i + 1}`;
220
+ if (!m || typeof m !== 'object' || typeof m.text !== 'string' || !m.text.trim()) return { error: `${w} needs text` };
221
+ const e = optText(m.text, 280, `${w} text`) || optText(m.who, 80, `${w} who`) || optText(m.at, 20, `${w} at`);
222
+ if (e) return { error: e };
223
+ thread.push({ who: m.who ? clip(m.who, 80) : null, at: m.at ? clip(m.at, 20) : null, text: clip(m.text, 280) });
224
+ }
225
+ if (thread.length) f.thread = thread;
226
+ }
227
+ const refs = refList(x.refs, at);
228
+ if (refs.error) return refs;
229
+ if (refs.refs.length) f.refs = refs.refs;
230
+ if (x.send !== undefined && x.send !== null) {
231
+ const sd = x.send;
232
+ if (typeof sd !== 'object' || Array.isArray(sd)) return { error: `${at}: send must be {to, suggest}` };
233
+ const e = optText(sd.to, 120, `${at}: send.to`) || optLines(sd.suggest, 3, 280, `${at}: send.suggest`);
234
+ if (e) return { error: e };
235
+ if (sd.review !== undefined && typeof sd.review !== 'boolean') return { error: `${at}: send.review must be true or false` };
236
+ f.send = { to: sd.to ? clip(sd.to, 120) : null, suggest: lines(sd.suggest, 280), ...(sd.review ? { review: true } : {}) };
237
+ }
238
+ return { fields: f };
239
+ }
240
+ function refList(v, at) {
241
+ if (v === undefined || v === null) return { refs: [] };
242
+ if (!Array.isArray(v) || v.length > 3) return { error: `${at}: refs must be a list of at most 3 sources` };
243
+ const refs = [];
244
+ for (const [i, r] of v.entries()) {
245
+ const w = `${at}: ref ${i + 1}`;
246
+ if (!r || typeof r !== 'object' || typeof r.label !== 'string' || !r.label.trim()) return { error: `${w} needs a label` };
247
+ const e = optText(r.label, 120, `${w} label`) || optText(r.source, 20, `${w} source`) || optLink(r.link, `${w} link`) || optLines(r.lines, 5, 200, `${w} lines`);
248
+ if (e) return { error: e };
249
+ if (!Array.isArray(r.lines) || !r.lines.length) return { error: `${w} needs lines: what the source says, in your words` };
250
+ refs.push({ label: clip(r.label, 120), source: r.source ? clip(r.source, 20) : null, link: r.link || null, lines: lines(r.lines, 200) });
251
+ }
252
+ return { refs };
253
+ }
254
+ /* Steps of an item: text, or {t, ref} where ref is the index of the source the step reads. */
255
+ function stepList(v, max, nRefs, at) {
256
+ if (v === undefined || v === null) return { steps: [] };
257
+ if (!Array.isArray(v) || v.length > max) return { error: `${at}: steps must be a list of at most ${max}` };
258
+ const steps = [];
259
+ for (const [i, s] of v.entries()) {
260
+ const o = typeof s === 'string' ? { t: s } : s;
261
+ if (!o || typeof o !== 'object' || typeof o.t !== 'string' || !o.t.trim()) return { error: `${at}: step ${i + 1} must be text` };
262
+ if (tooLong(o.t, 140)) return { error: `${at}: step ${i + 1} is over 140 characters` };
263
+ if (o.ref !== undefined && o.ref !== null && !(Number.isInteger(o.ref) && o.ref >= 0 && o.ref < nRefs)) return { error: `${at}: step ${i + 1}: ref must name one of the refs` };
264
+ steps.push(Number.isInteger(o.ref) ? { t: clip(o.t, 140), ref: o.ref } : { t: clip(o.t, 140) });
265
+ }
266
+ return { steps };
267
+ }
268
+
183
269
  /* The fixed order of items: higher confidence first, then PM-owned before agent-owned, then by title. */
184
270
  function sortItems(items) {
185
271
  return items.sort((a, b) => b.confidence - a.confidence || (a.owner === b.owner ? 0 : a.owner === 'pm' ? -1 : 1) || a.title.localeCompare(b.title));
@@ -419,5 +505,5 @@ function rereadBrief(stored, incoming) {
419
505
  digests, places, resolved: [...gone].slice(-MAX_RESOLVED) } };
420
506
  }
421
507
 
422
- module.exports = { SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS, MAX_PLACES,
508
+ module.exports = { VERBS, SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS, MAX_PLACES,
423
509
  isUrl, validateBrief, mergeBrief, rereadBrief, greeting, localDate };
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+ /* day-marks.js — the PM's marks on today's items and meetings (initiative `today-act-prepare`, PRD M1 to M6).
3
+ *
4
+ * A brief item the PM owns, or a meeting with prep, used to stay in Needs you all day: nothing the PM did changed it.
5
+ * A mark is what the PM did with it: done (a meeting: ready), snoozed until a time, waiting on someone until a time, or
6
+ * dismissed with a reason (not mine, or skip prep). A mark also holds the steps the PM checked, by their text.
7
+ *
8
+ * PURE: no I/O and no clock of its own. The server reads goals/<date>.marks.json, passes it with `now` and the ids of
9
+ * today's brief, and writes what `edit` returns. A refused edit returns the reason and the caller writes nothing.
10
+ *
11
+ * emptyMarks(date) → { date, marks: {} }
12
+ * readMarks(raw, date) → a checked record, or an empty one when `raw` is not one for `date`
13
+ * edit(rec, op, { now, known }) → { ok:true, rec, mark, prev } | { ok:false, error }
14
+ * op: { action: 'done' | 'reopen', id }
15
+ * | { action: 'snooze', id, until } until: a time later today
16
+ * | { action: 'hold', id, on, until } on: who or what (1 to 80 characters), until: a time later today
17
+ * | { action: 'dismiss', id, why } why: no-action | someone-else | done-elsewhere (an item)
18
+ * nothing | elsewhere (a meeting)
19
+ * | { action: 'step', id, step, done } step: the step's text, done: true | false
20
+ * | { action: 'note', id, text } the PM's own note, written in the card (at most 1000
21
+ * characters; empty clears it). It stays on this machine.
22
+ * | { action: 'restore', id, mark } mark: a `prev` an earlier edit returned, or null (Undo)
23
+ * known: Map of today's ids → 'item' | 'event'. An id not in it is refused.
24
+ * effective(rec, now) → { id: mark } with a snooze or hold whose time has passed read as open
25
+ *
26
+ * A mark lasts for its day: brief item ids are per day (lib/brief.js), so a snooze or a hold ends at midnight.
27
+ */
28
+
29
+ const STATES = ['done', 'snoozed', 'waiting', 'dismissed'];
30
+ const ACTIONS = ['done', 'reopen', 'snooze', 'hold', 'dismiss', 'step', 'note', 'restore'];
31
+ const KINDS = ['item', 'event'];
32
+ const REASONS = { item: ['no-action', 'someone-else', 'done-elsewhere'], event: ['nothing', 'elsewhere'] };
33
+ const ID_RE = /^[0-9a-f]{12}$/;
34
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
35
+ const MAX_IDS = 400;
36
+ const MAX_STEPS = 12;
37
+ const MAX_STEP = 140;
38
+ const MAX_ON = 80;
39
+ const MAX_NOTE = 1000;
40
+
41
+ const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
42
+ const pad = (n) => String(n).padStart(2, '0');
43
+ const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
44
+ const iso = (ms) => new Date(ms).toISOString();
45
+
46
+ const emptyMarks = (date) => ({ date, marks: {} });
47
+
48
+ /** One mark read from disk or from an Undo, checked. Returns the clean mark, or null when it is not one. */
49
+ function cleanMark(m, date) {
50
+ if (!m || typeof m !== 'object' || Array.isArray(m) || !KINDS.includes(m.kind)) return null;
51
+ const state = m.state === null || m.state === undefined ? null : m.state;
52
+ if (state !== null && !STATES.includes(state)) return null;
53
+ const out = { kind: m.kind, state, at: typeof m.at === 'string' ? m.at : null };
54
+ if (state === 'snoozed' || state === 'waiting') {
55
+ const t = Date.parse(m.until);
56
+ if (!Number.isFinite(t) || localDate(t) !== date) return null;
57
+ out.until = iso(t);
58
+ }
59
+ if (state === 'waiting') { const on = clip(m.on, MAX_ON); if (!on) return null; out.on = on; }
60
+ if (state === 'dismissed') { if (!REASONS[m.kind].includes(m.why)) return null; out.why = m.why; }
61
+ const steps = Array.isArray(m.steps) ? [...new Set(m.steps.map((s) => clip(s, MAX_STEP)).filter(Boolean))].slice(0, MAX_STEPS) : [];
62
+ if (steps.length) out.steps = steps;
63
+ if (typeof m.note === 'string' && m.note.trim()) out.note = m.note.trim().slice(0, MAX_NOTE);
64
+ return out;
65
+ }
66
+
67
+ /** A marks record read from disk, checked. Anything that is not one for `date` reads as an empty record. */
68
+ function readMarks(raw, date) {
69
+ if (!raw || typeof raw !== 'object' || raw.date !== date || !raw.marks || typeof raw.marks !== 'object') return emptyMarks(date);
70
+ const marks = {};
71
+ for (const [id, m] of Object.entries(raw.marks).slice(0, MAX_IDS)) {
72
+ if (!ID_RE.test(id)) continue;
73
+ const c = cleanMark(m, date);
74
+ if (c) marks[id] = c;
75
+ }
76
+ return { date, marks };
77
+ }
78
+
79
+ /** A time later today, or the reason it is not one. */
80
+ function laterToday(v, now, date) {
81
+ const t = typeof v === 'number' ? v : Date.parse(v);
82
+ if (!Number.isFinite(t)) return { error: 'until must be a date and time' };
83
+ if (t <= now) return { error: 'until must be later than now' };
84
+ if (localDate(t) !== date) return { error: 'until must be today: a mark ends with the day' };
85
+ return { at: t };
86
+ }
87
+
88
+ function edit(rec, op, { now = Date.now(), known } = {}) {
89
+ if (!rec || !DATE_RE.test(String(rec.date)) || !rec.marks || typeof rec.marks !== 'object') return { ok: false, error: 'no marks record to edit' };
90
+ if (!op || typeof op !== 'object' || !ACTIONS.includes(op.action)) return { ok: false, error: `action must be one of ${ACTIONS.join(', ')}` };
91
+ const id = String(op.id || '');
92
+ if (!ID_RE.test(id)) return { ok: false, error: 'id must be the id of an item or a meeting of today\'s brief' };
93
+ const kind = known && typeof known.get === 'function' ? known.get(id) : null;
94
+ if (!KINDS.includes(kind)) return { ok: false, error: 'no item or meeting with that id in today\'s brief' };
95
+ if (localDate(now) !== rec.date) return { ok: false, error: `the marks are for ${rec.date}, and today is ${localDate(now)}` };
96
+ const marks = { ...rec.marks };
97
+ const prev = marks[id] ? JSON.parse(JSON.stringify(marks[id])) : null;
98
+ const cur = prev ? { ...prev } : { kind, state: null, at: null };
99
+ cur.kind = kind;
100
+ const at = iso(now);
101
+ const setState = (state, extra) => {
102
+ delete cur.until; delete cur.on; delete cur.why;
103
+ Object.assign(cur, { state, at }, extra || {});
104
+ };
105
+
106
+ if (op.action === 'restore') {
107
+ if (op.mark === null || op.mark === undefined) { delete marks[id]; return { ok: true, rec: { date: rec.date, marks }, mark: null, prev }; }
108
+ const c = cleanMark({ ...op.mark, kind }, rec.date);
109
+ if (!c) return { ok: false, error: 'mark is not a mark of this item' };
110
+ marks[id] = c;
111
+ return { ok: true, rec: { date: rec.date, marks }, mark: c, prev };
112
+ }
113
+ if (op.action === 'step') {
114
+ const step = clip(op.step, MAX_STEP);
115
+ if (!step) return { ok: false, error: 'step must be the text of a step' };
116
+ if (typeof op.done !== 'boolean') return { ok: false, error: 'done must be true or false' };
117
+ const steps = new Set(cur.steps || []);
118
+ if (op.done) steps.add(step); else steps.delete(step);
119
+ if (steps.size > MAX_STEPS) return { ok: false, error: `at most ${MAX_STEPS} steps are kept for one item` };
120
+ if (steps.size) cur.steps = [...steps]; else delete cur.steps;
121
+ } else if (op.action === 'note') {
122
+ if (typeof op.text !== 'string') return { ok: false, error: 'text must be the note' };
123
+ if (op.text.trim().length > MAX_NOTE) return { ok: false, error: `the note is over ${MAX_NOTE} characters` };
124
+ if (op.text.trim()) cur.note = op.text.trim(); else delete cur.note;
125
+ } else if (op.action === 'done') {
126
+ if (cur.state === 'done') return { ok: false, error: kind === 'event' ? 'that meeting is already marked ready' : 'that item is already done' };
127
+ setState('done');
128
+ } else if (op.action === 'reopen') {
129
+ if (cur.state === null) return { ok: false, error: 'that item is already open' };
130
+ setState(null);
131
+ } else if (op.action === 'snooze') {
132
+ const t = laterToday(op.until, now, rec.date);
133
+ if (t.error) return { ok: false, error: t.error };
134
+ setState('snoozed', { until: iso(t.at) });
135
+ } else if (op.action === 'hold') {
136
+ if (kind !== 'item') return { ok: false, error: 'a meeting is not put on hold: snooze it, or skip prep' };
137
+ const on = clip(op.on, MAX_ON);
138
+ if (!on) return { ok: false, error: `on must say who or what you wait on, in 1 to ${MAX_ON} characters` };
139
+ if (typeof op.on === 'string' && op.on.trim().length > MAX_ON) return { ok: false, error: `on is over ${MAX_ON} characters` };
140
+ const t = laterToday(op.until, now, rec.date);
141
+ if (t.error) return { ok: false, error: t.error };
142
+ setState('waiting', { on, until: iso(t.at) });
143
+ } else if (op.action === 'dismiss') {
144
+ if (!REASONS[kind].includes(op.why)) return { ok: false, error: `why must be one of ${REASONS[kind].join(', ')}` };
145
+ setState('dismissed', { why: op.why });
146
+ }
147
+ if (!marks[id] && Object.keys(marks).length >= MAX_IDS) return { ok: false, error: `at most ${MAX_IDS} marks a day` };
148
+ const clean = cleanMark(cur, rec.date);
149
+ if (clean.state === null && !clean.steps && !clean.note) delete marks[id]; else marks[id] = clean;
150
+ return { ok: true, rec: { date: rec.date, marks }, mark: marks[id] || null, prev };
151
+ }
152
+
153
+ /** The marks as the page shows them now: a snooze or a hold whose time has passed is open again, with `back: true`. */
154
+ function effective(rec, now = Date.now()) {
155
+ const out = {};
156
+ for (const [id, m] of Object.entries((rec && rec.marks) || {})) {
157
+ const passed = (m.state === 'snoozed' || m.state === 'waiting') && Date.parse(m.until) <= now;
158
+ out[id] = passed ? { ...m, state: null, back: true, was: m.state } : { ...m };
159
+ }
160
+ return out;
161
+ }
162
+
163
+ module.exports = { STATES, ACTIONS, REASONS, MAX_ON, MAX_STEPS, MAX_NOTE, emptyMarks, readMarks, edit, effective };
@@ -15,6 +15,7 @@
15
15
  * newRun(date, now) · readRun(raw, date)
16
16
  * addTasks(run, brief) agent priorities (kind approve|auto) → waiting tasks
17
17
  * addBriefed(run, spec) one task the PM briefed in My work (initiative my-work, PRD R5)
18
+ * addSend(run, spec, now) the PM's own text, written in the card, to post as written (today-act-prepare)
18
19
  * toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
19
20
  * started · deliver · decide · sent · fail · sessionEnded · sessionStalled
20
21
  * stage(run, brief) reading | working | review | done
@@ -113,7 +114,8 @@ function addTasks(run, brief) {
113
114
  }
114
115
 
115
116
  /**
116
- * One task the PM briefed in My work: kind approve, with the PM's instruction and the item's context card on it, so the
117
+ * One task the PM briefed in My work, or a job of Your day the PM gave to the agent (`item_id`, initiative
118
+ * today-act-prepare, PRD C2): kind approve, with the PM's instruction and the item's context card on it, so the
117
119
  * day-task session reads the instruction as the ask (PRD R5, R6). `spec` is lib/my-work.js taskSpec. A run opened only
118
120
  * for this (no Prepare my day yet) is marked `opened_by: 'my-work'`, so it does not read as a morning read that failed.
119
121
  */
@@ -124,12 +126,43 @@ function addBriefed(run, spec) {
124
126
  if (!text(s.instruction)) return { ok: false, error: 'a briefed task needs the PM\'s instruction' };
125
127
  if (findTask(run, s.id)) return { ok: false, error: `task ${s.id} is already in today's run` };
126
128
  const next = clone(run);
127
- next.tasks.push({ id: s.id, item_id: null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
129
+ next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
128
130
  goal: null, my_work: s.my_work || null, brief_at: s.brief_at || null, instruction: text(s.instruction), context: s.context && typeof s.context === 'object' ? clone(s.context) : null,
129
131
  status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null });
130
132
  return { ok: true, run: next, added: s.id };
131
133
  }
132
134
 
135
+ /**
136
+ * The PM's own text, written in a Your day card, to post as written (initiative today-act-prepare, PRD F1 to F3). The press
137
+ * IS the approval: the task goes straight to `sending` with the PM's text as its draft (`by: 'pm'`), and one send session
138
+ * does that one outward step. A task already in the run for the item (the agent's draft) takes the PM's text instead.
139
+ * `spec`: { id, item_id, title, source, link, to, text, verdict? }.
140
+ */
141
+ function addSend(run, spec, now) {
142
+ if (!run) return { ok: false, error: 'no day run today' };
143
+ const s = spec && typeof spec === 'object' ? spec : {};
144
+ if (!TASK_RE.test(String(s.id || ''))) return { ok: false, error: 'a task needs an id like t-0123456789ab' };
145
+ const body = typeof s.text === 'string' ? s.text.trim() : '';
146
+ if (!body) return { ok: false, error: 'write the text to send' };
147
+ if (body.length > 4000) return { ok: false, error: 'the text is over 4000 characters' };
148
+ const to = text(s.to).slice(0, 120) || 'the place it came from';
149
+ const what = s.verdict === 'approve' ? 'review, approving' : s.verdict === 'changes' ? 'review, asking for changes' : 'reply';
150
+ const at = iso(now);
151
+ const draft = { summary: `The PM's own ${what}: post it as written to ${to}`.slice(0, 240), body, link: isUrl(s.link) ? s.link : null, at, by: 'pm' };
152
+ const next = clone(run);
153
+ const t = findTask(next, s.id);
154
+ if (t) {
155
+ if (['working', 'sending', 'sent'].includes(t.status)) return { ok: false, error: `task ${s.id} is ${t.status} — wait for it, or open its session` };
156
+ keepSession(t);
157
+ Object.assign(t, { kind: 'approve', status: 'sending', draft, decided: { action: 'approve', at, by: 'pm' }, error: null, started_at: null });
158
+ return { ok: true, run: next, task: s.id };
159
+ }
160
+ next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve',
161
+ source: s.source || 'today', link: isUrl(s.link) ? s.link : null, goal: null, instruction: `Post the PM's own text exactly as written to ${to}. Change nothing.`,
162
+ status: 'sending', session_id: null, started_at: null, attempts: 0, revisions: [], draft, decided: { action: 'approve', at, by: 'pm' }, error: null });
163
+ return { ok: true, run: next, task: s.id };
164
+ }
165
+
133
166
  /**
134
167
  * The tasks to start now, oldest first, so that the sessions running (working, and sending with a session) stay at
135
168
  * most `limit`. An approved task waiting for its send session comes first: the PM is waiting on it.
@@ -208,7 +241,8 @@ function decide(run, taskId, op, now) {
208
241
  }
209
242
  if (t.status !== 'failed') return `task ${taskId} is ${t.status} — only a failed task can be run again`;
210
243
  const stalled = !!t.stalled_session;
211
- t.status = 'waiting'; keepSession(t); t.error = null; t.decided = { action: 'retry', at };
244
+ // the PM's own text is sent again as it is: a retry never asks an agent to draft over it
245
+ t.status = t.draft && t.draft.by === 'pm' ? 'sending' : 'waiting'; keepSession(t); t.error = null; t.decided = { action: 'retry', at };
212
246
  if (stalled) { t.prev_session = null; delete t.stalled_session; delete t.stalled_status; }
213
247
  return null;
214
248
  });
@@ -350,5 +384,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
350
384
  }
351
385
 
352
386
  module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
353
- newRun, readRun, findTask, modeOf, addTasks, addBriefed, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
387
+ newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, sessionEnded, sessionStalled, stage, intervalMin,
354
388
  checkDue, checkStarted, checkDone };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.73.0",
3
+ "version": "0.74.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",
@@ -3346,6 +3346,40 @@
3346
3346
  .td-day__draftbox { display: grid; gap: 4px; }
3347
3347
  .td-day__draft { margin: 0; max-height: 220px; overflow: auto; padding: 8px 10px; white-space: pre-wrap; font: 12px/1.5 var(--mono); background: var(--muted); color: var(--foreground); }
3348
3348
  .td-day__revise { display: grid; gap: 6px; }
3349
+ /* act and prepare (initiative today-act-prepare): a verb first, one sentence, the steps, one button that fits, the marks */
3350
+ .td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.05em; text-transform: uppercase; text-align: center; color: var(--foreground); white-space: nowrap; }
3351
+ .td-day__verb[data-verb="decide"], .td-day__verb[data-verb="prepare"] { color: var(--warning); }
3352
+ .td-day__acth { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
3353
+ .td-day__todo { margin: 0; padding: 7px 10px; border-left: 3px solid var(--foreground); background: var(--muted); font-size: 14px; line-height: 1.45; color: var(--foreground); }
3354
+ .td-day__steps2 { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
3355
+ .td-day__step2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 8px; border: 1px solid var(--border); background: var(--background); }
3356
+ .td-day__step2[data-done="true"] .td-day__steprow { text-decoration: line-through; color: var(--muted-foreground); }
3357
+ .td-day__marks { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 8px; border-top: 1px solid var(--border); }
3358
+ .td-day__grow { flex: 1 1 0; }
3359
+ .td-day__mark { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 10px; border: 1px solid var(--border); background: var(--muted); font-size: 14px; }
3360
+ .td-day__mark[data-day-mark-state="done"] { border-color: var(--foreground); }
3361
+ .td-day__ready { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 12px; }
3362
+ .td-day__prep { display: grid; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
3363
+ .td-day__fold { border-top: 1px solid var(--border); padding-top: 6px; }
3364
+ .td-day__fold > summary { cursor: pointer; font-size: 12px; color: var(--muted-foreground); }
3365
+ .td-day__fold[open] > summary { margin-bottom: 6px; }
3366
+ .td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
3367
+ @media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary { min-height: 44px; } }
3368
+ .td-day__fold-head { justify-self: start; }
3369
+ .td-day__steprow { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3370
+ .td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox) { flex-direction: column; align-items: stretch; }
3371
+ .td-day__ref { display: grid; gap: 4px; padding: 6px 8px; background: var(--muted); font-size: 14px; }
3372
+ .td-day__ref > summary { display: flex; justify-content: space-between; gap: 8px; cursor: pointer; font-weight: 600; color: var(--foreground); }
3373
+ .td-day__reflines { margin: 0; padding-left: 18px; display: grid; gap: 2px; color: var(--foreground); line-height: 1.45; }
3374
+ .td-day__thread { display: grid; gap: 6px; }
3375
+ .td-day__msg { display: grid; gap: 2px; padding: 6px 10px; border-left: 2px solid var(--border); }
3376
+ .td-day__msg p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--foreground); }
3377
+ .td-day__msg--last { border-left-color: var(--foreground); }
3378
+ .td-day__compose { display: grid; gap: 6px; padding: 8px 10px; border: 1px solid var(--border); background: var(--background); }
3379
+ .td-day__chipsrow { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
3380
+ .td-day__notebox { display: grid; gap: 4px; }
3381
+ .td-day__a--primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
3382
+ @media (hover: hover) { .td-day__a--primary:hover { color: var(--primary-foreground); opacity: 0.9; } }
3349
3383
  .td-day__log { font-size: 12px; color: var(--muted-foreground); }
3350
3384
  .td-day__ch { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: 14px; color: var(--foreground); }
3351
3385
  .td-day__slist { display: grid; }
@@ -159,7 +159,7 @@ export const COPY = {
159
159
  all: 'All',
160
160
  status2: {
161
161
  waiting: 'waiting', working: 'agent working', review: 'approve', reviewAuto: 'check it', sending: 'sending', sent: 'sent',
162
- done: 'done', failed: 'failed', you: 'your act', notStarted: 'not started', handled: 'handled', replyNeeded: 'reply needed',
162
+ done: 'done', failed: 'failed', notStarted: 'not started', handled: 'handled', replyNeeded: 'reply needed',
163
163
  },
164
164
  newItem: 'new',
165
165
  pill: { prepare: 'prepare', ready: 'ready', ended: 'ended', notes: 'notes', goal: (n) => `goal ${n}` },
@@ -189,8 +189,6 @@ export const COPY = {
189
189
  prep: 'Prepare',
190
190
  phases: { before: 'Before', during: 'During', after: 'After' },
191
191
  who: { pm: 'you', agent: 'agent' },
192
- needPrep: 'You need to prepare',
193
- nothingToPrep: 'Nothing to prepare',
194
192
  initiative: 'Initiative',
195
193
  suggested: (why) => `Suggested. ${why}`,
196
194
  openInitiative: 'Open the initiative',
@@ -235,7 +233,6 @@ export const COPY = {
235
233
  waitingNote: 'The agent has not started this job yet. At most 5 sessions run at once.',
236
234
  workingNote: 'An agent session is doing this job now.',
237
235
  sendingNote: 'You said yes to this draft. An agent session is sending it now.',
238
- youNote: 'Only you can do this one. The link opens the conversation.',
239
236
  autoNote: 'The agent did this alone. Nothing went outside this machine.',
240
237
  notTask: 'The agent starts this job when you press Prepare my day.',
241
238
  related: 'Related',
@@ -351,6 +348,87 @@ export const COPY = {
351
348
  today: 'Today',
352
349
  },
353
350
  },
351
+ /** The items the PM acts on and the meetings the PM prepares for (initiative today-act-prepare, PRD A1 to D4). A verb
352
+ * first, one sentence of what to do, then the steps, one button that fits the first open step, and the marks. */
353
+ act: {
354
+ verb: { reply: 'Reply', review: 'Review', decide: 'Decide', 'sign-off': 'Sign off', read: 'Read', do: 'Do', prepare: 'Prepare', approve: 'Approve' },
355
+ groups: { later: 'Snoozed', waiting: 'Waiting on others', done: 'Done today' },
356
+ by: (at) => `by ${at}`,
357
+ at: (at) => `at ${at}`,
358
+ left: (min) => (min <= 0 ? 'now' : min < 60 ? `in ${min} min` : `in ${Math.floor(min / 60)} h ${min % 60} min`),
359
+ back: (at) => `back ${at}`,
360
+ check: (at) => `ask ${at}`,
361
+ chip: { done: 'done', ready: 'ready', dismissed: 'not mine', skipped: 'no prep', today: 'today' },
362
+ from: (who, where) => (who && where ? `${who} in ${where}` : who || where || ''),
363
+ howTitle: (d, n) => `How to do it · ${d} of ${n}`,
364
+ beforeTitle: (at) => `Before ${at}`,
365
+ more: 'More context',
366
+ moreMeeting: (during, after) => `In the meeting (${during}) · After (${after})`,
367
+ readOnly: 'Read only. Nothing is sent before you press a button that says so.',
368
+ open: { slack: 'Reply in Slack ↗', jira: 'Open in Jira ↗', confluence: 'Open in Confluence ↗', github: 'Open on GitHub ↗', figma: 'Open in Figma ↗',
369
+ mail: 'Open the mail ↗', drive: 'Open in Google Drive ↗', calendar: 'Open the event ↗', wispr: 'Open the notes ↗', website: 'Open the page ↗' },
370
+ openLink: 'Open the link ↗',
371
+ startStep: (t) => `Start: ${t}`,
372
+ askAgentStep: 'Ask the agent',
373
+ draft: 'Have the agent draft it',
374
+ drafting: 'The agent is drafting. The draft shows here, and nothing is sent before you approve it.',
375
+ draftStarted: 'The agent is drafting it. Approve or revise the draft when it is ready.',
376
+ done: 'Mark done',
377
+ ready: 'Mark ready',
378
+ snooze: 'Snooze',
379
+ remind: 'Remind me',
380
+ hold: 'Waiting on…',
381
+ notMine: 'Not mine',
382
+ skip: 'Skip prep',
383
+ snoozeTo: { hour: 'In 1 hour', later: 'Later today, 16:00', due: 'Before it is due', end: 'End of the day, 17:30', m30: '30 min before it', m15: '15 min before it' },
384
+ holdWho: 'Who or what you wait on',
385
+ holdHint: 'For example: Raj, or the cost model',
386
+ holdWhen: 'Ask again',
387
+ holdWhenOpts: { h2: 'In 2 hours', end: 'At 17:30' },
388
+ holdGo: 'Put on hold',
389
+ holdNeed: 'Say who or what you wait on, in 1 to 80 characters.',
390
+ why: { 'no-action': 'No action needed', 'someone-else': 'Someone else handles it', 'done-elsewhere': 'It is already done', nothing: 'Nothing to prepare', elsewhere: 'I prepare elsewhere' },
391
+ did: {
392
+ done: (t) => `Done: ${t}.`, ready: (t) => `Ready: ${t}.`, snooze: (at) => `Snoozed. It comes back at ${at}.`,
393
+ hold: (who, at) => `Waiting on ${who}. Today asks you again at ${at}.`, dismiss: (why) => `${why}. Taken off Needs you.`, reopen: 'Back in Needs you.',
394
+ },
395
+ undo: 'Undo',
396
+ undone: 'Put back as it was.',
397
+ state: {
398
+ snoozed: (at) => `Snoozed. It comes back to Needs you at ${at}.`,
399
+ waiting: (who, at) => `On hold: waiting on ${who}. Today asks you again at ${at}.`,
400
+ done: (at) => `Done at ${at}.`, ready: (at) => `Ready at ${at}.`,
401
+ dismissed: (why) => `Not on your list: ${why}.`,
402
+ back: 'Back from snooze or hold: the time you set has come.',
403
+ },
404
+ bringBack: 'Bring it back now',
405
+ reopen: 'Reopen',
406
+ readiness: (d, n) => `${d} of ${n} ready`,
407
+ fit: { ok: 'Your prep is done.', time: (left, free) => `${left} ${left === 1 ? 'step' : 'steps'} left, ${free} min free before it.`,
408
+ short: (left, free) => `${left} ${left === 1 ? 'step' : 'steps'} left and ${free} min free. Give a step to the agent, or snooze.` },
409
+ noSteps: 'No steps were given. The sentence above says what to do.',
410
+ stepFailed: (why) => `The step was not saved: ${why}`,
411
+ // the context and the answer in the card (PRD X1, X2, F1 to F3)
412
+ threadTitle: 'The conversation',
413
+ refsTitle: 'What the sources say',
414
+ refRead: 'Read: ',
415
+ refOpen: 'Open the source ↗',
416
+ compose: { reply: 'Your reply', review: 'Your review', decide: 'Your answer', 'sign-off': 'Your sign-off' },
417
+ composeHint: 'Write it here, or start from a suggestion.',
418
+ startFrom: 'Start from:',
419
+ verdictLabel: 'Your verdict',
420
+ verdict: { approve: 'Approve', changes: 'Ask for changes' },
421
+ sendTo: (to) => `Send to ${to}`,
422
+ postReview: (to) => `Post the review to ${to}`,
423
+ sendNote: 'When you press, the agent posts exactly this text, as you, to that place. Nothing else is sent.',
424
+ sendNeed: 'Write the text to send first.',
425
+ verdictNeed: 'Choose Approve or Ask for changes first.',
426
+ sendStarted: (to) => `Sending to ${to}. The job shows Sent when it is posted.`,
427
+ noteTitle: 'Your note',
428
+ noteHint: 'Write it here. It is saved when you leave the box.',
429
+ noteStays: 'Saved with the meeting on this machine. Nothing is sent.',
430
+ noteSaved: 'Note saved.',
431
+ },
354
432
  /** The Acceptance Gate card (D12): the PM's own act on a run. */
355
433
  accept: {
356
434
  review: 'Review',