create-pmos 0.225.0 → 0.227.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.225.0",
3
+ "version": "0.227.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@6a10a1d
1
+ kernel-source: pmos@87765df
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-14T06:35:37Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T07:02:16Z"}
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-14T06:35:37Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T07:02:16Z"}
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-14T06:35:37Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T07:02:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -30,7 +30,7 @@ const { harness } = require('./c3-ini-harness');
30
30
  const pf = await rows();
31
31
  ok('P3 the core set lists 11 documents, strategy first', pf.split(',').length === 11 && pf.startsWith('strategy,okrs,roadmap,invest'), pf);
32
32
  ok('P4 states: up to date, draft ready, due, comes later where it applies', (await rowState('strategy')) === 'ok' && (await rowState('roadmap')) === 'draft' && (await rowState('status')) === 'due');
33
- ok('P5 a row shows its question, cadence, reader and who writes it', await st(() => { const t = document.querySelector('[data-cd-row="strategy"]').textContent; return /Yearly, reviewed each quarter/.test(t) && /Leadership, every PM/.test(t) && /You write it/.test(t) && /new/.test(t) && /2 key results do not trace to it/.test(t); }));
33
+ ok('P5 a row shows its question, cadence, reader and who writes it', await st(() => { const t = document.querySelector('[data-cd-row="strategy"]').textContent; return /Yearly, reviewed each quarter/.test(t) && /Leadership, every PM/.test(t) && /PMOS proposes/.test(t) && /new/.test(t) && /2 key results do not trace to it/.test(t); }));
34
34
  ok('P6 the right column says what the level is for, lists the drafts and how the levels connect', await st(() => { const l = document.querySelector('[data-cd="level"]'); return !!l && /Are we betting on the right things\?/.test(l.textContent) && document.querySelectorAll('[data-cd="drafts"] [data-cd-wait]').length === 2 && !!document.querySelector('[data-cd="connect"]'); }));
35
35
  ok('P7 the address holds the Documents view', await st(() => /init=docs/.test(location.search)));
36
36
  ok('P8 the summary line counts the drafts and what is due', await st(() => /2 drafts to review, 1 due/.test(document.querySelector('[data-cd="core"]').textContent)));
@@ -29,7 +29,6 @@ Risks are written once, on the initiative. Board risks and cross-board risks col
29
29
  | Due | The cadence passed, or the current stage needs the document and it is not there. |
30
30
  | Draft ready | PMOS drafted it. It waits for you. |
31
31
  | Not started | Nothing is there, and the level needs it now. |
32
- | None yet | Nothing is there yet, and this fills as the work goes: the decision log, kill conditions, market and user context. |
33
32
  | Comes later | The initiative has not reached the stage that needs it. |
34
33
  | Stale | It was issued before a change it depends on: a stage move (initiative risks, launch checklist, status update), a newer risk list or outcome review (rolled-up documents), or a change to the board settings (board charter). |
35
34
 
@@ -39,13 +38,15 @@ A row that is not started always has a next step:
39
38
 
40
39
  - **Draft it**: PMOS drafts a document it keeps itself.
41
40
  - **Propose them** (OKRs and health budgets): with no OKR files, PMOS drafts proposed key results and health budgets for you to review and issue. Once OKR files exist, they are the source.
41
+ - **Propose it** (decision log, kill conditions, market and user context): until the real source exists, PMOS drafts one from what it has read. The decision log comes from meeting notes and initiative records. Kill conditions come from each bet and its key result. Market and user context comes from research notes and Horizon scans. You review and issue it. Once the real source exists (a decisions file, kill conditions on the initiatives, research under `planning/research/` or `planning/radar/`), that source is used instead.
42
+ - **Draft it** on the product strategy: PMOS proposes a strategy from your key results, board charters and initiatives. It is a proposal only. You change it, then issue it or drop it.
42
43
  - **Run a scan** (Horizon scan reports): opens the Portfolio Overview at Horizon scan, where Scan now is.
43
44
  - **Set it up** (status reports): opens Reports, where you set up the report.
44
45
 
45
46
  ## How PMOS drafts a document
46
47
 
47
48
  - **When you ask.** Press **Draft it** on a row, or **Ask PMOS to change it** on a draft. PMOS starts one background run. The run reads the sources of that document and writes one draft. It has no tool that sends, posts or edits.
48
- - **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While no OKR files exist, it also proposes the OKRs. There is no limit on how many drafts run at once. If a run writes nothing, PMOS tries again on the next tick. If you drop a draft, PMOS does not draft that document again until the next day. It never drafts a document that you write (the product strategy).
49
+ - **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While their sources do not exist, it also proposes the OKRs, the decision log, kill conditions and market and user context. There is no limit on how many drafts run at once. If a run writes nothing, PMOS tries again on the next tick. If you drop a draft, PMOS does not draft that document again until the next day. The product strategy is proposed when it is not started or its quarterly review is due.
49
50
  - **The Initiative Driver.** When the current stage of an initiative needs a document that is not there or is stale, the driver drafts it and files a proposal. **Keep** issues the draft. The driver does not propose a stage move while a document of the current stage is due.
50
51
 
51
52
  ## Issue a draft
@@ -134,7 +134,9 @@ Or choose one option:
134
134
  - **Keep it as a draft** leaves the draft as it is.
135
135
  - **Mark it done** closes the item. Nothing is posted.
136
136
 
137
- The agent only drafts. It never posts to Jira until you approve the draft.
137
+ Some work needs an action in another system, for example creating an exploration in GA4. The agent does not try that action while it drafts. The draft names the action as its step, and the card shows **Step when you approve**. Then the first option is **Do it**, in place of **Post it to**, also on an item you added. Hold to confirm. A new job session starts with Claude in Chrome and permission to use the browser tools only, does that one step, and reports what it made. Chrome must be open with the Claude extension, and you must be signed in to the system.
138
+
139
+ The agent only drafts. It never posts to Jira or acts in another system until you approve the draft.
138
140
 
139
141
  ### Defer and flag
140
142
 
@@ -49,17 +49,17 @@ const LEVEL = Object.freeze({
49
49
  const e = (level, o) => Object.freeze({ level, reader: null, cadence: null, build: false, cat: null, isNew: false, days: null, rollup: null, tool: null, store: false, ...o });
50
50
  const SETS = Object.freeze({
51
51
  portfolio: Object.freeze([
52
- e('portfolio', { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', kind: 'doc', days: 91, isNew: true }),
52
+ e('portfolio', { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'proposes', kind: 'doc', days: 91, isNew: true }),
53
53
  e('portfolio', { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', kind: 'derived', store: true, days: 91 }),
54
54
  e('portfolio', { key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', kind: 'doc', days: 30 }),
55
55
  e('portfolio', { key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', kind: 'doc', days: 91, isNew: true }),
56
56
  e('portfolio', { key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', kind: 'derived' }),
57
57
  e('portfolio', { key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', kind: 'report' }),
58
- e('portfolio', { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', kind: 'derived' }),
59
- e('portfolio', { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', kind: 'derived' }),
58
+ e('portfolio', { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', kind: 'derived', store: true }),
59
+ e('portfolio', { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', kind: 'derived', store: true }),
60
60
  e('portfolio', { key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', kind: 'doc', days: 30, rollup: 'risks', isNew: true }),
61
61
  e('portfolio', { key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', kind: 'doc', days: 91, rollup: 'outcome', isNew: true }),
62
- e('portfolio', { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', kind: 'derived' }),
62
+ e('portfolio', { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', kind: 'derived', store: true }),
63
63
  ]),
64
64
  board: Object.freeze([
65
65
  e('board', { key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', kind: 'doc', isNew: true }),
@@ -120,7 +120,8 @@ const levelOfScope = (scope) => { const p = parseScope(scope); return p ? p.leve
120
120
  const scopeDir = (scope) => String(scope).replace(':', '-');
121
121
  /** A doc entry PMOS may draft: kind doc, and not one the PM writes. */
122
122
  const draftable = (entry) => !!entry && entry.kind === 'doc' && entry.by !== 'you';
123
- /** A derived entry with `store` (the OKRs) is read from its own source, and PMOS may propose it as a core document while that source is absent. */
123
+ /** A derived entry with `store` (the OKRs, the decision log, kill conditions, market context) is read from its own source, and PMOS may propose
124
+ * it as a core document while that source is absent. */
124
125
  const storable = (entry) => !!entry && (entry.kind === 'doc' || !!entry.store);
125
126
  /** A derived entry that fills as the work goes: with no source yet it is "none", not missing (portfolio and board only). */
126
127
  const FILLS = Object.freeze(['files', 'watches', 'keeps']);
@@ -573,7 +574,7 @@ function actOf(entry, st) {
573
574
  if (st === 'stale' && draftable(entry)) return { label: 'Draft it', do: 'draft' };
574
575
  if (st !== 'missing') return null;
575
576
  if (draftable(entry)) return { label: 'Draft it', do: 'draft' };
576
- if (entry.store && entry.by !== 'you') return { label: 'Propose them', do: 'draft' };
577
+ if (entry.store && entry.by !== 'you') return { label: entry.key === 'okrs' ? 'Propose them' : 'Propose it', do: 'draft' };
577
578
  if (entry.key === 'scan') return { label: 'Run a scan', go: 'scan' };
578
579
  if (entry.kind === 'report' && entry.level !== 'initiative') return { label: 'Set it up', go: 'reports' };
579
580
  return null;
@@ -623,8 +624,8 @@ function driverLists(set) {
623
624
  // ── the core-doc run's prompt ────────────────────────────────────────────────────────────────────────
624
625
  const S = '<state>'; // the state folder, named in the prompt as the run sees it
625
626
  const GUIDE = Object.freeze({
626
- strategy: { sources: ['The issued product strategy, if any, under <state>/core-docs/portfolio/strategy/', 'planning/okrs/ (the key results and health budgets)', 'The board charters under <state>/core-docs/board-*/charter/'],
627
- sections: ['Where we are going', 'Where we play (one line per pillar, with its boards)', 'What we will not do', 'How we will know (a table: Pillar, Key results, Boards)'] },
627
+ strategy: { sources: ['The issued product strategy, if any, under <state>/core-docs/portfolio/strategy/', 'planning/okrs/ (the key results and health budgets)', 'The board charters under <state>/core-docs/board-*/charter/', 'The boards and their initiatives with their anchors (`pmos board`, `pmos initiative list`)', 'The Horizon scan reports and the research under planning/radar/ and planning/research/, if any'],
628
+ sections: ['A short line: this is a proposal for the PM to change, keep or drop; the PM issues the strategy', 'Where we are going', 'Where we play (one line per pillar, with its boards)', 'What we will not do', 'How we will know (a table: Pillar, Key results, Boards)'] },
628
629
  roadmap: { sources: ['`pmos initiative list` (every initiative with its horizon, stage and anchor)', 'planning/okrs/ (the key results the bets serve)', 'The issued portfolio roadmap before this one, if any'],
629
630
  sections: ['Now (each bet, its board and the key result it serves)', 'Next', 'Later', 'What changed since the last roadmap'] },
630
631
  invest: { sources: ['`pmos initiative list` (anchors: key result or health budget)', 'The plan items and their points on every board (`pmos board`)', 'The people and their points a week (`pmos people list`)', 'The Jira snapshots under the state folder, if any (read-only)'],
@@ -647,6 +648,12 @@ const GUIDE = Object.freeze({
647
648
  sections: ['Patterns (each with the initiatives it comes from)', 'Keep, change, try (a table)'] },
648
649
  okrs: { sources: ['The product strategy, if one is issued: <state>/core-docs/portfolio/strategy/', 'The boards and their initiatives with their anchors (`pmos board`)', 'planning/okrs/health-budgets.md, if any', 'The KR metric records and their readings (planning/records/kr-metrics.jsonl, `pmos measure`), if any'],
649
650
  sections: ['A short line: these are proposals for the PM to change, keep or drop', 'Objectives, each with two to four key results (a table: Key result, Metric, Now, Target, Initiatives that serve it); write "not known" where no reading exists', 'Health budgets for upkeep work (a table: Budget, What it covers, Limit)', 'Initiatives with no key result to anchor to, if any'] },
651
+ decisions: { sources: ['Meeting notes and their summaries in the state folder (decisions, agreed, chose, will not)', 'The initiative records and their stage moves, holds, cancels and merges (`pmos initiative list`)', 'The proposals the PM kept or dropped (the driver ledger), if any', 'The issued decision log before this one, if any: <state>/core-docs/portfolio/decisions/'],
652
+ sections: ['A short line: these are decisions found in the sources, for the PM to keep, correct or drop', 'A table: Date, Decision, Why, Boards, Who decided, Source', 'Open questions that look like decisions still to make'] },
653
+ kill: { sources: ['Every initiative with its bet, its anchor and its kill conditions, if any (`pmos initiative list`, the stage tools)', 'planning/okrs/ and the key result readings (`pmos measure` records), if any', 'The issued kill conditions before these, if any: <state>/core-docs/portfolio/kill/'],
654
+ sections: ['A short line: these are proposed kill conditions for the PM to change, keep or drop; the PM writes them onto each initiative', 'A table: Initiative, Bet, Key result, Kill condition (a number and a date), Reading now', 'Bets with no key result to measure, if any'] },
655
+ context: { sources: ['The research under planning/research/ and the Horizon scan reports under planning/radar/, if any', 'Meeting notes that name users, customers, competitors or the market', 'The problem statements and research notes of the initiatives (the stage tools)', 'The issued market and user context before this one, if any: <state>/core-docs/portfolio/context/'],
656
+ sections: ['A short line: this is a proposal built from what PMOS has read, for the PM to change, keep or drop', 'Jobs to be done (a table: Who, Job, Evidence)', 'The market map (a table: Player, What they offer, Where we differ)', 'What we do not know yet'] },
650
657
  risks: { sources: ['The initiative record and every file it names (PRD, design, rubric)', 'The Jira snapshot of the initiative, if any (blocked and late tickets)', 'Meeting notes and requirement notes that name the initiative'],
651
658
  sections: ['A table: Risk, Impact (High, Medium, Low), Likelihood, Next step, Owner, Since', 'Waits on other boards, if any'] },
652
659
  outcome: { sources: ['The key result readings since the release (`pmos measure` records)', 'The release that shipped it (the releases ledger)', 'The initiative bet, its kill conditions and its learnings (the stage tools)', 'Support tickets and notes that name it, if any'],
@@ -204,14 +204,21 @@ function deliver(run, taskId, d, now) {
204
204
  if (!body) return 'a draft needs a body';
205
205
  if (body.length > 8000) return 'the body is over 8000 characters';
206
206
  if (b.link !== undefined && b.link !== null && b.link !== '' && !isUrl(b.link)) return 'the link must be an http(s) address of at most 500 characters';
207
- t.draft = { summary, body, link: b.link || null, at: iso(now) };
208
- logTurn(t, { who: 'agent', kind: 'draft', at: iso(now), text: summary, body });
207
+ // the one step in another system that approval triggers (amendment 17), such as "Create the cohort exploration in GA4"
208
+ const step = text(b.step);
209
+ if (step.length > 240) return 'the step is over 240 characters';
210
+ if (step && t.kind !== 'approve') return 'only a task the PM approves can name a step';
211
+ t.draft = { summary, body, link: b.link || null, at: iso(now), ...(step ? { step } : {}) };
212
+ logTurn(t, { who: 'agent', kind: 'draft', at: iso(now), text: summary, body, ...(step ? { step } : {}) });
209
213
  t.status = t.kind === 'auto' ? 'done' : 'review';
210
214
  t.error = null;
211
215
  return null;
212
216
  });
213
217
  }
214
218
 
219
+ /** The step in another system that a send of task `t` does (amendment 17), or null when the send posts the draft. */
220
+ const stepOf = (t) => (t && t.draft && typeof t.draft.step === 'string' && t.draft.step.trim() ? t.draft.step.trim() : null);
221
+
215
222
  /** The task leaves its session: `prev_session` keeps its id, so the next step can run in the same session while it is open. */
216
223
  function keepSession(t) {
217
224
  if (t.session_id) t.prev_session = t.session_id;
@@ -451,5 +458,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
451
458
  }
452
459
 
453
460
  module.exports = { MAX_PARALLEL, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
454
- newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, edit, carry, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
461
+ newRun, readRun, findTask, modeOf, stepOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, edit, carry, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
455
462
  checkDue, checkStarted, checkDone };
@@ -194,7 +194,7 @@ function stateOf(item, presses, runs) {
194
194
  if (!last || last.action === 'reopen') return { state: 'brief', sub: null, task: null, instruction: null };
195
195
  if (last.action === 'handle') return { state: 'yours', sub: null, task: null, instruction: null, decided_at: last.at };
196
196
  const t = taskOf(runs, last);
197
- const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null, edited_at: t.draft ? t.draft.edited_at || null : null,
197
+ const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null, edited_at: t.draft ? t.draft.edited_at || null : null, step: t.draft ? t.draft.step || null : null,
198
198
  sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
199
199
  session: t.stalled_session || null, asks: t.asks || null, log: Array.isArray(t.log) ? t.log : null } : null;
200
200
  const base = { task, instruction: last.instruction, decided_at: last.at };
@@ -20,7 +20,7 @@
20
20
  * pmos proactive goals [list] | add TEXT [--initiative ID] | done N | drop N | reopen N | remove N | plan [--repo PATH]
21
21
  * pmos proactive day [--repo PATH] the day run: its stage, its tasks and the daytime check
22
22
  * pmos proactive day show TASK one task with its item, draft and revision notes (what day-task reads)
23
- * pmos proactive day deliver TASK --summary S --body-file F [--link URL]
23
+ * pmos proactive day deliver TASK --summary S --body-file F [--link URL] [--step S]
24
24
  * pmos proactive day sent TASK [--link URL] | day fail TASK --reason R
25
25
  * pmos proactive day approve|revise|retry TASK [--note N] | day check
26
26
  *
@@ -233,7 +233,7 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
233
233
  }
234
234
  if (verb === 'day') {
235
235
  const sub = pos[0] || 'status';
236
- const usage = 'usage: pmos proactive day [status] | show TASK | deliver TASK --summary S --body-file F [--link URL] | sent TASK [--link URL] | fail TASK --reason R | approve|revise|retry TASK [--note N] | check';
236
+ const usage = 'usage: pmos proactive day [status] | show TASK | deliver TASK --summary S --body-file F [--link URL] [--step S] | sent TASK [--link URL] | fail TASK --reason R | approve|revise|retry TASK [--note N] | check';
237
237
  const wsOfRepo = async () => {
238
238
  let root; try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: flags.repo ? path.resolve(cwd, flags.repo) : cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
239
239
  const wl = await call('GET', '/api/workspaces');
@@ -307,6 +307,8 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
307
307
  say(`draft (${t.draft.at}): ${t.draft.summary}${t.draft.link ? ' · ' + t.draft.link : ''}`);
308
308
  // the PM edited the draft (c3-work-agent amendment 13): the body below is the PM's text, to post as it is
309
309
  if (t.draft.edited_at) say(`edited by the PM (${t.draft.edited_at}): the text below is the PM's own. Post it exactly as written.`);
310
+ // the one step in another system that the PM approves (c3-work-agent amendment 17); a send does this step, not a post
311
+ if (t.draft.step) say(`step: ${t.draft.step}${mode === 'send' ? ' — approved by the PM. Do exactly this step with Claude in Chrome, and nothing else.' : ' — runs only after the PM approves it.'}`);
310
312
  say(t.draft.body);
311
313
  }
312
314
  t.revisions.forEach((rv, i) => say(`revision note ${i + 1} (${rv.at}): ${rv.note}`));
@@ -316,9 +318,9 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
316
318
  return 0;
317
319
  }
318
320
  if (sub === 'deliver') {
319
- if (!flags.summary || !flags['body-file']) return fail('usage: pmos proactive day deliver TASK --summary S --body-file F [--link URL]');
321
+ if (!flags.summary || !flags['body-file']) return fail('usage: pmos proactive day deliver TASK --summary S --body-file F [--link URL] [--step S]');
320
322
  let body; try { body = fs.readFileSync(path.resolve(cwd, flags['body-file']), 'utf8'); } catch (e) { return fail(`could not read ${flags['body-file']} — ${e.message}`); }
321
- return post('deliver', { task: t.id, summary: flags.summary, body, ...(flags.link ? { link: flags.link } : {}) }, (r) => `delivered — ${t.id} is now ${r.task.status}`);
323
+ return post('deliver', { task: t.id, summary: flags.summary, body, ...(flags.link ? { link: flags.link } : {}), ...(flags.step ? { step: flags.step } : {}) }, (r) => `delivered — ${t.id} is now ${r.task.status}`);
322
324
  }
323
325
  if (sub === 'sent') return post('sent', { task: t.id, ...(flags.link ? { link: flags.link } : {}) }, () => `sent — ${t.id}`);
324
326
  if (sub === 'fail') {
@@ -272,7 +272,8 @@ function agentItemPress(ctx, res, verb, body, itemId, now) {
272
272
  const t = run ? dayRun.findTask(run, taskId) : null;
273
273
  if (!t) { ctx.json(res, { ok: false, error: 'no agent works on this item — let the agent draft it first' }, 409); return; }
274
274
  let next = null;
275
- 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; }
275
+ // an item the PM added has no place to post to; a draft that names a step in another system (amendment 17) can still be approved
276
+ if (verb === 'decide' && body.action === 'approve' && !g.postable && !dayRun.stepOf(t)) { 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; }
276
277
  if (verb === 'decide' && run.date !== localDate(now) && (body.action === 'revise' || body.action === 'retry')) {
277
278
  // 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
278
279
  const note = typeof body.note === 'string' ? body.note.trim().slice(0, 500) : '';
@@ -31,7 +31,7 @@ function agentOf(x) {
31
31
  if (!state) return null;
32
32
  return {
33
33
  state, suggest: x.suggest || null, instruction: x.instruction || null,
34
- draft: t && (t.draft || t.body) ? { summary: t.draft || null, body: t.body || null, edited_at: t.edited_at || null } : null,
34
+ draft: t && (t.draft || t.body) ? { summary: t.draft || null, body: t.body || null, edited_at: t.edited_at || null, step: t.step || null } : null,
35
35
  sent: t && t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null,
36
36
  error: t ? t.error || null : null, session: t && t.session ? String(t.session) : null, asks: t ? t.asks || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null, log: t ? t.log || null : null,
37
37
  };
@@ -77,7 +77,7 @@ function taskAgent(taskId, suggest, run) {
77
77
  const base = { suggest, instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null, log: null };
78
78
  if (!t) return { state: 'suggest', ...base };
79
79
  return { ...base, state: TASK_STATE[t.status] || 'queued', instruction: t.instruction || null,
80
- draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null, edited_at: t.draft.edited_at || null } : null,
80
+ draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null, edited_at: t.draft.edited_at || null, step: t.draft.step || null } : null,
81
81
  sent: t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null, error: t.error || null,
82
82
  session: t.stalled_session ? String(t.stalled_session) : null, asks: t.asks || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
83
83
  started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null, log: Array.isArray(t.log) ? t.log : null };
@@ -94,7 +94,7 @@ function localSuggest(item) { return `Draft a first version: ${String(item.title
94
94
  function localDraftSpec(item, instruction) {
95
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
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.`,
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. If the ask needs one action in another system, such as creating a report in GA4, name that action as the draft's step: it runs only after the PM approves it.`,
98
98
  context: { about: item.title, due: item.due || null, from: item.person || null, ask: item.waiting_on ? 'waiting on ' + item.waiting_on : null } };
99
99
  }
100
100
  const localAgent = (item, run) => taskAgent(localTaskId(item), localSuggest(item), run);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.225.0",
3
+ "version": "0.227.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.225.0",
9
+ "version": "0.227.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.225.0",
3
+ "version": "0.227.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",
@@ -19,17 +19,17 @@ const RHYTHM = { portfolio: 'each quarter, with a monthly pulse', board: 'every
19
19
 
20
20
  /* the core sets: key, title, ic, for, reader, cadence, by, kind (doc | live | report), cat, isNew, state, when, due, flag, home */
21
21
  const PF = [
22
- { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
22
+ { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'proposes', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
23
23
  { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12, kind: 'derived', store: true },
24
24
  { key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', st: 'draft', w: 0, due: 4 },
25
25
  { key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'draft', w: 0, due: 4, isNew: true },
26
26
  { key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', st: 'ok', w: -1, kind: 'report' },
27
27
  { key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', st: 'due', w: -10, due: 4, kind: 'report' },
28
- { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', st: 'ok', w: -2 },
29
- { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', st: 'ok', w: -30, flag: '1 tripped' },
28
+ { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', store: true, st: 'ok', w: -2 },
29
+ { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', store: true, st: 'ok', w: -30, flag: '1 tripped' },
30
30
  { key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', st: 'ok', w: -3, isNew: true },
31
31
  { key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
32
- { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', st: 'ok', w: -20 },
32
+ { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', store: true, st: 'ok', w: -20 },
33
33
  ];
34
34
  const BD = [
35
35
  { key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', st: 'ok', w: -15, isNew: true },
@@ -104,7 +104,7 @@ function actOf(e, stt) {
104
104
  const kind = e.kind || 'doc';
105
105
  if (kind === 'doc' && e.by !== 'you' && (stt === 'missing' || stt === 'stale')) return { label: 'Draft it', do: 'draft' };
106
106
  if (stt !== 'missing') return null;
107
- if (e.store && e.by !== 'you') return { label: 'Propose them', do: 'draft' };
107
+ if (e.store && e.by !== 'you') return { label: e.key === 'okrs' ? 'Propose them' : 'Propose it', do: 'draft' };
108
108
  if (e.key === 'scan') return { label: 'Run a scan', go: 'scan' };
109
109
  if (kind === 'report' && !e.cat) return { label: 'Set it up', go: 'reports' };
110
110
  return null;
@@ -736,6 +736,11 @@ const AG = {
736
736
  handed: (w) => `Handed to the agent. Nothing goes to ${w} until you post it.`, stopped: 'Stopped. Nothing was kept.', kept: 'Kept as a draft. Post it when you are ready.',
737
737
  posting: 'Approved. A session posts exactly this text.', sentBack: 'Sent back with your note. The agent drafts it again.', retried: 'The agent tries again.',
738
738
  needNote: 'Say what to change first: type a note, then send it back.', link: 'Open what was posted',
739
+ // a draft that names one step in another system (amendment 17): approving it starts a session with Claude in Chrome
740
+ doIt: (s) => 'Do it: ' + s, stepLine: 'Step when you approve: ', stepFx1: 'Outward: this one step, as you', stepFx2: 'A new session with Chrome does only this',
741
+ doing: 'Approved. A session with Chrome does exactly this step.', sitStepReady: (at) => `Draft ready${at ? ' at ' + at : ''}. The step has not run.`,
742
+ sitStepping: 'Doing the step you approved. A session with Chrome does exactly that step and nothing else.',
743
+ stepping: 'Agent: doing the step', stepDone: 'Agent: step done', sitStepDone: (at) => `The agent did the step you approved${at ? ' at ' + at : ''}.`, stepLink: 'Open what it made',
739
744
  };
740
745
 
741
746
  function agentChip(item) {
@@ -745,8 +750,9 @@ function agentChip(item) {
745
750
  if (a.state === 'queued') return st('st', 'clock', AG.queued);
746
751
  if (a.state === 'working') return st('st c3strip', 'spark', AG.working(minsAgo(a.started_at)), true);
747
752
  if (a.state === 'ready') return st('st ok', 'check', AG.ready);
748
- if (a.state === 'sending') return st('st c3strip', 'out', AG.sending, true);
749
- if (a.state === 'sent') return st('st ok', 'out', AG.sent(whereOf(item), a.sent && a.sent.at ? hhmm(a.sent.at) : ''));
753
+ const step = a.draft && a.draft.step;
754
+ if (a.state === 'sending') return st('st c3strip', 'out', step ? AG.stepping : AG.sending, true);
755
+ if (a.state === 'sent') return st('st ok', 'out', step ? AG.stepDone : AG.sent(whereOf(item), a.sent && a.sent.at ? hhmm(a.sent.at) : ''));
750
756
  if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.asks));
751
757
  return null;
752
758
  }
@@ -793,26 +799,29 @@ function agentCase(inst, item, L) {
793
799
  extras.push(a.state === 'queued' ? strip(false, 'clock', 'Queued' + (a.decided_at ? ' at ' + hhmm(a.decided_at) : ''))
794
800
  : strip(true, 'spark', (redraft ? 'Redrafting with your note' : 'Working') + (a.started_at ? ' · started ' + minsAgo(a.started_at) : '')));
795
801
  } else if (a.state === 'ready') {
796
- sit = AG.sitReady('', w);
797
- outbox = true; cta = 'Hold to post';
802
+ const step = a.draft && a.draft.step;
803
+ sit = step ? AG.sitStepReady('') : AG.sitReady('', w);
804
+ outbox = true; cta = step ? 'Hold to do it' : 'Hold to post';
798
805
  const mine = isMine(item);
799
806
  opts = [
800
- mine ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
807
+ // a step in another system replaces the post: the approved send does the step (amendment 17)
808
+ step ? o('step', AG.doIt(step), [fx('lock', AG.stepFx1, 'warn'), fx('out', AG.stepFx2, 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.doing), { outward: true }) : null,
809
+ mine || step ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
801
810
  mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)) : null,
802
- o('edit', AG.editDraft, [fx('edit', mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
811
+ o('edit', AG.editDraft, [fx('edit', step ? 'The step then uses your version' : mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
803
812
  o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: mine ? AG.keptMine : AG.kept, timeout: 3000 })),
804
813
  mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
805
814
  ].filter(Boolean);
806
815
  extras.push(draftCard(inst, item, k, false), conversation(inst, item, false, w));
807
816
  before = el('div', { class: 'wk-conv-h', 'data-pm-agent-decide': item.id }, el('span', { class: 'dim mono' }, c3ic('check', 'sm'), AG.orDecide));
808
817
  } else if (a.state === 'sending') {
809
- sit = AG.sitSending(k);
818
+ sit = a.draft && a.draft.step ? AG.sitStepping : AG.sitSending(k);
810
819
  if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
811
820
  } else if (a.state === 'sent') {
812
- sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
821
+ sit = a.draft && a.draft.step ? AG.sitStepDone(a.sent && a.sent.at ? hhmm(a.sent.at) : '') : AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
813
822
  opts = L.opts.map((x) => ({ ...x }));
814
823
  if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
815
- if (a.sent && a.sent.link) extras.push(el('div', { class: 'chips2' }, el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link)));
824
+ if (a.sent && a.sent.link) extras.push(el('div', { class: 'chips2' }, el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), a.draft && a.draft.step ? AG.stepLink : AG.link)));
816
825
  }
817
826
  if (opts.length) opts[0].rec = 1;
818
827
  const model = { id: 'task:' + item.id, sit, quote, chg, conf: 0, cta, opts: opts.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: !!x.outward })), why: '', outbox,
@@ -854,6 +863,7 @@ function draftCard(inst, item, k, busy) {
854
863
  el('span', { class: 'chip2', 'data-pm-agent-version': item.id }, c3ic(cur.kind === 'edit' ? 'edit' : 'spark', 'sm'), cur.label + (cur.at ? ' · ' + hhmm(cur.at) : ''))),
855
864
  el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.open], () => openDraft(item, inst)),
856
865
  c3btn('chip2 lk', { 'data-pm-agent-copy': item.id }, [c3ic('copy', 'sm'), AG.copy], () => copyText(d.body)))),
866
+ d.step ? el('div', { class: 'dim mono wk-agent-will', 'data-pm-agent-step': item.id }, c3ic('out', 'sm'), ' ' + AG.stepLine + d.step) : null,
857
867
  body,
858
868
  long ? c3btn('chip2 lk', { 'data-pm-agent-all': item.id, 'aria-expanded': String(all) }, [c3ic(all ? 'zout' : 'zin', 'sm'), all ? AG.showLess : AG.showAll],
859
869
  () => { if (all) inst.editing.delete('all:' + item.id); else inst.editing.add('all:' + item.id); draw(inst); }) : null);
@@ -4342,6 +4342,9 @@ function taskWsId(run, t) {
4342
4342
  } catch { return null; }
4343
4343
  }
4344
4344
  /** Start one task session; a start that fails marks the task failed with the reason. Returns the new run. */
4345
+ /** The launch flags of a session that does an approved step (amendment 17): Claude in Chrome, and an allow rule for its
4346
+ * browser tools only. Nothing else is allowed beyond what the PM's own settings allow. */
4347
+ const STEP_FLAGS = [{ flag: '--chrome' }, { flag: '--allowedTools', value: 'mcp__claude-in-chrome' }];
4345
4348
  function startDayTask(run, t, now) {
4346
4349
  // a job runs in its own repository's folder, so it reads that repository's files and records (one day, many repositories)
4347
4350
  const sk = daySkillPrompt(run, 'day-task', taskWsId(run, t));
@@ -4349,14 +4352,18 @@ function startDayTask(run, t, now) {
4349
4352
  const line = `This run: /day-task ${t.id} ${t.mode} — read the task first with: pmos proactive day show ${t.id}`;
4350
4353
  // The session that did the task's last step is used again while it is open: it has the skill and the task in its
4351
4354
  // context already, so only this run's line is typed into it, and the PM keeps one session per job.
4352
- const prev = t.prev_session && sessions.get(t.prev_session);
4355
+ // An approved step in another system (c3-work-agent amendment 17) runs in a NEW session that starts with Claude in Chrome
4356
+ // and an allow rule for the browser tools only (STEP_FLAGS), because the PM approved that step in Work. The earlier
4357
+ // session is not used again: it started without that rule, and no line typed into it can stand in for the PM's consent.
4358
+ const step = t.mode === 'send' && dayRunLib.stepOf(t);
4359
+ const prev = !step && t.prev_session && sessions.get(t.prev_session);
4353
4360
  if (!sk.error && prev && prev.state !== 'exited' && prev.pty) {
4354
4361
  try { firstInput.deliver(prev, line + '\r'); s = prev; } catch { s = null; }
4355
4362
  }
4356
4363
  if (!sk.error && !s) {
4357
4364
  const prompt = `${sk.prompt}\n\n${line}`;
4358
4365
  s = spawnSession({ name: jobSessionName(t.title), agent: sk.task.agent || 'claude', ws: sk.w ? sk.w.id : run.ws, origin: 'day-task', initialInput: prompt + '\r',
4359
- modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill } });
4366
+ modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill }, ...(step ? { launchFlags: STEP_FLAGS } : {}) });
4360
4367
  }
4361
4368
  const live = s && s.state !== 'exited';
4362
4369
  const st = dayRunLib.started(run, t.id, live ? s.id : null, now);
@@ -8626,7 +8633,7 @@ async function handleRequest(req, res) {
8626
8633
  }
8627
8634
  const id = String(body.task || '');
8628
8635
  if (!dayRunLib.findTask(run, id)) return json(res, { ok: false, error: `no task ${id || '(none given)'} in today's run` }, 404);
8629
- const r = verb === 'deliver' ? dayRunLib.deliver(run, id, { summary: body.summary, body: body.body, link: body.link }, now)
8636
+ const r = verb === 'deliver' ? dayRunLib.deliver(run, id, { summary: body.summary, body: body.body, link: body.link, step: body.step }, now)
8630
8637
  : verb === 'sent' ? dayRunLib.sent(run, id, { link: body.link }, now)
8631
8638
  : verb === 'fail' ? dayRunLib.fail(run, id, body.reason, now)
8632
8639
  : dayRunLib.decide(run, id, { action: body.action, note: body.note }, now);
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: day-task
3
3
  label: "Day task"
4
- description: "Do one task of the PM's day for one task id and one mode (do, revise or send). In do and revise it prepares ONE draft and sends nothing. In send it carries out the one outward step of the draft the PM approved."
4
+ description: "Do one task of the PM's day for one task id and one mode (do, revise or send). In do and revise it prepares ONE draft and sends nothing. In send it carries out the one outward step of the draft the PM approved, or the one step in another system that the draft names."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
@@ -20,14 +20,15 @@ Rules that always hold:
20
20
  5. NOTHING INVENTED. If a fact is not in a source you read, leave it out or write "unverified".
21
21
  6. NEVER APPROVE FOR THE PM. Do not run `pmos proactive day approve`, `revise` or `retry`. Those commands are the PM's.
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
+ 8. A STEP IN ANOTHER SYSTEM WAITS FOR APPROVAL. If the ask needs an action in another system (for example "create the cohort exploration in GA4"), do not try it in do or revise mode. Name it as the draft's step with `--step`. The PM approves the step in Work, and then a new session with Claude in Chrome does that one step.
23
24
 
24
25
  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, 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
+ 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 it asks for an action in another system, such as creating a report, a dashboard or an exploration, put that action in `--step` (rule 8). 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
27
  2. If MODE is do:
27
28
  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
29
  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
30
  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
- 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.
31
+ 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>] [--step "<at most 240 characters: the one action in another system that approval triggers>"]`. Use `--step` only for an action in another system that is not a post of the draft text. Start it with a verb and name the system, for example "Create the 'New users by week' cohort exploration in GA4 property Acme Web". The body then gives the exact settings the step uses, so the PM can check them before approving. 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.
31
32
  e. If the workspace refuses the draft, read the reason, fix the draft and submit once more. Do not work around a refusal.
32
33
  3. If MODE is revise:
33
34
  a. Read the draft so far and the LAST note of the PM in the output of `day show`. The note says what to change. Earlier notes still hold unless the last one cancels them.
@@ -40,6 +41,7 @@ Procedure:
40
41
  b. Do the one outward step the summary names, with the connector that fits, to the place the link names. Examples: post the approved reply in the linked Slack thread, add the approved comment to the linked Jira issue, reply to the linked mail thread. If the summary names more than one step, do only the first and report the rest with `day fail`.
41
42
  c. If the place does not match the link, the thread is closed, or the connector is not available, do not try another route. Run `pmos proactive day fail TASK --reason "<what stopped it>"`.
42
43
  d. When it is done, run `pmos proactive day sent TASK --link <URL of what you posted or sent>`. Leave out `--link` only if the connector returns none.
44
+ e. If `day show` prints a `step:` line, the PM approved that step, not a post. Do that one step in the system it names, with the Claude in Chrome tools (`mcp__claude-in-chrome__*`), and use the settings in the approved body. Do not post the body anywhere. Do not do a second action, and do not change or delete anything that already exists. If Chrome is not connected, the PM is not signed in to the system, or the page does not match the step, run `day fail` with what stopped it. When the step is done, run `day sent` with the URL of what you made.
43
45
  5. If you cannot produce a draft or do the step for any reason, run `pmos proactive day fail TASK --reason "<one sentence, at most 240 characters>"`. A true failure helps the PM more than a weak draft.
44
46
  6. Finish with the output contract.
45
47