create-pmos 0.516.0 → 0.517.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.
Files changed (65) hide show
  1. package/package.json +20 -24
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/changelog.json +19 -0
  7. package/workspace/ev-everywhere-test.js +3 -3
  8. package/workspace/guide/board.md +7 -63
  9. package/workspace/guide/breakdown.md +8 -108
  10. package/workspace/guide/briefing-premium.md +5 -526
  11. package/workspace/guide/console-calendar.md +7 -206
  12. package/workspace/guide/docs.md +10 -125
  13. package/workspace/guide/home.md +8 -5
  14. package/workspace/guide/index.md +78 -79
  15. package/workspace/guide/initiatives-and-next-steps.md +6 -19
  16. package/workspace/guide/layout.md +23 -230
  17. package/workspace/guide/live-notes.md +5 -7
  18. package/workspace/guide/my-work.md +8 -172
  19. package/workspace/guide/notes.md +14 -21
  20. package/workspace/guide/people.md +6 -39
  21. package/workspace/guide/report-design.md +2 -2
  22. package/workspace/guide/report-structure.md +1 -1
  23. package/workspace/guide/reports-set-up.md +9 -10
  24. package/workspace/guide/reports.md +17 -1
  25. package/workspace/guide/settings.md +16 -69
  26. package/workspace/guide/today.md +12 -188
  27. package/workspace/lib/report-checks.js +3 -0
  28. package/workspace/lib/report-cli.js +112 -3
  29. package/workspace/lib/report-engine.js +779 -0
  30. package/workspace/lib/report-export.js +36 -1
  31. package/workspace/lib/report-pyramid.js +20 -0
  32. package/workspace/lib/routes-ready-reports.js +3 -1
  33. package/workspace/lib/routes-report-design.js +7 -2
  34. package/workspace/lib/routes-report-setups.js +17 -2
  35. package/workspace/package-lock.json +2 -2
  36. package/workspace/package.json +1 -1
  37. package/workspace/public/pm/cozy-evui.js +1 -0
  38. package/workspace/public/pm/cozy-nav.js +4 -1
  39. package/workspace/public/pm/premium-vw-guide.css +36 -0
  40. package/workspace/public/pm/premium-vw-guide.js +137 -0
  41. package/workspace/public/pm/premium-vw-open.js +4 -1
  42. package/workspace/public/pm/premium-vw-page.js +2 -1
  43. package/workspace/run-tests.js +6 -0
  44. package/workspace/skills/report-biweekly.skill +7 -0
  45. package/workspace/skills/report-daily.skill +7 -0
  46. package/workspace/skills/report-monthly.skill +7 -0
  47. package/workspace/skills/report-special.skill +2 -0
  48. package/workspace/skills/report-template.skill +8 -1
  49. package/workspace/skills/report-weekly.skill +7 -0
  50. package/workspace/skills/report-write.skill +44 -0
  51. package/workspace/test-lists/report-engine.json +5 -0
  52. package/workspace/guide/ask-the-agent.md +0 -46
  53. package/workspace/guide/briefing-cards.md +0 -52
  54. package/workspace/guide/briefing.md +0 -159
  55. package/workspace/guide/calendar-popup.md +0 -46
  56. package/workspace/guide/command-palette.md +0 -77
  57. package/workspace/guide/console3-sessions.md +0 -147
  58. package/workspace/guide/document-sheet.md +0 -89
  59. package/workspace/guide/initiative-boards.md +0 -123
  60. package/workspace/guide/initiatives-screen.md +0 -179
  61. package/workspace/guide/initiatives-timeline.md +0 -55
  62. package/workspace/guide/phone-companion.md +0 -89
  63. package/workspace/guide/reports-dashboards.md +0 -70
  64. package/workspace/guide/reports-page.md +0 -87
  65. package/workspace/guide/timeline.md +0 -70
@@ -332,6 +332,40 @@ function pyramidSlideXml(s, L) {
332
332
  let yy = y + (n ? 146 : 0);
333
333
  if (a.more) { text(67, yy, 1146, 60, a.more, { size: 18, color: L.soft }); yy += 70; }
334
334
  text(67, yy, 1146, 26, [[['Who ', { bold: true }], [a.who, {}], ...(a.by ? [[' By ', { bold: true }], [a.by, {}]] : [])]], { size: 18, color: L.soft });
335
+ } else if (s.kind === 'beyond') {
336
+ // the report engine's parts beyond the question: numbered, as the answer's supports
337
+ s.items.forEach((x, i) => {
338
+ const yy = y + i * 104;
339
+ if (i) shape(67, yy - 10, 1146, 1, { fill: L.line });
340
+ shape(67, yy, 35, 35, { geom: 'ellipse', fill: L.accent });
341
+ text(67, yy + 6, 35, 24, String(i + 1), { size: 18, bold: true, color: '#FFFFFF', align: 'ctr' });
342
+ text(121, yy, 1092, 30, x.head, { size: 22, bold: true });
343
+ text(121, yy + 32, 1092, 60, x.text, { size: 17, color: L.soft });
344
+ });
345
+ } else if (s.kind === 'section') {
346
+ // a section of an engine report: paragraphs and lists as text, a table as a grid, bars as shapes. Anything below the footer is cut, never squeezed.
347
+ for (const b of s.blocks || []) {
348
+ if (y > 600) break;
349
+ if (b.kind === 'p') { const h = Math.min(120, 26 * Math.ceil(b.text.length / 110) + 6); text(67, y, 1146, h, b.text, { size: 18, color: L.soft }); y += h + 10; }
350
+ else if (b.kind === 'list') { for (const it of b.items) { if (y > 620) break; text(67, y, 1146, 30, '• ' + it, { size: 18, color: L.soft }); y += 34; } y += 8; }
351
+ else if (b.kind === 'table') {
352
+ const n = b.columns.length; const w = 1146 / n; const rowH = 40;
353
+ b.columns.forEach((c, i) => text(67 + i * w + 6, y, w - 12, 22, c.toUpperCase(), { size: 13, bold: true, color: L.muted }));
354
+ shape(67, y + 26, 1146, 2, { fill: '#D3CCBF' }); y += 34;
355
+ for (const r of b.rows) { if (y > 630) break; r.forEach((c, i) => text(67 + i * w + 6, y + 8, w - 12, 26, c, { size: 15, bold: i === 0 })); shape(67, y + rowH, 1146, 1, { fill: L.line }); y += rowH; }
356
+ y += 12;
357
+ } else if (b.kind === 'bars') {
358
+ for (const r of b.rows) {
359
+ if (y > 630) break;
360
+ text(67, y, 290, 26, r.label, { size: 17 });
361
+ shape(367, y + 4, 700, 18, { geom: 'roundRect', fill: L.track });
362
+ shape(367, y + 4, Math.max(1, 700 * r.share), 18, { geom: 'roundRect', fill: L.accent });
363
+ text(1083, y, 130, 26, r.text, { size: 17, bold: true, align: 'r' });
364
+ y += 36;
365
+ }
366
+ y += 10;
367
+ }
368
+ }
335
369
  } else if (s.kind === 'sources') {
336
370
  text(67, y, 540, 20, 'REACHED', { size: 15, bold: true, color: L.muted });
337
371
  (s.reached.length ? s.reached : ['None']).slice(0, 8).forEach((x, i) => text(67, y + 34 + i * 44, 540, 40, x, { size: 18, color: L.soft }));
@@ -480,7 +514,8 @@ function exportOf(markdown, fmt, opts = {}) {
480
514
  if (!Object.prototype.hasOwnProperty.call(FORMATS, fmt)) return { ok: false, error: `fmt must be one of ${Object.keys(FORMATS).join(', ')}` };
481
515
  const report = parseReport(markdown);
482
516
  let buffer;
483
- try { buffer = fmt === 'pptx' ? pptx(report, require('./report-pyramid.js').deckOf(markdown, opts.look, { data: opts.data || null })) : BUILD[fmt](report); } catch (e) { return { ok: false, error: 'the file could not be built: ' + String((e && e.message) || e).slice(0, 160) }; }
517
+ // opts.deck: the report engine's deck (lib/report-engine.js engineDeckOf), so the PPTX holds the slides the reader shows
518
+ try { buffer = fmt === 'pptx' ? pptx(report, opts.deck || require('./report-pyramid.js').deckOf(markdown, opts.look, { data: opts.data || null })) : BUILD[fmt](report); } catch (e) { return { ok: false, error: 'the file could not be built: ' + String((e && e.message) || e).slice(0, 160) }; }
484
519
  const stem = plain(report.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'report';
485
520
  return { ok: true, fmt, name: `${stem}.${fmt}`, mime: FORMATS[fmt].mime, buffer };
486
521
  }
@@ -460,6 +460,12 @@ function slideHtml(s, L) {
460
460
  const opts = a.options.map((o, i) => `<div class="ask${i ? ' alt' : ''}" style="flex:1"><div class="tag">Option ${'ABC'[i]}</div><div class="q">${esc(o)}</div></div>`).join('');
461
461
  body = (opts ? `<div style="display:flex;gap:26px">${opts}</div>` : '') + (a.more ? `<p class="sm" style="margin:${opts ? '22px' : '0'} 0 0">${esc(a.more)}</p>` : '')
462
462
  + `<div class="sm" style="display:flex;gap:45px;margin-top:29px"><span><b>Who</b> ${esc(a.who)}</span>${a.by ? `<span><b>By</b> ${esc(a.by)}</span>` : ''}</div>`;
463
+ } else if (s.kind === 'beyond') {
464
+ // the report engine's parts beyond the question (lib/report-engine.js deckOf): each part a numbered support
465
+ body = s.items.map((x, i) => `<div class="sup"><span class="n">${i + 1}</span><div><b>${esc(x.head)}</b><span>${esc(x.text)}</span></div></div>`).join('');
466
+ } else if (s.kind === 'section') {
467
+ // a section of an engine report: any title the writer chose, its blocks as they are (paragraphs, lists, a table, bars of readings)
468
+ body = (s.blocks || []).map((b) => sectionBlockHtml(b, L)).join('');
463
469
  } else if (s.kind === 'sources') {
464
470
  const reached = s.reached.map((x) => `<div class="sup"><span class="sm">${esc(x)}</span></div>`).join('') || '<div class="sup"><span class="sm">None</span></div>';
465
471
  const miss = s.notReached.map((x) => `<div class="sup"><div><b>${esc(x.source)}</b><span>${esc(cap(x.why))}${x.why ? '.' : ''}</span></div></div>`).join('');
@@ -467,6 +473,20 @@ function slideHtml(s, L) {
467
473
  }
468
474
  return open('') + `<div class="in">${head(s.tag)}${body}</div>${footer(s)}</section>`;
469
475
  }
476
+ /** One block of an engine section slide. */
477
+ function sectionBlockHtml(b, L) {
478
+ if (b.kind === 'p') return `<p class="sm" style="margin:0 0 14px;max-width:1120px">${esc(b.text)}</p>`;
479
+ if (b.kind === 'list') return `<div style="margin:0 0 10px">${b.items.map((x) => `<div class="sup" style="padding:9px 0"><span class="sm">${esc(x)}</span></div>`).join('')}</div>`;
480
+ if (b.kind === 'table') {
481
+ return `<div class="tb" style="grid-template-columns:repeat(${b.columns.length},minmax(0,1fr));font-size:16px;margin:0 0 14px;align-items:stretch">${b.columns.map((c) => `<div class="h">${esc(c)}</div>`).join('')}`
482
+ + b.rows.map((r) => r.map((c, i) => `<div${i ? '' : ' style="font-weight:800"'}>${esc(c)}</div>`).join('')).join('') + '</div>';
483
+ }
484
+ if (b.kind === 'bars') {
485
+ return `<div style="display:flex;flex-direction:column;gap:12px;margin:0 0 14px">${b.rows.map((r) => `<div style="display:flex;align-items:center;gap:16px;font-size:18px"><span style="width:300px;flex:none">${esc(r.label)}</span>`
486
+ + `<span style="flex:1;height:20px;background:${L.track};border-radius:4px;overflow:hidden"><i style="display:block;height:100%;width:${Math.round(r.share * 1000) / 10}%;background:${L.accent}"></i></span><b style="width:120px;flex:none;text-align:right">${esc(r.text)}</b></div>`).join('')}</div>`;
487
+ }
488
+ return '';
489
+ }
470
490
  /** The deck as one HTML string (the viewer's frames and the PDF print). */
471
491
  function deckHtml(deck) {
472
492
  if (!deck) return '';
@@ -437,7 +437,9 @@ async function tickScope(ctx, w, s, now, cache = null) {
437
437
  const e = ensureSetup(w, s.scope, m.kind, s.name, { now });
438
438
  if (!e.ok) { out.push({ scope: s.scope, kind: m.kind, ok: false, error: e.error }); continue; }
439
439
  const r = await SR.draftIntelligence(ctx, w, e.setup, m.slot, { model: X.model, now, ready: extras(X) });
440
- if (r.ok || r.existing) RR.append(RR.ledgerFile(stateDir(w)), { ev: 'moment', key, slot: m.slot, why: m.why }, { now });
440
+ if (r.ok || r.existing || r.quiet) RR.append(RR.ledgerFile(stateDir(w)), { ev: 'moment', key, slot: m.slot, why: m.why }, { now });
441
+ // nothing to report at this moment: one quiet line (Your day says it), no draft to grade
442
+ if (r.quiet) RR.append(RR.ledgerFile(stateDir(w)), { ev: 'quiet', key, slot: m.slot }, { now });
441
443
  out.push({ scope: s.scope, kind: m.kind, slot: m.slot, why: m.why, ...r });
442
444
  }
443
445
  // a heads-up: a broken health budget or a critical slip, at once and outside the schedule (PbG3)
@@ -49,6 +49,7 @@ const RS = require('./report-setups.js');
49
49
  const RC = require('./report-checks.js');
50
50
  const RDraft = require('./report-draft.js');
51
51
  const RP = require('./report-pyramid.js');
52
+ const RE = require('./report-engine.js');
52
53
  const headless = require('./headless.js');
53
54
 
54
55
  const text = (v) => (typeof v === 'string' ? v : '');
@@ -293,6 +294,8 @@ function startCycle(ctx, w, cadence, rel) {
293
294
  const st = D.stateOf(w.root, cadence);
294
295
  if (!st.saved) return { ok: false, error: 'no saved design' };
295
296
  if (D.isIssuedSlot(w.root, cadence, c.slot)) return { ok: false, error: 'issued' };
297
+ // the report engine draws its own deck from report.json; the saved design is its theme, so no per-cycle render rewrites its words
298
+ if (RE.engineReportOf(c.md)) return { ok: false, error: 'drawn by the report engine' };
296
299
  const key = keyOf(w.root, cadence);
297
300
  if (jobs.has(key)) {
298
301
  const j = jobs.get(key);
@@ -436,7 +439,8 @@ function cycleAnswer(w, c) {
436
439
  const differences = rec && Array.isArray(rec.differences) ? rec.differences : [];
437
440
  // designed: THIS cycle has a render drawn to a saved design (the contract's answer is per document). A cycle drafted before the design,
438
441
  // or one whose first render is still running, is not designed yet: the page reads it in its format view and fetches no cycle-html.
439
- return { ok: true, designed: !!st.saved && !!rec, design_version: rec ? rec.design_version : null,
442
+ const engine = !!RE.engineReportOf(c.md);
443
+ return { ok: true, designed: !engine && !!st.saved && !!rec, engine, design_version: rec ? rec.design_version : null,
440
444
  format: (rec && rec.format) || (st.saved && (D.versionMeta(w.root, c.cadence, st.saved.version) || {}).format) || st.format || 'deck',
441
445
  outline: rec && Array.isArray(rec.outline) ? rec.outline : [], differences, decisions: rec && Array.isArray(rec.decisions) ? rec.decisions : [],
442
446
  matches: !!rec && differences.length === 0, rendering: !!(j && j.slotPath === c.rel), previous: prev ? { path: prev.path, slot: prev.slot } : null };
@@ -482,7 +486,8 @@ async function handle(req, res, url, ctx) {
482
486
  // The finished slides of a cadence draft (canvas RsA): the Pyramid deck in the report's saved look, else the standard PMOS look.
483
487
  const d = reportDoc(w, text(url.searchParams.get('path'))); if (d.error) return send(d.error);
484
488
  let md; try { md = fs.readFileSync(d.c.md, 'utf8'); } catch { return send(bad(404, 'no such report document')); }
485
- const deck = RP.deckOf(md, RP.lookOf(w.root, d.c.cadence), { data: RP.dataOf(d.c.md) });
489
+ // a draft the report engine wrote is drawn from its own model (report.json), in the report's look: the reader, the PDF and the PPTX are one deck
490
+ const deck = RE.engineDeckOf(d.c.md, RP.lookOf(w.root, d.c.cadence)) || RP.deckOf(md, RP.lookOf(w.root, d.c.cadence), { data: RP.dataOf(d.c.md) });
486
491
  if (!deck) return send(bad(404, 'this document is not a cadence draft, so it has no Pyramid deck'));
487
492
  return sendHtml(res, D.sanitize(RP.deckHtml(deck)));
488
493
  }
@@ -254,7 +254,10 @@ async function tick(ctx, now = new Date()) {
254
254
  if (s.drafted.some((d) => d.slot === slot)) continue;
255
255
  let out;
256
256
  if (IR.isIntelligence(s.source)) {
257
+ const qk = `${w.root}|${s.id}|${slot}`;
258
+ if (quietSlots.has(qk)) continue; // nothing to report at this slot already: not read again every tick
257
259
  try { out = await draftIntelligence({ ...ctx, getWs: (id) => (id === w.id || id == null ? w : ctx.getWs(id)) }, w, s, slot, { now }); } catch (e) { out = { ok: false, error: String((e && e.message) || e) }; }
260
+ if (out && out.quiet) quietSlots.add(qk);
258
261
  done.push({ id: s.id, slot, ...out });
259
262
  continue;
260
263
  }
@@ -293,7 +296,9 @@ function exportAnswer(ctx, res, w, url) {
293
296
  } catch { return bad(404, 'no such document'); }
294
297
  const cyc = /(?:^|\/)reports\/([a-z][a-z0-9-]{0,39})\/\d{4}-\d{2}-\d{2}\.md$/.exec(rel);
295
298
  const RPy = require('./report-pyramid.js');
296
- const out = RX.exportOf(md, fmt, fmt === 'pptx' && cyc ? { look: RPy.lookOf(abs, cyc[1]), data: RPy.dataOf(full) } : {});
299
+ const look = fmt === 'pptx' && cyc ? RPy.lookOf(abs, cyc[1]) : null;
300
+ const deck = look ? require('./report-engine.js').engineDeckOf(full, look) : null;
301
+ const out = RX.exportOf(md, fmt, fmt === 'pptx' && cyc ? { look, data: RPy.dataOf(full), ...(deck ? { deck } : {}) } : {});
297
302
  if (!out.ok) return bad(500, out.error);
298
303
  return { status: 200, file: out };
299
304
  }
@@ -563,6 +568,8 @@ async function draftIntelligence(ctx, w, s, slot, { model, now = new Date(), rea
563
568
  try { if (fs.statSync(path.join(path.resolve(w.root), rel)).isFile()) return { ok: false, existing: true, slot, path: rel, error: 'drafted already' }; } catch { /* not there yet */ }
564
569
  const m = model || await intelModel(null, ctx, w.id, null);
565
570
  if (!m) return { ok: false, error: 'Intelligence could not be read' };
571
+ // no draft without a finding (design R item 2, report engine E step 3): a scope with no reading, no verdict and no read budget has nothing to report
572
+ if (!hasFinding(m, s)) return { ok: false, quiet: true, status: 422, slot, error: 'nothing to report: no goal here has a reading yet' };
566
573
  const mine = (RN.reportsOf(w.root).reports || []).filter((x) => x.cadence === s.id && x.slot < slot).sort((a, b) => (a.slot < b.slot ? -1 : 1));
567
574
  const before = mine.filter((x) => x.issued).pop() || null;
568
575
  let prior = null;
@@ -572,6 +579,14 @@ async function draftIntelligence(ctx, w, s, slot, { model, now = new Date(), rea
572
579
  return r.ok ? { ok: true, slot, path: r.path } : { ok: false, status: r.status, error: r.error };
573
580
  }
574
581
 
582
+ /** The slots of Intelligence reports that had nothing to report, in this process: the tick does not read them again. */
583
+ const quietSlots = new Set();
584
+ /** True when Intelligence holds something to report for the setup's scope: a reading on a goal or budget, or a recorded verdict. */
585
+ function hasFinding(model, setup) {
586
+ const scope = IR.scopeOf(setup && setup.source) || 'view';
587
+ const sl = IR.storylineOf(model, { scope });
588
+ return sl.items.some((it) => it.kind === 'scorecard' || it.kind === 'exhibit' || it.kind === 'verdict');
589
+ }
575
590
  /** Draft a report's cycle now through the one writer (report-draft.js writeDraft). → { slot, path } | { error } */
576
591
  function draftCycle(root, cadence, slot, actor) {
577
592
  const d = RD.draftOf(root, { cadence, slot: slot || undefined });
@@ -587,4 +602,4 @@ function firstDraft(root, r, actor) {
587
602
  return draftCycle(root, r.setup.id, r.first_draft && r.first_draft.slot, actor);
588
603
  }
589
604
 
590
- module.exports = { prefixes: ['/api/report-setups', '/api/report-documents', '/api/report-export'], handle, tick, ensureMigrated, documentsOf, listAnswer, copyOf, draftIntelligence, intelModel };
605
+ module.exports = { prefixes: ['/api/report-setups', '/api/report-documents', '/api/report-export'], handle, tick, ensureMigrated, documentsOf, listAnswer, copyOf, draftIntelligence, intelModel, hasFinding };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.516.0",
3
+ "version": "0.517.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.516.0",
9
+ "version": "0.517.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.516.0",
3
+ "version": "0.517.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",
@@ -23,6 +23,7 @@ export const ICON = {
23
23
  refresh: 'M4 12a8 8 0 0 1 14-5.3M20 4v4h-4M20 12a8 8 0 0 1-14 5.3M4 20v-4h4', jira: 'M4 4h16v16H4zM8.5 12.5l2.5 2.5 4.5-5', cal: 'M4 5h16v15H4zM4 10h16M8 3v4M16 3v4',
24
24
  repo: 'M6 3h12v18l-6-4-6 4z', mic: 'M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM5 11a7 7 0 0 0 14 0M12 18v3', tools: 'M3 6h18v12H3zM3 7l9 6 9-6',
25
25
  analytics: 'M4 20V10M10 20V4M16 20v-8M22 20H2', check: 'M5 12l4.5 4.5L19 7',
26
+ guide: 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6v.6M12 17h.01',
26
27
  };
27
28
  export function toast(t) { const el = typeof document !== 'undefined' && document.getElementById('toast'); if (!el) return; el.textContent = t; el.classList.add('on'); clearTimeout(toast.t); toast.t = setTimeout(() => el.classList.remove('on'), 4200); }
28
29
  export const wsParam = () => (typeof location !== 'undefined' ? new URLSearchParams(location.search).get('ws') || '' : '');
@@ -3,12 +3,13 @@
3
3
  * item 5, canvas FmA1Menu and FmA4Desk: fewer menus).
4
4
  *
5
5
  * One button, bottom left on desktop and bottom right on a phone, opens the menu: the three places, Home, Board and Intelligence; then a
6
- * tray of two icons, Search (⌘K on a desk) and Settings. It is the one way between places, and it holds nothing else: New for you is a fold
6
+ * tray of three icons, Search (⌘K on a desk), Guide and Settings. It is the one way between places, and it holds nothing else: New for you is a fold
7
7
  * in Your day, Sources are Settings › Connectors, Theme is Settings › General, and More is gone (feedback batch 16 item 5).
8
8
  * The button keeps its two dots on every page: blue when something is new for you, amber when a source is stale. Open, the blue dot sits on
9
9
  * the Home row (where Your day is) and the amber dot on the Settings icon; the button's own name says both ("Open the menu. 3 new for you.
10
10
  * Jira is stale.").
11
11
  * Search pm/cozy-search.js: across the repository and every worktree; Ctrl K (Cmd K) or "/" opens it from any page
12
+ * Guide pm/premium-vw-guide.js: the user guide's Start here, in the Viewer of the page you are on (initiative guide-in-app)
12
13
  * Settings pm/cozy-settings.js; it opens at Connectors while a source is stale, so the stale one is the first thing there
13
14
  * A page that passes opts.here() runs it when its own place is pressed, in place of a reload (Home: Menu > Home closes the page that is open).
14
15
  * The counts come from GET /api/news-for-you and GET /api/sources, read when the page opens and every two minutes while it is shown; each
@@ -55,6 +56,7 @@ export function menuHtml(here, o = {}) {
55
56
  }).join('')
56
57
  + '<div class="ev-tray">'
57
58
  + `<button type="button" class="ev-tb" data-cz-search aria-label="Search (⌘K)" title="Search ⌘K">${svg(ICON.search, 20)}</button>`
59
+ + `<button type="button" class="ev-tb" data-cz-guide aria-label="Guide" title="Guide">${svg(ICON.guide, 20)}</button>`
58
60
  + `<button type="button" class="ev-tb" data-cz-settings aria-label="Settings${o.stale ? '. ' + esc(o.stale.text) : ''}" title="Settings">${svg(ICON.settings, 20)}${o.stale ? dot('risk') : ''}</button>`
59
61
  + '</div>';
60
62
  }
@@ -116,6 +118,7 @@ export function mountNav(here, doc = document, opts = {}) {
116
118
  set(false);
117
119
  if ('czSearch' in b.dataset) import('./cozy-search.js').then((M) => M.openSearch(opts.search ? { go: opts.search() } : {}));
118
120
  // a stale source opens Settings at Connectors, the stale one first (FmA3Settings: open where it matters)
121
+ else if ('czGuide' in b.dataset) import('./premium-vw-open.js').then((M) => M.openGuide('index'));
119
122
  else if ('czSettings' in b.dataset) import('./cozy-settings.js').then((M) => M.openSettings(N.stale ? 'connectors' : undefined));
120
123
  });
121
124
  // Ctrl K (Cmd K) and "/" open Search from any page; a second press focuses it
@@ -0,0 +1,36 @@
1
+ /* premium-vw-guide.css — the guide in the Viewer (premium-vw-guide.js; canvas Exploring › User guide, option B): a page rail on the left,
2
+ * the page on the right with its section chips. Under 720 px the rail hides and the list icon shows it over the page. Quiet: hairlines, one
3
+ * well for the open page, no colour but the chips' hover. */
4
+ .vwdoc.gd{padding:0}
5
+ .gd-wrap{container-type:inline-size;display:flex;align-items:flex-start;gap:20px;position:relative}
6
+ .gd-rail{width:248px;flex:none;position:sticky;top:0;max-height:calc(100vh - 140px);overflow:auto;padding:4px 4px 16px 0;display:flex;flex-direction:column;gap:2px}
7
+ .gd-read{flex:1;min-width:0}
8
+ .gd-tog{display:none}
9
+ .gd-find{display:flex;align-items:center;gap:8px;height:40px;padding:0 10px;margin:0 0 6px;border-radius:10px;background:var(--well);color:var(--ink-3)}
10
+ .gd-find .i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;flex:none}
11
+ .gd-find input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--sans);outline:none;box-shadow:none}
12
+ .gd-find input:focus,.gd-find input:focus-visible{outline:none}
13
+ .gd-find:focus-within{outline:2px solid var(--ember);outline-offset:1px}
14
+ .gd-grp summary{list-style:none;cursor:pointer;padding:8px 8px 4px;color:var(--ink-3);font:var(--fw-strong) var(--fs-meta)/var(--lh-meta) var(--sans);border-radius:6px}
15
+ .gd-grp summary::-webkit-details-marker{display:none}
16
+ .gd-grp summary::after{content:"";display:inline-block;width:6px;height:6px;margin-left:8px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-2px) rotate(45deg)}
17
+ .gd-grp:not([open]) summary::after{transform:translateY(0) rotate(-45deg)}
18
+ .gd-grp summary:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
19
+ .gd-grp ul{list-style:none;margin:0;padding:0}
20
+ .gd-grp a{display:block;padding:5px 8px;border-radius:6px;color:var(--ink-2);text-decoration:none;font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
21
+ .gd-grp a:hover{background:var(--well);color:var(--ink)}
22
+ .gd-grp a[aria-current="page"]{background:var(--well);color:var(--ink);font-weight:var(--fw-strong)}
23
+ .gd-grp a:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
24
+ .gd-rail [hidden]{display:none}
25
+ .gd-none{margin:8px;color:var(--ink-3);font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
26
+ .gd-secs{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}
27
+ .gd-chip{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;border-radius:999px;background:var(--well);color:var(--ink-2);text-decoration:none;font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
28
+ .gd-chip:hover{color:var(--ink)}
29
+ .gd-chip[hidden]{display:none}
30
+ .gd-more{border:0;cursor:pointer;color:var(--ink-3)}
31
+ .gd-chip:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
32
+ @container (max-width: 720px){
33
+ .gd-rail{display:none}
34
+ .gd-tog{display:flex}
35
+ .gd-railon .gd-rail{display:flex;position:absolute;z-index:3;top:64px;left:0;right:0;width:auto;max-height:70vh;padding:12px;border:1px solid var(--hair);border-radius:14px;background:var(--card-solid);box-shadow:var(--card-shadow)}
36
+ }
@@ -0,0 +1,137 @@
1
+ /* pm/premium-vw-guide.js — the user guide, read in the Viewer (initiative guide-in-app; canvas Exploring › User guide, option B, Wawan 2026-11-03).
2
+ *
3
+ * The guide ships with the app (workspace/guide/*.md, GET /api/guide). The menu's Guide icon (cozy-nav.js) opens its start page here, and
4
+ * /premium-viewer.html?open=guide&page=<name> opens any page on its own. The guide opens wide (the Viewer's wide mode) unless the PM set a width
5
+ * for it before.
6
+ * Rail every page, grouped by Start here's own headings, with Find a page on top; the open page is marked; a group folds and stays folded.
7
+ * Page the title, then one chip per section (a press scrolls there), then the text. A link to another guide page opens that page in the same
8
+ * Viewer card as a drill-in, so ← comes back. A link to a repository file is drawn as text.
9
+ * Narrow under 720 px (a phone, or a narrow Viewer) the rail hides; the list icon beside the title shows it over the page.
10
+ * Nothing here writes, except the folded groups, kept in this browser.
11
+ * openGuide(page = 'index') -> the pane
12
+ * guideHtml(name, text, groups) -> the page as html (pure)
13
+ * guideLinks(md) -> the markdown with each link to a guide page rewritten to #guide:<name> (pure)
14
+ * contentsOf(indexMd) -> [{ group, pages: [{ page, title }] }] from Start here's headings and links (pure) */
15
+ import { vwOpen, vwStyle } from './premium-viewer.js';
16
+ import { drill, wide, widthOf } from './cozy-viewer.js';
17
+ import { bodyHtml, pageHead, splitDoc, readMinutes } from './premium-vw-doc-model.js';
18
+
19
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
20
+ const NAME = /^[a-z0-9][a-z0-9-]*$/;
21
+ const TOC_ICON = '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01"/></svg>';
22
+ const FIND_ICON = '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4a7 7 0 1 0 0 14a7 7 0 1 0 0-14M20 20l-4-4"/></svg>';
23
+ const FOLD_KEY = 'pmos.guide.folded';
24
+ const CHIPS = 6;
25
+ let TOC = null; // the groups, read once from Start here
26
+
27
+ const store = { get() { try { return JSON.parse(localStorage.getItem(FOLD_KEY) || '[]'); } catch { return []; } }, set(v) { try { localStorage.setItem(FOLD_KEY, JSON.stringify(v)); } catch { /* a private window */ } } };
28
+
29
+ /** Start here's groups: each ## or ### heading from "Read next" on, with the guide pages its list links, in order. A page linked twice keeps its first place. */
30
+ export function contentsOf(md) {
31
+ const out = [], seen = new Set(['index']); let cur = null, on = false;
32
+ for (const l of String(md || '').replace(/\r/g, '').split('\n')) {
33
+ const h = /^#{2,3}\s+(.*)$/.exec(l);
34
+ if (h && /^Read next$/i.test(h[1].trim())) { on = true; cur = null; continue; }
35
+ if (h && on) { cur = { group: h[1].trim(), pages: [] }; out.push(cur); continue; }
36
+ if (!cur) continue;
37
+ for (const m of l.matchAll(/\[([^\]]+)\]\(([a-z0-9][a-z0-9-]*)\.md\)/g)) if (!seen.has(m[2])) { seen.add(m[2]); cur.pages.push({ page: m[2], title: m[1] }); }
38
+ }
39
+ return [{ group: 'Start here', pages: [{ page: 'index', title: 'Start here' }] }, ...out.filter((g) => g.pages.length)];
40
+ }
41
+
42
+ /** The rail: Find a page, then every page by group, the open one marked. folded: the group names kept folded. */
43
+ export function railHtml(name, groups, folded = []) {
44
+ return '<nav class="gd-rail" aria-label="Guide pages">'
45
+ + `<div class="gd-find">${FIND_ICON}<input type="search" placeholder="Find a page" aria-label="Find a page" autocomplete="off" data-gd-find></div>`
46
+ + (groups || []).map((g) => {
47
+ const here = g.pages.some((x) => x.page === name), open = here || !folded.includes(g.group);
48
+ return `<details class="gd-grp" data-gd-grp="${esc(g.group)}"${open ? ' open' : ''}><summary>${esc(g.group)}</summary><ul>`
49
+ + g.pages.map((x) => `<li><a href="#guide:${esc(x.page)}"${x.page === name ? ' aria-current="page"' : ''} data-gd-t="${esc(x.title.toLowerCase())}">${esc(x.title)}</a></li>`).join('')
50
+ + '</ul></details>';
51
+ }).join('')
52
+ + '<p class="gd-none" data-gd-none hidden>No page has that name.</p></nav>';
53
+ }
54
+
55
+ /** Each markdown link to a guide page (`[x](layout.md)`, `[x](layout.md#keys)`) becomes `[x](#guide:layout)`, which the page model keeps as a link. */
56
+ export const guideLinks = (md) => String(md || '').replace(/\]\(([a-z0-9][a-z0-9-]*)\.md(?:#[^)\s]*)?\)/g, '](#guide:$1)');
57
+
58
+ /** One guide page: the rail, then the page with its title, its section chips, the text and a quiet footer. */
59
+ export function guideHtml(name, text, groups = TOC, folded = []) {
60
+ const t = guideLinks(text), h = pageHead(t), sd = splitDoc(t);
61
+ const b = bodyHtml([sd.pre, ...sd.sections.map((s, n) => (n === 0 && h.h1 ? s.body : s.head + '\n' + s.body))].join('\n'));
62
+ const secs = b.heads.filter((x) => x[0] === 2);
63
+ // At most 6 chips show; the rest wait behind "N more", so a long page does not open on a wall of chips.
64
+ const chip = ([, s, id], n) => `<a href="#${esc(id)}" class="gd-chip" data-gd-jump="${esc(id)}"${n >= CHIPS ? ' hidden' : ''}>${esc(s)}</a>`;
65
+ const more = secs.length > CHIPS ? `<button type="button" class="gd-chip gd-more" data-gd-more>${secs.length - CHIPS} more</button>` : '';
66
+ const chips = secs.length > 1 ? `<div class="gd-secs" role="group" aria-label="On this page">${secs.map(chip).join('')}${more}</div>` : '';
67
+ const home = name === 'index' ? '' : '<span class="gap"></span><a href="#guide:index">Start here</a>';
68
+ return `<div class="vwdoc gd"><div class="gd-wrap">${railHtml(name, groups || [], folded)}`
69
+ + `<article class="cf vwfile gd-read"><header class="cfh vwfh"><h1 class="cft">${esc(h.title || name)}</h1>`
70
+ + `<div class="vwacts gd-tog"><button type="button" class="vwib" data-gd-toc aria-label="Guide pages" title="Guide pages" aria-expanded="false">${TOC_ICON}</button></div></header>`
71
+ + chips
72
+ + `<div class="cfb">${b.html || '<p class="vwmut">This page has no text yet.</p>'}</div>`
73
+ + `<footer class="cff"><span>Guide · ${readMinutes(text)} min read</span>${home}</footer></article></div></div>`;
74
+ }
75
+
76
+ const failHtml = (why) => `<div class="vwdoc"><div class="vwfnone" role="status"><b>The guide page could not be shown</b><p class="vwmut">${esc(why)}</p></div></div>`;
77
+
78
+ async function read(name) {
79
+ try {
80
+ const r = await fetch('/api/guide?page=' + encodeURIComponent(name), { credentials: 'same-origin' });
81
+ return r.ok ? { ok: true, text: await r.text() } : { ok: false, why: 'There is no guide page called ' + name + '.' };
82
+ } catch { return { ok: false, why: 'The workspace server did not answer.' }; }
83
+ }
84
+
85
+ function wire(el) {
86
+ const wrap = () => el.querySelector('.gd-wrap'), btn = () => el.querySelector('[data-gd-toc]');
87
+ const show = (on) => { const w = wrap(), b = btn(); if (!w || !b) return; w.classList.toggle('gd-railon', on); b.setAttribute('aria-expanded', String(on)); if (on) { const f = w.querySelector('[data-gd-find]'); if (f) f.focus(); } };
88
+ el.addEventListener('input', (e) => {
89
+ if (!e.target.matches('[data-gd-find]')) return;
90
+ const q = e.target.value.trim().toLowerCase(), r = el.querySelector('.gd-rail'); let any = false;
91
+ r.querySelectorAll('details.gd-grp').forEach((d) => {
92
+ let n = 0; d.querySelectorAll('li').forEach((li) => { const hit = !q || li.firstChild.dataset.gdT.includes(q); li.hidden = !hit; if (hit) n++; });
93
+ d.hidden = !n; if (q && n) d.open = true; if (n) any = true;
94
+ });
95
+ r.querySelector('[data-gd-none]').hidden = any;
96
+ });
97
+ el.addEventListener('toggle', (e) => {
98
+ const d = e.target; if (!d.matches || !d.matches('details.gd-grp') || el.querySelector('[data-gd-find]').value) return;
99
+ const k = d.dataset.gdGrp, f = store.get().filter((x) => x !== k);
100
+ if (!d.open) f.push(k);
101
+ store.set(f);
102
+ }, true);
103
+ el.addEventListener('keydown', (e) => { if (e.key === 'Escape' && wrap() && wrap().classList.contains('gd-railon')) { e.stopPropagation(); show(false); btn().focus(); } });
104
+ el.addEventListener('click', (e) => {
105
+ if (e.target.closest('[data-gd-toc]')) { show(!wrap().classList.contains('gd-railon')); return; }
106
+ const m = e.target.closest('[data-gd-more]');
107
+ if (m) { m.parentElement.querySelectorAll('.gd-chip[hidden]').forEach((c) => { c.hidden = false; }); m.remove(); return; }
108
+ const j = e.target.closest('[data-gd-jump]');
109
+ if (j) { e.preventDefault(); const h = el.querySelector('#' + CSS.escape(j.dataset.gdJump)); if (h) h.scrollIntoView({ block: 'start', behavior: 'smooth' }); return; }
110
+ const a = e.target.closest('a'); if (!a) return;
111
+ const href = a.getAttribute('href') || '';
112
+ const g = /^#guide:([a-z0-9-]+)$/.exec(href);
113
+ if (g) { e.preventDefault(); drill(); openGuide(g[1]); return; }
114
+ if (href === '#') e.preventDefault(); // a link to a repository file: the page model drew it without a target
115
+ });
116
+ }
117
+
118
+ /** Open a guide page in the Viewer. */
119
+ export function openGuide(page = 'index') {
120
+ const name = NAME.test(String(page || '')) ? String(page) : 'index';
121
+ vwStyle('doc'); vwStyle('guide');
122
+ const id = 'guide:' + name;
123
+ let pane = null, done = null;
124
+ pane = vwOpen({ id, kind: 'Guide', title: 'Guide', icon: 'doc', url: '/premium-viewer.html?open=guide&page=' + name,
125
+ render: (el) => { el.innerHTML = done || '<div class="vwdoc"><p class="vwmut" role="status">Opening the guide.</p></div>'; wire(el); } });
126
+ try { if (!widthOf('Guide')) wide(true); } catch { /* a page with no cozy Viewer */ }
127
+ const toc = TOC ? Promise.resolve() : read('index').then((r) => { if (r.ok) TOC = contentsOf(r.text); });
128
+ Promise.all([read(name), toc]).then(([r]) => {
129
+ done = r.ok ? guideHtml(name, r.text, TOC, store.get()) : failHtml(r.why);
130
+ const el = document.querySelector(`.obody.vw[data-vw="${CSS.escape(id)}"]`) || pane;
131
+ if (el && el.isConnected) {
132
+ el.innerHTML = done; el.scrollTop = 0;
133
+ const cur = el.querySelector('.gd-rail [aria-current="page"]'); if (cur) cur.scrollIntoView({ block: 'nearest' });
134
+ }
135
+ });
136
+ return pane;
137
+ }
@@ -8,6 +8,7 @@
8
8
  * openReport(idOrRow, { ws, name, cycles, onIssue, onChange }) a report path reports/<cadence>/<date>.md or a Reports card row
9
9
  * newNote(ws | { ws }) starts an untitled note now and opens it, recording
10
10
  * makeWorkItem(noteId, step) Becomes V.4: a ticket drafted in PMOS from a next step (never pushed to Jira here)
11
+ * openGuide(page) a page of the user guide (workspace/guide/<page>.md); no page: Start here
11
12
  * The modules load on first use (dynamic import), so this file has no import cycle with the view modules that import it.
12
13
  * It also adds the Viewer's style sheet (cozy-vw.css) to the page once. */
13
14
 
@@ -19,6 +20,7 @@ export function vzStyle() {
19
20
  const mtg = () => import('./premium-vw-mtg.js');
20
21
  const cal = () => import('./premium-vw-cal.js');
21
22
  const rep = () => import('./premium-report-read.js');
23
+ const gd = () => import('./premium-vw-guide.js');
22
24
  const plain = (o) => (o && typeof o === 'object' && !o.openDlg && !o.$ ? o : {}); // an old caller passed the shell's deps
23
25
 
24
26
  export async function openMeeting(ref, o = {}) { vzStyle(); return (await mtg()).openMeeting(ref, plain(o)); }
@@ -27,8 +29,9 @@ export async function openNotes(o = {}) { vzStyle(); return (await cal()).openNo
27
29
  export async function openReport(ref, o = {}) { vzStyle(); return (await rep()).openReportView(ref, plain(o)); }
28
30
  export async function newNote(o) { vzStyle(); return (await mtg()).newNote(typeof o === 'string' ? o : (o && o.ws) || ''); }
29
31
  export async function makeWorkItem(noteId, step) { return (await mtg()).workItem(noteId, step); }
32
+ export async function openGuide(page) { vzStyle(); return (await gd()).openGuide(page); }
30
33
 
31
- export const views = { openMeeting, openCalendar, openNotes, openReport, newNote, makeWorkItem };
34
+ export const views = { openMeeting, openCalendar, openNotes, openReport, newNote, makeWorkItem, openGuide };
32
35
  if (typeof window !== 'undefined') {
33
36
  window.czViews = Object.assign(window.czViews || {}, views);
34
37
  window.pmViewer = window.czViews;
@@ -1,6 +1,6 @@
1
1
  /* pm/premium-vw-page.js — /premium-viewer.html: opens the item its address names in the Viewer card (initiative premium-agreed-design).
2
2
  * ?open=meeting&note=<id> | &event=<id>&date=YYYY-MM-DD ?open=calendar[&tab=notes][&date=] ?open=notes[&initiative=]
3
- * ?open=report&path=reports/<cadence>/<date>.md each with an optional &ws=
3
+ * ?open=report&path=reports/<cadence>/<date>.md ?open=guide[&page=<name>] each with an optional &ws=
4
4
  * Closing the card leaves a line that links to Home. An address that names nothing says so. */
5
5
  import { views } from './premium-vw-open.js';
6
6
  import { onChange } from './cozy-viewer.js';
@@ -13,4 +13,5 @@ if (what === 'meeting') views.openMeeting(q.get('note') ? { note: q.get('note')
13
13
  else if (what === 'calendar') views.openCalendar({ ws, tab: q.get('tab') === 'notes' ? 'notes' : 'cal', date: q.get('date') || '' });
14
14
  else if (what === 'notes') views.openNotes({ ws, initiative: q.get('initiative') || '' });
15
15
  else if (what === 'report' && q.get('path')) views.openReport(q.get('path'), { ws });
16
+ else if (what === 'guide') views.openGuide(q.get('page') || 'index');
16
17
  else gone.hidden = false;
@@ -498,6 +498,12 @@ function traceChanges(changes, ctx) {
498
498
  }
499
499
  if (/^(guide|docs)\/|\.md$/.test(f)) continue; // prose: no suite asserts on it except guide.test.js, a unit suite
500
500
  if (f === 'test-timings.json') continue; // run order only: no suite reads it
501
+ if (/^skills\/[^/]+\.skill$/.test(f) && get(f) != null) {
502
+ // a built-in skill is prose an agent reads: a suite reads it by its name, or loads the whole folder
503
+ nameHits(f).forEach((s) => picked.add(s));
504
+ for (const s of list) if (/\b(loadDir|readdirSync)\(path\.join\(__dirname, 'skills'\)/.test(get(s) || '')) picked.add(s);
505
+ continue;
506
+ }
501
507
  let why = null;
502
508
  const before = picked.size;
503
509
  if (get(f) == null) { // removed: only the names in the suites can say who used it
@@ -23,6 +23,13 @@ Rules that always hold (D71):
23
23
  7. THE SCRIPT WRITES EVERY NUMBER. In a note you type none: no count, no ratio such as "7 of 12", no percentage, no date, no day count. If a sentence needs a number, write it without the number; the block above the note already shows it. A name is not a number: an initiative id, a file path and a note id may be written as they are.
24
24
  8. PROSE ONLY BETWEEN NOTE MARKERS. In the draft file, write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
25
25
 
26
+ THE REPORT ENGINE FIRST (design E). A new biweekly report is written with the report engine:
27
+ - Run `pmos report make biweekly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
28
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read biweekly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
29
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
30
+ - If `pmos report make biweekly` refuses because the definition names no purpose, use the procedure below.
31
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
32
+
26
33
  Procedure:
27
34
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
28
35
  2. Read the definition: `<state>/reports/definitions/biweekly.md`.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new daily report is written with the report engine:
26
+ - Run `pmos report make daily`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read daily --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make daily` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/daily.md`.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new monthly report is written with the report engine:
26
+ - Run `pmos report make monthly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read monthly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make monthly` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/monthly.md`.
@@ -13,6 +13,8 @@ output:
13
13
 
14
14
  /report-special — prepare a one-off report for THIS repository as a draft.
15
15
 
16
+ THE REPORT ENGINE FIRST (design E). A one-off report is an ad hoc report of the engine: once the PM has said what it is for, follow `skills/report-write.skill` with an id you make from the topic (lower case, dashes) and the PM's question as `--ask`. You choose the outline; every figure is a reading. The procedure below is the older path: use it only when the engine refuses the brief.
17
+
16
18
  Rules that always hold (D71):
17
19
  1. DRAFT ONLY. You write a file in this repository. Never send, post, share, email, publish or upload the report anywhere; the PM does that.
18
20
  2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar or any other source.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: report-template
3
3
  label: "Saved report (from its template)"
4
- description: "Prepare a saved report as a draft. The schedule's args name the template; the script writes every number, then this skill writes the notes between the markers. Never sends or posts."
4
+ description: "Prepare a saved report as a draft with the report engine (pmos report make, then the report-write skill). The schedule's args name the template. Never sends or posts."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
@@ -23,6 +23,13 @@ Rules that always hold (D71):
23
23
  6. THE SCRIPT WRITES EVERY NUMBER. In a note you type no count, ratio, percentage or date.
24
24
  7. PROSE ONLY BETWEEN NOTE MARKERS. Write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
25
25
 
26
+ THE REPORT ENGINE FIRST (design E). A new cycle is written with the report engine, not with the note markers:
27
+ - Run `pmos report make <id>` (the id is the schedule's args). It derives the brief from the template's purpose, audience and sources, and from a saved structure's purpose and questions, reads every source PMOS reaches, and finds what changed.
28
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read <id> --from <file>`). If there is still no finding, there is nothing to report this cycle: stop and say so in one line. Do not draft a report about nothing.
29
+ - Otherwise follow `skills/report-write.skill` from its step 2. The template's sections are the agreed outline only when the last issue was written by the engine; you choose the outline. A saved structure's charts are sources to read: file each value as a reading.
30
+ - A change to a draft that has a `<slot>.report.json` is a change to that report.json: edit it and run `pmos report write <id> --slot <slot> --from <file>` again. A figure stays a reading.
31
+ The procedure below is for a change to a draft written before the engine (a draft with note markers and no `<slot>.report.json`), and for a template whose engine brief is refused.
32
+
26
33
  Procedure:
27
34
  1. The template id is the schedule's `args`. It must match `^[a-z][a-z0-9-]{0,39}$`. If it is missing or does not match, stop and say which schedule has no valid template id.
28
35
  2. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new weekly report is written with the report engine:
26
+ - Run `pmos report make weekly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read weekly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make weekly` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/weekly.md`.