create-pmos 0.199.0 → 0.201.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.199.0",
3
+ "version": "0.201.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@0ede922
1
+ kernel-source: pmos@2bd8a36
2
2
  assembled: 2026-10-13
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-13T10:19:39Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T11:15:18Z"}
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-13T10:19:39Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T11:15:18Z"}
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-13T10:19:39Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T11:15:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -123,6 +123,27 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
123
123
  await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-panel]');
124
124
  await page.setInputFiles('[data-rd-file]', { name: 'Biweekly update, 26 Mar.pptx', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', buffer: PPTX });
125
125
  ok('the uploaded sample shows in the conversation with its thumbnail', !!(await until(async () => /26 Mar\.pptx/.test(await text('[data-rd-chat]')) && !!(await page.$('[data-rd-chat] img.rd-dks-thumb')))));
126
+ // a Google Slides link is kept by its address; PMOS says it cannot open it and which download to add, and the long link stays in its column
127
+ if (!(await page.$('[data-rd-link]'))) { await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-link]'); }
128
+ await page.fill('[data-rd-link]', 'https://docs.google.com/presentation/d/1FqiafPx1SjmntCVT8SOOIfFbxHrkemAwEc_MpAnGGh0/edit');
129
+ await page.click('[data-rd-smp="link"]');
130
+ ok('a Google Slides link says PMOS cannot open it and to add the PDF download', !!(await until(async () => /Google Slides deck/.test(await text('[data-rd-chat]')) && /File, Download, PDF document \(\.pdf\)/.test(await text('[data-rd-chat] [data-rd-hint]')))));
131
+ if (!(await page.$('[data-rd-link]'))) { await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-link]'); }
132
+ await page.fill('[data-rd-link]', 'https://example.atlassian.net/wiki/d/1FqiafPx1SjmntCVT8SOOIfFbxHrkemAwEc_MpAnGGh0xYzW1FqiafPx1SjmntCVT8SOOIfFbx/edit');
133
+ await page.click('[data-rd-smp="link"]');
134
+ ok('any other link says PMOS cannot open web pages and to add a PDF or a screenshot', !!(await until(async () => /PMOS cannot open web pages/.test(await text('[data-rd-chat]')))));
135
+ const box = async (sel) => page.evaluate((q) => { const r = document.querySelector(q).getBoundingClientRect(); return { l: r.left, r: r.right, t: r.top, b: r.bottom, w: r.width }; }, sel);
136
+ // nothing in the conversation (a long link, a bubble) may reach past the column into the preview
137
+ const chatRight = () => page.evaluate(() => Math.max(...[...document.querySelectorAll('.rd-dks-chat, .rd-dks-chat *')].map((n) => n.getBoundingClientRect().right)));
138
+ const sideBySide = async () => { const o = await box('.rd-dks-out'); return (await chatRight()) <= o.l + 1; };
139
+ ok('the conversation column does not run under the preview', await sideBySide(), JSON.stringify([await chatRight(), await box('.rd-dks-out')]));
140
+ ok('the studio has Half, 3/4 and Whole, and opens at Whole', (await page.$$('[data-rd-size]')).length === 3 && await page.getAttribute('[data-rd-size="whole"]', 'aria-pressed') === 'true');
141
+ await page.click('[data-rd-size="half"]');
142
+ ok('Half makes the studio half the window and stacks the preview under the conversation', !!(await until(async () => { const s = await box('.sheet.rd-wide'); const c = await box('.rd-dks-chat'); const o = await box('.rd-dks-out'); return Math.abs(s.w - 720) < 4 && o.t >= c.b - 1; })), JSON.stringify(await box('.sheet.rd-wide')));
143
+ await page.click('[data-rd-size="three-quarters"]');
144
+ ok('3/4 makes the studio three-quarters of the window, side by side', !!(await until(async () => Math.abs((await box('.sheet.rd-wide')).w - 1080) < 4 && await sideBySide())));
145
+ await page.click('[data-rd-size="whole"]');
146
+ ok('Whole fills the window again', !!(await until(async () => Math.abs((await box('.sheet.rd-wide')).w - 1416) < 4)));
126
147
  await page.fill('[data-rd-in]', 'A biweekly deck for leadership: numbered blue header band, one slide per initiative.');
127
148
  await page.click('[data-rd-act="send"]');
128
149
  ok('while the job runs the conversation shows PMOS working', !!(await until(() => page.$('[data-rd-running]'), 8000)));
@@ -159,10 +180,12 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
159
180
  ok('This is it saves D2', !!(await until(async () => /Saved design: D2/.test(await text('[data-rd-saved]')))));
160
181
  const P = D.paths(team, id);
161
182
  ok('brief.md is written', fs.existsSync(P.brief));
183
+ ok('This is it says the current cycle is being redrawn in the design', /redrawing the .* cycle in it now/.test(await text('body')));
184
+ ok('the current cycle is redrawn in the saved design', !!(await until(() => { const c = D.cyclePaths(team, firstPath); if (!fs.existsSync(c.json)) return false; const j = JSON.parse(fs.readFileSync(c.json, 'utf8')); return j.design_version === 2; }, 30000)), (() => { try { return fs.readFileSync(D.cyclePaths(team, firstPath).json, 'utf8'); } catch (e) { return String(e); } })());
162
185
  await page.click('[data-rd-act="brief"]');
163
186
  await page.waitForSelector('[data-rd-brief]');
164
187
  ok('the Design brief shows the design, the description, the sample, the reference version and the rules',
165
- !!(await until(async () => /Design D2/.test(await text('[data-rd-design]')))) && /numbered blue header band/.test(await text('[data-rd-desc]')) && (await page.$$('[data-rd-sample]')).length === 1
188
+ !!(await until(async () => /Design D2/.test(await text('[data-rd-design]')))) && /numbered blue header band/.test(await text('[data-rd-desc]')) && (await page.$$('[data-rd-sample]')).length === 3
166
189
  && !!(await until(() => page.$('[data-rd-ref] iframe'))) && !!(await page.$('[data-rd-rule="learned"]')));
167
190
  ok('the brief lists the cycles drawn with the design', !!(await until(() => page.$('[data-rd-briefcycle]'))));
168
191
  await closeAll();
@@ -45,7 +45,7 @@ A row that is not started always has a next step:
45
45
  ## How PMOS drafts a document
46
46
 
47
47
  - **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. It drafts a document at most once a day, and it runs at most 3 drafts at once. It never drafts a document that you write (the product strategy).
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
49
  - **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
50
 
51
51
  ## Issue a draft
@@ -22,10 +22,12 @@ Use one of these two buttons:
22
22
 
23
23
  1. Select **Deck** or **Document** at the top of the studio.
24
24
  2. Write what the report is for and who reads it. For example: "This goes to the progress session every other Thursday. The decisions must stand out."
25
- 3. Press **Add a sample** to give PMOS a report that people liked. You can upload a file, paste a link, or pick a report document that PMOS has. Each sample shows what PMOS took from it.
25
+ 3. Press **Add a sample** to give PMOS a report that people liked. You can upload a file, paste a link, or pick a report document that PMOS has. Each sample shows what PMOS took from it. PMOS cannot open a link, so a link sample tells you which file to add. For Google Slides, choose **File**, **Download**, **PDF document (.pdf)** in Slides, then add that file. PMOS sees the pages of a PDF or an image, so it can follow their look. From a .pptx or a .docx, PMOS gets only the words.
26
26
  4. Press **Make the next version**. PMOS makes a version from your words and this cycle's data. This takes a short time. You can leave the studio. The studio shows when the version is ready.
27
27
  5. Read the version in the preview. Write what to change and press **Make the next version** again. Each turn makes a new version: D1, D2, D3 and so on.
28
28
 
29
+ To set the width of the studio, use **Half**, **3/4** or **Whole** at the top right. PMOS keeps your choice on this computer.
30
+
29
31
  If a turn fails, the studio shows why. Nothing changes. Try the turn again.
30
32
 
31
33
  ### Point at a part
@@ -40,7 +42,7 @@ Keep **Point and say** on. Click a part of the preview and write what to change
40
42
 
41
43
  ### Save the design
42
44
 
43
- Press **This is it** to save the version as the design. Every next draft follows it. Press **Design brief** to read the saved design: your description, your samples, the reference version, and what PMOS learned. What PMOS learned has three parts: what stays the same each cycle, what changes each cycle, and what it learned from your turns.
45
+ Press **This is it** to save the version as the design. PMOS redraws the current cycle in the design at once, and every next draft follows it. Older cycles, and a cycle that was already sent, keep their look. Press **Design brief** to read the saved design: your description, your samples, the reference version, and what PMOS learned. What PMOS learned has three parts: what stays the same each cycle, what changes each cycle, and what it learned from your turns.
44
46
 
45
47
  ## Read a cycle as a deck
46
48
 
@@ -204,7 +204,8 @@ function addSample(root, cadence, input, { actor, now } = {}) {
204
204
  let u; try { u = new URL(link); } catch { return refuse(400, 'link must be a full address, such as https://…'); }
205
205
  if (u.protocol !== 'https:' && u.protocol !== 'http:') return refuse(400, 'link must be an http or https address');
206
206
  if (link.length > 2000) return refuse(400, 'link is too long');
207
- rec = { id, name: oneLine(input.name || u.hostname + u.pathname, 120), kind: 'link', source: link, ext: '', readable: true, thumb: null, text_file: null };
207
+ // the job reads files only (Read, Glob, Grep), so a link is kept by its address and the PM is told how to add a copy PMOS can read
208
+ rec = { id, name: oneLine(input.name || linkName(u), 120), kind: 'link', source: link, ext: '', readable: false, thumb: null, text_file: null };
208
209
  } else if (typeof input.doc === 'string' && input.doc.trim()) {
209
210
  const rel = input.doc.trim().replace(/\\/g, '/');
210
211
  if (!/(?:^|\/)reports\/(?!definitions\/)[a-z][a-z0-9-]{0,39}\/\d{4}-\d{2}-\d{2}\.md$/.test(rel)) return refuse(400, 'doc must be a report document: <state>/reports/<cadence>/<slot>.md');
@@ -245,7 +246,21 @@ function addSample(root, cadence, input, { actor, now } = {}) {
245
246
  return { ok: true, sample: sampleOut(rec) };
246
247
  }
247
248
  function sampleOut(s) {
248
- return { id: s.id, name: s.name, kind: s.kind, source: s.source, took: s.took || null, thumb: typeof s.thumb === 'string' && s.thumb.startsWith('data:image/') ? s.thumb : null, readable: s.readable !== false };
249
+ const out = { id: s.id, name: s.name, kind: s.kind, source: s.source, took: s.took || null, thumb: typeof s.thumb === 'string' && s.thumb.startsWith('data:image/') ? s.thumb : null, readable: s.readable !== false };
250
+ if (s.kind === 'link') out.hint = linkHint(s.source);
251
+ return out;
252
+ }
253
+ /* Google files: name them for what they are, and say which download PMOS can read */
254
+ // a PDF, because the job sees its pages; from a .pptx or .docx it gets the words only, not the look
255
+ const GOOGLE = [[/^\/presentation\//, 'Google Slides deck', 'Slides'], [/^\/document\//, 'Google Docs document', 'Docs'], [/^\/spreadsheets\//, 'Google Sheets sheet', 'Sheets']];
256
+ const googleOf = (u) => (u && /(^|\.)docs\.google\.com$/i.test(u.hostname) ? GOOGLE.find((g) => g[0].test(u.pathname)) || null : null);
257
+ const parseUrl = (v) => { try { return new URL(String(v || '')); } catch { return null; } };
258
+ function linkName(u) { const g = googleOf(u); return g ? g[1] : u.hostname + u.pathname; }
259
+ /** What the PM sees under a link sample: PMOS cannot open web pages, and how to add a copy it can read. */
260
+ function linkHint(source) {
261
+ const g = googleOf(parseUrl(source));
262
+ if (g) return `PMOS cannot open ${g[2]} from a link, because it needs your Google sign-in. In ${g[2]}, choose File, Download, PDF document (.pdf), then add that file here. PMOS sees the pages of a PDF, so it can follow their look.`;
263
+ return 'PMOS cannot open web pages. Save the page as a PDF or take a screenshot, then add that file here.';
249
264
  }
250
265
  /** Record what a job took from a sample (also kept in samples/<id>.json). */
251
266
  function recordTook(root, cadence, id, took, readable, { now } = {}) {
@@ -13,11 +13,11 @@
13
13
  *
14
14
  * THE RUN. A press on Draft it (and the schedule tick) starts ONE headless run, kind `core-doc` (lib/headless.js), origin
15
15
  * `core-doc:<scope>:<key>`. Its tools are Read, Glob, Grep and `pmos coredoc write`; the server pins the entry it may write with
16
- * PMOS_CORE_DOC_SCOPE and PMOS_CORE_DOC_KEY. At most RUN_CAP core-doc runs run at once.
16
+ * PMOS_CORE_DOC_SCOPE and PMOS_CORE_DOC_KEY. There is no cap on how many run at once (PM, 2026-10-13: no run limit).
17
17
  *
18
18
  * THE TICK (PRD section 4). tick(ctx, { now }) runs every 30 minutes inside the working hours (lib/driver-run.js dayCheckOpen: the day-check
19
19
  * window and the quiet hours). For each repository (the lead tree) it drafts every `doc` entry of the portfolio and of each board whose cadence
20
- * has passed (state `due`) and that has no open draft, at most once per entry per day; never an entry the PM writes (`by: you`). Initiative
20
+ * has passed (state `due`), is out of date or is not started, and that has no open draft and no run; never an entry the PM writes (`by: you`). Initiative
21
21
  * documents are the Initiative Driver's (lib/driver.js doc-due). ctx adds to the route ctx: { proSettings(), inQuiet(now, quiet), isLead(w) }.
22
22
  */
23
23
  const guard = require('./pm-guard.js');
@@ -27,11 +27,9 @@ const headless = require('./headless.js');
27
27
  const text = (v) => (typeof v === 'string' ? v : '');
28
28
  const bad = (status, error) => ({ status, body: { ok: false, error } });
29
29
  const POSTS = ['draft', 'issue', 'drop'];
30
- const RUN_CAP = 3;
31
30
  const TICK_EVERY_MS = 30 * 60 * 1000;
32
31
 
33
32
  const runs = new Map(); // `${ws}|${scope}|${key}` → session id, for the core-doc runs this server started
34
- const tried = new Map(); // `${ws}|${scope}|${key}` → the local day the tick last started a run for it
35
33
  let lastTick = null;
36
34
 
37
35
  const runKey = (ws, scope, key) => `${ws}|${scope}|${key}`;
@@ -60,7 +58,6 @@ function startRun(ctx, w, scope, entry, { label, now } = {}) {
60
58
  if (headless.kindFor({ agent: 'claude', origin }) !== 'core-doc') return { ok: false, status: 409, error: 'Background runs are switched off here (PMOS_HEADLESS=off), so PMOS cannot draft this document.' };
61
59
  const k = runKey(w.id, scope, entry.key);
62
60
  if (runs.has(k)) return { ok: false, status: 409, error: `PMOS is drafting the ${entry.title.toLowerCase()} already` };
63
- if (runs.size >= RUN_CAP) return { ok: false, status: 409, error: `${RUN_CAP} documents are being drafted already. Try again when one is done.` };
64
61
  const prompt = CD.runPrompt(entry, scope, { today: CD.localDay(now == null ? Date.now() : now), label });
65
62
  const s = ctx.spawnSession({ name: `Core document · ${entry.title}`, agent: 'claude', ws: w.id, origin, initialInput: prompt + '\r',
66
63
  initiative: scope.startsWith('initiative:') ? scope.slice('initiative:'.length) : undefined,
@@ -90,7 +87,7 @@ function tick(ctx, { now = Date.now(), force = false } = {}) {
90
87
  const out = [];
91
88
  const list = typeof ctx.workspaceList === 'function' ? ctx.workspaceList() : [ctx.getWs(null)].filter(Boolean);
92
89
  // Every candidate first, then the most pressing: due (issued, cadence passed), out of date, then not started (the first draft, and the
93
- // proposed OKRs while no OKR files exist). At most RUN_CAP runs at once; each entry at most once a day.
90
+ // proposed OKRs while no OKR files exist). No cap: every candidate starts; a failed run is tried again on the next tick.
94
91
  const RANK = { due: 0, stale: 1, missing: 2 };
95
92
  const todo = [];
96
93
  for (const w of list) {
@@ -107,17 +104,15 @@ function tick(ctx, { now = Date.now(), force = false } = {}) {
107
104
  const st = CD.stateOf(entry, g.facts);
108
105
  if (!(st.st in RANK) || st.draft) continue;
109
106
  const k = runKey(w.id, g.facts.scope, entry.key);
110
- if (runs.has(k) || tried.get(k) === day) continue;
107
+ if (runs.has(k)) continue;
111
108
  const x = g.facts.store.get(`${g.facts.scope}|${entry.key}`);
112
- if (x && x.drafts.some((d) => CD.localDay(Date.parse(d.at)) === day)) continue; // drafted today already (by a press, or before a restart)
109
+ if (x && x.drafts.some((d) => x.drops.has(d.slot) && CD.localDay(Date.parse(d.at)) === day)) continue; // the PM dropped today's draft: not again until tomorrow
113
110
  todo.push({ w, g, entry, k, order: [RANK[st.st], li, ei] });
114
111
  }
115
112
  }
116
113
  }
117
114
  todo.sort((a, b) => a.order[0] - b.order[0] || a.order[1] - b.order[1] || a.order[2] - b.order[2]);
118
115
  for (const t of todo) {
119
- if (runs.size >= RUN_CAP) break;
120
- tried.set(t.k, day);
121
116
  const r = startRun(ctx, t.w, t.g.facts.scope, t.entry, { label: labelOf(t.g.facts), now });
122
117
  out.push({ ws: t.w.id, scope: t.g.facts.scope, key: t.entry.key, ok: r.ok, ...(r.ok ? { run: r.run } : { error: r.error }) });
123
118
  }
@@ -175,4 +170,4 @@ async function handle(req, res, url, ctx) {
175
170
  return send({ status: 200, body: { ok: true } });
176
171
  }
177
172
 
178
- module.exports = { prefixes: ['/api/core-docs'], handle, tick, startRun, RUN_CAP, TICK_EVERY_MS, _runs: runs, _reset: () => { runs.clear(); tried.clear(); lastTick = null; } };
173
+ module.exports = { prefixes: ['/api/core-docs'], handle, tick, startRun, TICK_EVERY_MS, _runs: runs, _reset: () => { runs.clear(); lastTick = null; } };
@@ -6,7 +6,8 @@
6
6
  * POST /api/report-design/describe { ws, id, text, format? }
7
7
  * POST /api/report-design/sample { ws, id, name, kind, data } | { ws, id, link, name? } | { ws, id, doc, name? } → { ok, sample }
8
8
  * POST /api/report-design/turn { ws, id, text, points: [{ n, slide, x, y, text }] } → { ok, running } ONE job per report; 409 while one runs
9
- * POST /api/report-design/accept { ws, id, n } → { ok, design, brief } "This is it": writes brief.md
9
+ * POST /api/report-design/accept { ws, id, n } → { ok, design, brief, redraw } "This is it": writes brief.md and redraws the current
10
+ * cycle in the saved design (redraw: { path, slot, started, reason? }; a sent cycle keeps its look)
10
11
  * POST /api/report-design/revert { ws, id, n } → { ok, design }
11
12
  * GET /api/report-design/cycle?ws&path → { ok, designed, design_version, format, outline, differences, decisions, matches, rendering, previous }
12
13
  * GET /api/report-design/cycle-html?ws&path → text/html, the sanitized cycle render
@@ -473,7 +474,11 @@ async function handle(req, res, url, ctx) {
473
474
  D.append(root, id, { ev: 'accept', n, rules: D.rulesFor(root, id, n) }, { actor: g.actor });
474
475
  const b = D.writeBrief(root, id, setup);
475
476
  if (!b.ok) return fail(b);
476
- return send({ status: 200, body: { ok: true, setup, brief: b.path, design: D.designAnswer(root, id, { running: runningOf(root, id) }) } });
477
+ // the current cycle is redrawn in the saved design at once (Wawan, 2026-10-13); older cycles, and a cycle already sent, keep their look
478
+ const cur = D.currentCycle(root, id);
479
+ let redraw = null;
480
+ if (cur) { const r = startCycle(ctx, w, id, cur.path); redraw = { path: cur.path, slot: cur.slot, started: !!r.ok, ...(r.ok ? {} : { reason: r.error === 'issued' ? 'sent' : String(r.error || '') }) }; }
481
+ return send({ status: 200, body: { ok: true, setup, brief: b.path, redraw, design: D.designAnswer(root, id, { running: runningOf(root, id) }) } });
477
482
  }
478
483
  return send(bad(404, 'no such route'));
479
484
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.199.0",
3
+ "version": "0.201.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.199.0",
9
+ "version": "0.201.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.199.0",
3
+ "version": "0.201.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",
@@ -21,6 +21,11 @@ const MAX = 20 * 1024 * 1024;
21
21
  const KIND_OF = (name) => { const e = String(name || '').toLowerCase().split('.').pop(); return e === 'pdf' ? 'pdf' : e === 'pptx' ? 'pptx' : e === 'docx' ? 'docx' : /^(png|jpe?g)$/.test(e) ? 'image' : ''; };
22
22
  const SICON = { pdf: 'pdf', pptx: 'img', docx: 'doc', image: 'img', link: 'link', 'pmos-doc': 'doc' };
23
23
  const SWORD = { pdf: 'Upload, PDF', pptx: 'Upload, .pptx', docx: 'Upload, .docx', image: 'Upload, image', link: 'Link', 'pmos-doc': 'Past PMOS document' };
24
+ /* the studio's width, as a session's terminal sheet has it: half, three-quarters or the whole window; kept on this browser */
25
+ const SIZE_KEY = 'pmos.workspace.c3.studio-size';
26
+ export const STUDIO_SIZES = [['half', 'Half', 'Half of the window'], ['three-quarters', '3/4', 'Three-quarters of the window'], ['whole', 'Whole', 'Whole window']];
27
+ const sizeOf = (v) => (STUDIO_SIZES.some((x) => x[0] === v) ? v : 'whole');
28
+ const sizeStore = { get() { try { return localStorage.getItem(SIZE_KEY); } catch { return null; } }, set(v) { try { localStorage.setItem(SIZE_KEY, v); } catch { /* no storage */ } } };
24
29
  const hhmm = (iso) => { const m = /T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? m[1] + ':' + m[2] : ''; };
25
30
  const dayAt = (iso) => { const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? slotWord(m[1]) + ', ' + m[2] + ':' + m[3] : String(iso || ''); };
26
31
  const short = (t, n = 60) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
@@ -33,7 +38,8 @@ const local = (id) => LOCAL[id] || (LOCAL[id] = { view: null, cmp: false, cmpA:
33
38
  function sampleChip(s) {
34
39
  const th = s.thumb && /^data:image\//.test(s.thumb) ? el('img', { class: 'rd-dks-thumb img', src: s.thumb, alt: '' }) : el('span', { class: 'rd-dks-thumb ' + (s.kind === 'pptx' || s.kind === 'image' ? 'deck' : 'page'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i'));
35
40
  return el('div', { class: 'rd-dks-smp', 'data-rd-sample': s.id }, th, el('div', null, el('b', null, icon(SICON[s.kind] || 'doc', 'sm'), s.name), el('span', { class: 'dim mono' }, SWORD[s.kind] || s.kind),
36
- s.readable === false ? el('p', { class: 'rd-warn' }, 'PMOS could not read it with your tools. It is kept, but it does not shape the design.')
41
+ s.hint ? el('p', { class: 'rd-warn', 'data-rd-hint': '' }, s.hint)
42
+ : s.readable === false ? el('p', { class: 'rd-warn' }, 'PMOS could not read it with your tools. It is kept, but it does not shape the design.')
37
43
  : el('p', null, el('span', { class: 'dim' }, 'PMOS took: '), s.took || 'PMOS reads it on the next turn.')));
38
44
  }
39
45
 
@@ -43,7 +49,15 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
43
49
  const K = local(id); if (view != null) { K.view = view; K.cmp = false; }
44
50
  const S = { D: null, setup: null, err: '', cache: {}, timer: null, closed: false, busy: false };
45
51
  const body = el('div', { class: 'brief rs rd-dk rd-dks-wrap', 'data-rd-studio': id });
46
- const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide', body, onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
52
+ let size = sizeOf(sizeStore.get());
53
+ const sizeBtns = STUDIO_SIZES.map(([k, label, title]) => btn({ class: 'ds-btn', 'aria-pressed': String(k === size), 'data-rd-size': k, title, 'aria-label': title, onclick: () => setSize(k) }, label));
54
+ const seg = el('div', { class: 'seg rd-dks-size', role: 'group', 'aria-label': 'Studio width' }, ...sizeBtns);
55
+ const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide rdsz-' + size, body, actions: [seg], onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
56
+ function setSize(k) {
57
+ ui.sheet.classList.remove('rdsz-' + size); size = sizeOf(k); ui.sheet.classList.add('rdsz-' + size);
58
+ sizeStore.set(size);
59
+ sizeBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.rdSize === size)));
60
+ }
47
61
 
48
62
  async function load(keepView) {
49
63
  const r = await RD.design(ws, id);
@@ -85,7 +99,7 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
85
99
  function panel() {
86
100
  if (!K.panel) return null;
87
101
  const file = el('input', { type: 'file', class: 'rd-hide', accept: '.pdf,.pptx,.docx,.png,.jpg,.jpeg', 'data-rd-file': '', onChange: (e) => upload(e.target.files && e.target.files[0]) });
88
- const link = el('input', { type: 'text', 'data-rd-link': '', placeholder: 'Paste a Google Drive or Confluence link', 'aria-label': 'Link to a sample', onKeyDown: (e) => { if (e.key === 'Enter') addLink(); } });
102
+ const link = el('input', { type: 'text', 'data-rd-link': '', placeholder: 'Paste a link to keep with the design', 'aria-label': 'Link to a sample', onKeyDown: (e) => { if (e.key === 'Enter') addLink(); } });
89
103
  const docs = (RS.docs || []).filter((d) => d.setup !== id || d.state === 'issued').slice(0, 3);
90
104
  return el('div', { class: 'rd-dks-panel', 'data-rd-panel': '' }, el('div', { class: 'ds-h' }, 'Add a sample'),
91
105
  btn({ class: 'rd-dks-opt', 'data-rd-smp': 'file', onclick: () => file.click() }, icon('drop', 'sm'), el('span', null, 'Upload a past deck or document', el('br'), el('span', { class: 'dim rd-small' }, 'PDF, .pptx, .docx or a screenshot, up to 20 MB'))), file,
@@ -240,7 +254,10 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
240
254
  async function accept() {
241
255
  const n = K.view != null ? K.view : S.D.current; if (n == null) { toast('Make a version first.'); return; }
242
256
  const r = await RD.accept({ ws, id, n }); if (!r || !r.ok) { failed(r, 'The design was not saved'); return; }
243
- await load(true); toast('Saved D' + n + ' as the design of this report. PMOS drafts each cycle to match it. Nothing is sent.');
257
+ const rd = r.redraw; const day = rd && rd.slot ? slotWord(rd.slot) : '';
258
+ const what = rd && rd.started ? ' PMOS is redrawing the ' + day + ' cycle in it now; older cycles keep their look.'
259
+ : rd && rd.reason === 'sent' ? ' The ' + day + ' cycle was already sent, so it keeps its look; the next cycle uses the design.' : ' PMOS draws each new cycle in it.';
260
+ await load(true); toast('Saved D' + n + ' as the design of this report.' + what + ' Nothing is sent.');
244
261
  }
245
262
  function paint() {
246
263
  if (S.closed) return;
@@ -3,6 +3,15 @@
3
3
  sheet's .ds-btn/.ds-log/.ds-sync/.ds-stale. The slide is a render in a sandboxed frame (.rd-card > iframe), scaled with --rp-scale.
4
4
  Tokens only. Corners: 6px on a control, 8px on a container, 50% only on a dot. c3-report-deck.js links it once. */
5
5
  .c3 .sheet.rd-wide { width: min(1320px, calc(100% - 24px)); max-width: none; }
6
+ /* the Design studio's width control (Half, 3/4, Whole), as a session's terminal sheet has it; a phone sheet is already full width */
7
+ @media (min-width: 761px) {
8
+ .c3 .sheet.rd-wide.rdsz-half { width: 50vw; }
9
+ .c3 .sheet.rd-wide.rdsz-three-quarters { width: 75vw; }
10
+ .c3 .sheet.rd-wide.rdsz-whole { width: calc(100vw - 24px); }
11
+ }
12
+ .c3 .rd-dks-size { flex: none; }
13
+ .c3 .rd-dks-size button { padding: 4px 8px; }
14
+ .c3.ph .rd-dks-size { display: none; }
6
15
  .c3 .rd-dk { display: grid; gap: var(--sp-3); min-width: 0; }
7
16
  .c3 .rd-dk > * { min-width: 0; }
8
17
  .c3 .rd-wrap { flex-wrap: wrap; min-width: 0; }
@@ -92,8 +101,18 @@
92
101
 
93
102
  /* the Design studio: the conversation left, the live result right */
94
103
  .c3 .rd-dks { display: grid; grid-template-columns: minmax(300px, 380px) minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
95
- .c3 .rd-dks-chat { display: grid; gap: var(--sp-2); position: sticky; top: 0; max-height: calc(100vh - 220px); grid-template-rows: auto minmax(0, 1fr) auto auto; min-width: 0; }
96
- .c3 .rd-dks-desc { display: grid; gap: 6px; }
104
+ .c3 .rd-dks-chat { display: grid; gap: var(--sp-2); position: sticky; top: 0; max-height: calc(100vh - 220px); grid-template-rows: auto minmax(0, 1fr) auto auto; grid-template-columns: minmax(0, 1fr); min-width: 0; }
105
+ .c3 .rd-dks-chat > *, .c3 .rd-dks-desc > * { min-width: 0; }
106
+ .c3 .rd-dks-desc { display: grid; gap: 6px; grid-template-columns: minmax(0, 1fr); }
107
+ /* a long sample name (a pasted link) stays in the conversation column */
108
+ .c3 .rd-dks-desc .chip2 { display: block; max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
+ /* the studio stacks when its sheet is narrow (Half, or a small window) */
110
+ .c3 .rd-dks-wrap { container-type: inline-size; }
111
+ @container (max-width: 820px) {
112
+ .c3 .rd-dks { grid-template-columns: minmax(0, 1fr); }
113
+ .c3 .rd-dks-chat { position: static; max-height: none; }
114
+ .c3 .rd-dks-log { max-height: 420px; }
115
+ }
97
116
  .c3 .rd-dks-desc .dim { font-size: var(--fs-1); }
98
117
  .c3 .rd-dks-log { list-style: none; margin: 0; padding: 0 4px 0 0; display: grid; gap: 12px; align-content: start; overflow: auto; min-height: 120px; }
99
118
  .c3 .rd-dks-msg { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 8px; align-items: start; }
@@ -37,7 +37,7 @@ The modes:
37
37
  - match: as cycle, but match the reference exactly: the same slides, ids, kinds and order. Put data that has no place in the reference into the nearest slide of the reference.
38
38
 
39
39
  How to read a sample:
40
- - A PDF or an image: read the file with Read.
40
+ - A PDF or an image: read the file with Read and look at every page. Follow its look closely: the grid and the place of each part, the colours, the header and footer bands, the type sizes, how dense each slide or page is, and the slide kinds and their order. Say in "took" what you followed.
41
41
  - A .pptx or a .docx: read its words in the .txt file named for it. You cannot see its look, only its words and order.
42
42
  - A report document: read the .md file named for it.
43
43
  - A link: you have no web tool. Read it only through a connector the PM's settings allow. If you cannot read it, say so with "readable": false.