create-pmos 0.93.0 → 0.95.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.93.0",
3
+ "version": "0.95.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@fdd90d9
1
+ kernel-source: pmos@1e39390
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T18:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
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-07T18:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
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-07T18:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -22,6 +22,7 @@
22
22
  // budget is for the whole screen and the bar is on every screen. The calibration stays specimen A's own count: the
23
23
  // specimen did not change. Every place +1 again for People in the top bar (the PM, 2026-10-06, people-directory), and
24
24
  // +1 again for My work beside Agenda (the PM approved that mockup and said build it, 2026-10-07, my-work).
25
+ // reports: +1 for New report in the Reports header (the PM asked for it, 2026-10-07, reports-new-session).
25
26
  const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
26
27
  const BAR_DOCS = 1;
27
28
  const BAR_PEOPLE = 1;
@@ -30,7 +31,7 @@ const BAR = BAR_DOCS + BAR_PEOPLE + BAR_MY_WORK;
30
31
  // Agenda opens on the month (the PM's Agenda mockup v5, 2026-10-06): the month adds its own controls to Today's budget,
31
32
  // at most six week rows of a week button and seven days, the two month arrows and the five lenses.
32
33
  const AGENDA_MONTH = 6 * 8 + 2 + 5;
33
- const EXPECT = Object.freeze({ today: 17 + BAR + AGENDA_MONTH, initiatives: 24 + BAR, board: 33 + BAR, reports: 15 + BAR });
34
+ const EXPECT = Object.freeze({ today: 17 + BAR + AGENDA_MONTH, initiatives: 24 + BAR, board: 33 + BAR, reports: 16 + BAR });
34
35
 
35
36
  /* ── shoot.js audit(), copied verbatim (scratchpad/specimens2/shoot.js, 2026-10-05) ─────────────────────────────── */
36
37
  // runs in the page
@@ -11,7 +11,7 @@ The Notes view shows one meeting, top to bottom. A bar at the top shows whether
11
11
 
12
12
  ## Start a meeting
13
13
 
14
- 1. Press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository.
14
+ 1. Press the **Notes** button in the top bar, or press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository. The **Notes** button shows a red dot when notes are being recorded.
15
15
  2. Press **New meeting**. Type a title.
16
16
  3. Under **Your notes**, type a note and press Enter. Each note is its own line, with the meeting time you added it at. To remove a line, press the **x** beside it. Each line also shows in the transcript as **You, typed** and is labelled like a spoken line, so a decision or a next step you type goes into its section. Your notes steer the summary, and they save at once.
17
17
  4. Press **All notes** to open an earlier note. The **⋯** menu has Copy as Markdown, New meeting, the meeting and the initiative, Summary by agent, Mark private, Settings and **Delete note**.
@@ -181,8 +181,16 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(
181
181
  try {
182
182
  fs.mkdirSync(path.dirname(logFile), { recursive: true });
183
183
  fd = fs.openSync(logFile, 'w');
184
+ fs.writeSync(fd, `${new Date().toISOString()} clearing npm cache…\n`);
185
+ fs.writeSync(fd, `${new Date().toISOString()} ${NPM} cache clean --force\n`);
184
186
  fs.writeSync(fd, `${new Date().toISOString()} ${NPX} -y ${PKG_NAME}@${version} workspace --reinstall ${root}\n`);
185
187
  } catch (e) { return { ok: false, error: `Could not write the log file: ${e.message}` }; }
188
+ try {
189
+ // Clear npm cache synchronously before spawning the update, so npx downloads the fresh version
190
+ cp.execSync(`${NPM} cache clean --force`, { cwd: String(root), env, stdio: ['ignore', fd, fd] });
191
+ } catch (e) {
192
+ try { fs.appendFileSync(logFile, `Note: cache clear had an issue, but continuing: ${e.message}\n`); } catch { /* silent */ }
193
+ }
186
194
  try {
187
195
  const child = spawn(NPX, ['-y', `${PKG_NAME}@${version}`, 'workspace', '--reinstall', String(root)],
188
196
  { cwd: String(root), env, detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
@@ -7,6 +7,7 @@
7
7
  * reportsOf(root) → { ok, reports: [{ cadence, slot, path, status, bytes, mtimeMs, issued, edited }],
8
8
  * flags, sig } the report files joined to issues.jsonl; the routes read it too
9
9
  * cadenceOfSkill(skill) 'report-biweekly' → 'biweekly', else null
10
+ * cadenceOfSchedule(s) a template schedule (skill report-template) → its args; else cadenceOfSkill(s.skill)
10
11
  *
11
12
  * THE THREE ROW KINDS (rows are held to fixtures/today-api/provider-rows.json):
12
13
  * report-issue kind 1. The newest draft of a cadence that the report script wrote, that is not issued, and that is
@@ -56,6 +57,11 @@ function reportsDir(root) {
56
57
  const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
57
58
  const short = (ymd) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(ymd || '')); return m ? `${Number(m[3])} ${MONTHS[Number(m[2]) - 1]}` : String(ymd || ''); };
58
59
  const cadenceOfSkill = (skill) => { const m = /^report-([a-z][a-z0-9-]{0,39})$/.exec(String(skill || '')); return m ? m[1] : null; };
60
+ /** A saved template runs the generic report-template skill; its schedule's args name the template, which is its cadence id. */
61
+ const cadenceOfSchedule = (s) => {
62
+ if (s && s.skill === 'report-template') { const a = String(s.args || '').trim(); return /^[a-z][a-z0-9-]{0,39}$/.test(a) ? a : null; }
63
+ return cadenceOfSkill(s && s.skill);
64
+ };
59
65
  /** What `git hash-object` prints for these bytes, without running git: the sha issues.jsonl records. */
60
66
  const blobSha = (buf) => crypto.createHash('sha1').update(`blob ${buf.length}\0`).update(buf).digest('hex');
61
67
 
@@ -173,7 +179,7 @@ function noOutputRows(w, now, schedules) {
173
179
  const out = [];
174
180
  const seen = new Set();
175
181
  for (const s of schedules) {
176
- const cadence = cadenceOfSkill(s && s.skill);
182
+ const cadence = cadenceOfSchedule(s);
177
183
  if (!cadence || s.ws !== w.id || s.enabled === false || !s.lastRun) continue;
178
184
  const fired = Date.parse(s.lastRun);
179
185
  if (!Number.isFinite(fired)) continue;
@@ -210,4 +216,4 @@ function sig(w) {
210
216
  return [reportsOf(w.root).sig, schedulesStamp(), kill, hour].join(':');
211
217
  }
212
218
 
213
- module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, cadenceOfSkill, blobSha, reportsDir, KILL_FILE, GRACE_MS };
219
+ module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, cadenceOfSkill, cadenceOfSchedule, blobSha, reportsDir, KILL_FILE, GRACE_MS };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.93.0",
3
+ "version": "0.95.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.93.0",
9
+ "version": "0.95.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.93.0",
3
+ "version": "0.95.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",
@@ -38,6 +38,7 @@ import { state, setState, subscribe, flush, FIXTURE, LEGACY_LAYOUT_FLAG } from '
38
38
  import { isMobile, mountMobile, applyMobile } from './mobile.js';
39
39
  /* The frame's parts (Run 3, slice 3A). Small modules with no effect at import: a fixture page loads them and calls none. */
40
40
  import { createTopbar, unsyncedOf } from './topbar.js';
41
+ import { openView } from './panel-cockpit.js';
41
42
  import { createDockRail } from './dock.js';
42
43
  import { SEL, ensureComponentsCss, iconButton, menu, toast, sheet, button } from './primitives.js';
43
44
  import { ensureSprite } from './icons.js';
@@ -607,7 +608,7 @@ const frEnd = el('span', { class: 'fr-end', 'aria-hidden': 'true' });
607
608
  function setupFrame() {
608
609
  ensureComponentsCss();
609
610
  ensureSprite();
610
- bar = createTopbar({ go, openPalette, repoMenu, sync: runSync });
611
+ bar = createTopbar({ go, openPalette, openView, repoMenu, sync: runSync });
611
612
  document.addEventListener('pointerdown', (e) => { fr.downAt = e.target; }, true);
612
613
  dockRail = createDockRail({ open: openDock, resize: dockWidth });
613
614
  fr.dockW = readDockW();
@@ -2342,6 +2342,17 @@
2342
2342
  font-family: var(--mono);
2343
2343
  font-size: 11px;
2344
2344
  }
2345
+ .fr-recording-dot {
2346
+ width: 8px;
2347
+ height: 8px;
2348
+ border-radius: 50%;
2349
+ background: var(--destructive);
2350
+ margin: 0 6px;
2351
+ display: none;
2352
+ }
2353
+ .fr-recording-dot--active {
2354
+ display: block;
2355
+ }
2345
2356
  /* The body: the destination, then (when open) the detail, the dock's rail and the dock. One row, columns in order. */
2346
2357
  .fr-body {
2347
2358
  display: grid;
@@ -4442,9 +4453,23 @@
4442
4453
  flex-wrap: wrap;
4443
4454
  gap: 8px;
4444
4455
  }
4456
+ .rp-card {
4457
+ align-items: stretch;
4458
+ padding: 10px 12px;
4459
+ border: 1px solid var(--border);
4460
+ border-radius: 8px;
4461
+ background: var(--card);
4462
+ }
4463
+ .rp-card > .rp-list { border-top: 0; }
4445
4464
  .rp-viewer {
4446
4465
  align-items: stretch;
4447
4466
  }
4467
+ .rp-viewer__type {
4468
+ margin: 0;
4469
+ font-size: 12px;
4470
+ line-height: 16px;
4471
+ color: var(--muted-foreground);
4472
+ }
4448
4473
  .rp-viewer__state {
4449
4474
  margin: 0;
4450
4475
  font-size: 12px;
@@ -5352,7 +5377,15 @@
5352
5377
 
5353
5378
  @container (min-width: 720px) {
5354
5379
  .rp-cols {
5355
- grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
5380
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
5381
+ align-items: start;
5382
+ }
5383
+ .rp-col--doc .rp-viewer {
5384
+ position: sticky;
5385
+ top: 8px;
5386
+ max-height: calc(100vh - 16px);
5387
+ overflow: auto;
5388
+ align-content: start;
5356
5389
  }
5357
5390
  }
5358
5391
  @media (pointer: coarse) {
@@ -31,6 +31,11 @@ const insts = new WeakMap(); // host -> instance
31
31
 
32
32
  /** report-biweekly -> biweekly. The same join as lib/report-needs.js cadenceOfSkill. */
33
33
  const cadenceOfSkill = (skill) => { const m = /^report-([a-z][a-z0-9-]{0,39})$/.exec(String(skill || '')); return m ? m[1] : null; };
34
+ /** A template schedule (skill report-template) names its template in args. The same join as lib/report-needs.js cadenceOfSchedule. */
35
+ const cadenceOfSchedule = (x) => {
36
+ if (x && x.skill === 'report-template') { const a = String(x.args || '').trim(); return /^[a-z][a-z0-9-]{0,39}$/.test(a) ? a : null; }
37
+ return cadenceOfSkill(x && x.skill);
38
+ };
34
39
  const keyOf = (r) => r.cadence + ':' + r.slot;
35
40
 
36
41
  function whenOf(s) {
@@ -43,7 +48,7 @@ function make(host) {
43
48
  const root = el('div', { class: 'rp-page', 'data-reports': '' });
44
49
  const inst = {
45
50
  host, root, ws: null, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null,
46
- sel: null, showAll: false, otherOpen: false, stop: null, seq: 0, last: '',
51
+ sel: null, showAll: false, otherOpen: false, locked: false, stop: null, seq: 0, last: '',
47
52
  };
48
53
  return inst;
49
54
  }
@@ -56,6 +61,7 @@ export function mountDestination(host, ctx) {
56
61
  if (ctx && ctx.FIXTURE) { inst.root.replaceChildren(empty({ text: COPY.fixture })); return; }
57
62
  const repo = (ctx && ctx.tab && ctx.tab.repo) || (ctx && ctx.state && ctx.state.product) || null;
58
63
  const ws = repo ? wsOf(repo) : null;
64
+ inst.repo = repo;
59
65
  if (ws !== inst.ws) {
60
66
  if (inst.stop) { inst.stop(); inst.stop = null; }
61
67
  Object.assign(inst, { ws, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null, sel: null, showAll: false, last: '' });
@@ -127,10 +133,10 @@ const isDraft = (r) => !!r && !r.issued;
127
133
  function draftCadence(inst) {
128
134
  const r = selected(inst);
129
135
  if (r) return r.cadence;
130
- const s = inst.schedules.map((x) => cadenceOfSkill(x.skill)).find(Boolean);
136
+ const s = inst.schedules.map((x) => cadenceOfSchedule(x)).find(Boolean);
131
137
  return s || 'biweekly';
132
138
  }
133
- const knownCadences = (inst) => [...new Set([...inst.reports.map((r) => r.cadence), ...inst.schedules.map((s) => cadenceOfSkill(s.skill)).filter(Boolean)])].sort();
139
+ const knownCadences = (inst) => [...new Set([...inst.reports.map((r) => r.cadence), ...inst.schedules.map((s) => cadenceOfSchedule(s)).filter(Boolean)])].sort();
134
140
 
135
141
  async function afterDraft(inst, r) {
136
142
  if (r && r.ok && r.slot) inst.sel = (r.cadence || draftCadence(inst)) + ':' + r.slot;
@@ -140,7 +146,7 @@ async function afterDraft(inst, r) {
140
146
  function primaryAction(inst) {
141
147
  const r = selected(inst);
142
148
  if (isDraft(r)) {
143
- return button({ label: COPY.issue, variant: 'primary', attrs: { 'data-rp-act': 'issue', 'data-rp-key': 'primary' },
149
+ return button({ label: COPY.issue, variant: 'primary', disabled: inst.locked, attrs: { 'data-rp-act': 'issue', 'data-rp-key': 'primary' },
144
150
  onClick: () => openIssueSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, onDone: () => refresh(inst) }) });
145
151
  }
146
152
  const cadence = draftCadence(inst);
@@ -148,36 +154,48 @@ function primaryAction(inst) {
148
154
  onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence })) });
149
155
  }
150
156
 
151
- /* the schedules, split by what they feed */
157
+ /** Set the recommended report schedules up (the one write that creates schedules; the schedules route, no new one). */
158
+ async function seedSchedules(inst) {
159
+ const r = await pmPost('/api/schedules/seed', { ws: inst.ws, set: 'reports' });
160
+ if (r && r.ok) toast({ text: COPY.schedulesSeeded((r.created || []).length), timeout: 4000 }); else toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' });
161
+ await refresh(inst);
162
+ }
163
+
164
+ /* the schedules, split by what they feed; the selected report's schedule carries Run now above the list */
152
165
  function schedulesBlock(inst) {
153
- const feeds = inst.schedules.filter((s) => cadenceOfSkill(s.skill));
154
- const others = inst.schedules.filter((s) => !cadenceOfSkill(s.skill));
166
+ const feeds = inst.schedules.filter((s) => cadenceOfSchedule(s));
167
+ const others = inst.schedules.filter((s) => !cadenceOfSchedule(s));
168
+ const sel = selected(inst);
155
169
  const sec = el('section', { class: 'rp-section', 'data-rp-section': 'schedules', 'aria-label': COPY.schedulesTitle }, el('h3', { class: 'rp-section__title' }, COPY.schedulesTitle));
156
170
  if (!feeds.length) {
157
171
  sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.schedulesNone));
158
- sec.appendChild(button({ label: COPY.schedulesSeed, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'seed', 'data-rp-key': 'seed' }, onClick: async () => {
159
- const r = await pmPost('/api/schedules/seed', { ws: inst.ws, set: 'reports' });
160
- if (r && r.ok) toast({ text: COPY.schedulesSeeded((r.created || []).length), timeout: 4000 }); else toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' });
161
- await refresh(inst);
162
- } }));
172
+ sec.appendChild(button({ label: COPY.schedulesSeed, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'seed', 'data-rp-key': 'seed' }, onClick: () => seedSchedules(inst) }));
163
173
  }
164
174
  const list = el('div', { class: 'rp-list', role: 'list' });
165
175
  for (const s of feeds) {
166
176
  const on = s.enabled !== false;
167
- const cadence = cadenceOfSkill(s.skill);
177
+ const cadence = cadenceOfSchedule(s);
178
+ const isSel = !!sel && sel.cadence === cadence;
179
+ const trail = [button({ label: on ? COPY.pause : COPY.resume, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': on ? 'pause' : 'resume', 'data-rp-key': 'sched:' + s.id }, onClick: async () => {
180
+ const r = await pmPost('/api/schedules/update', { id: s.id, enabled: !on });
181
+ if (!r || !r.ok) { toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' }); }
182
+ await refresh(inst);
183
+ } })];
184
+ if (isSel) trail.push(button({ label: COPY.runNow, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'run', 'data-rp-key': 'run' },
185
+ onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence })) }));
168
186
  list.appendChild(row({
169
187
  title: s.name || cadenceName(cadence),
170
- meta: [COPY.scheduleMeta(whenOf(s), s.lastRun ? COPY.lastRan(s.lastRun) : COPY.neverRan)],
188
+ meta: [cadenceName(cadence), whenOf(s), s.nextRun ? COPY.nextRun(s.nextRun) : '', s.lastRun ? COPY.lastRan(s.lastRun) : COPY.neverRan],
171
189
  signals: on ? [] : [signal({ text: COPY.paused, tone: 'warning' })],
172
- trail: [button({ label: on ? COPY.pause : COPY.resume, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': on ? 'pause' : 'resume', 'data-rp-key': 'sched:' + s.id }, onClick: async () => {
173
- const r = await pmPost('/api/schedules/update', { id: s.id, enabled: !on });
174
- if (!r || !r.ok) { toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' }); }
175
- await refresh(inst);
176
- } })],
177
- attrs: { 'data-schedule': s.id, 'data-cadence': cadence, role: 'listitem' },
190
+ trail,
191
+ attrs: { 'data-schedule': s.id, 'data-cadence': cadence, class: isSel ? 'rp-list__row rp-list__row--on' : 'rp-list__row', role: 'listitem' },
178
192
  }));
179
193
  }
180
194
  if (feeds.length) sec.appendChild(list);
195
+ if (feeds.length && sel && !feeds.some((s) => cadenceOfSchedule(s) === sel.cadence)) {
196
+ sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.oneOff(sel.cadence)));
197
+ sec.appendChild(button({ label: COPY.makeRecurring, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'recurring', 'data-rp-key': 'recurring' }, onClick: () => seedSchedules(inst) }));
198
+ }
181
199
  if (others.length) {
182
200
  sec.appendChild(button({ label: COPY.otherSchedules(others.length), variant: 'ghost', size: 'sm', icon: inst.otherOpen ? 'chevron-down' : 'chevron-right',
183
201
  attrs: { 'data-rp-act': 'other', 'data-rp-key': 'other', 'aria-expanded': inst.otherOpen ? 'true' : 'false' }, onClick: () => { inst.otherOpen = !inst.otherOpen; draw(inst); } }));
@@ -192,7 +210,7 @@ function schedulesBlock(inst) {
192
210
 
193
211
  /* the reports, newest first */
194
212
  function listBlock(inst) {
195
- const sec = el('section', { class: 'rp-section', 'data-rp-section': 'list', 'aria-label': COPY.list }, el('h3', { class: 'rp-section__title' }, COPY.list));
213
+ const sec = el('section', { class: 'rp-section rp-card', 'data-rp-section': 'list', 'aria-label': COPY.list }, el('h3', { class: 'rp-section__title' }, COPY.list));
196
214
  if (!inst.reports.length) { sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.none)); return sec; }
197
215
  const shown = inst.showAll ? inst.reports : inst.reports.slice(0, SHOWN);
198
216
  // the selected report stays on screen even when it is older than the shown ones
@@ -203,7 +221,7 @@ function listBlock(inst) {
203
221
  const key = keyOf(r);
204
222
  list.appendChild(row({
205
223
  title: COPY.rowTitle(r.cadence, r.slot),
206
- meta: [r.issued ? COPY.issued(r.issued.at, r.issued.by) : COPY.draft],
224
+ meta: [cadenceName(r.cadence), r.issued ? COPY.issued(r.issued.at, r.issued.by) : COPY.draft],
207
225
  signals: r.edited ? [signal({ text: COPY.edited, tone: 'warning', title: COPY.editedWhy })] : [],
208
226
  owed: isDraft(r),
209
227
  onOpen: () => selectReport(inst, key),
@@ -222,9 +240,10 @@ function listBlock(inst) {
222
240
  /* the selected report and what can be done to it */
223
241
  function viewerBlock(inst) {
224
242
  const r = selected(inst);
225
- const sec = el('section', { class: 'rp-section rp-viewer', 'data-rp-section': 'viewer', 'aria-label': r ? COPY.reportName(r.cadence, r.slot) : COPY.title });
243
+ const sec = el('section', { class: 'rp-section rp-viewer rp-card', 'data-rp-section': 'viewer', 'aria-label': r ? COPY.reportName(r.cadence, r.slot) : COPY.title });
226
244
  if (!r) return sec;
227
245
  const doc = inst.doc && inst.docKey === inst.sel ? inst.doc : null;
246
+ sec.appendChild(el('p', { class: 'rp-viewer__type' }, COPY.typeSummary(r.cadence, r.issued ? COPY.issuedState : COPY.draft)));
228
247
  const bar = el('div', { class: 'rp-toolbar', role: 'toolbar', 'aria-label': COPY.reportName(r.cadence, r.slot) });
229
248
  bar.appendChild(el('h3', { class: 'rp-section__title rp-toolbar__title' }, COPY.reportName(r.cadence, r.slot)));
230
249
  const acts = el('div', { class: 'rp-toolbar__acts' });
@@ -232,41 +251,74 @@ function viewerBlock(inst) {
232
251
  const present = button({ label: COPY.present, variant: 'outline', size: 'sm', icon: 'columns-2', attrs: { 'data-rp-act': 'present', 'data-rp-key': 'present' }, disabled: !doc,
233
252
  onClick: () => doc && openSlides({ md: doc.markdown, label: COPY.reportName(r.cadence, r.slot), printable: !!r.issued, returnFocus: present }) });
234
253
  acts.appendChild(present);
254
+ acts.appendChild(button({ label: inst.locked ? COPY.unlock : COPY.lock, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'lock', 'data-rp-key': 'lock', 'aria-pressed': inst.locked ? 'true' : 'false' },
255
+ onClick: () => { inst.locked = !inst.locked; draw(inst); } }));
256
+ acts.appendChild(button({ label: COPY.fullScreen, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'full', 'data-rp-key': 'full' },
257
+ onClick: () => { inst.locked = true; draw(inst); goFullScreen(); } }));
235
258
  if (r.issued) {
236
259
  acts.appendChild(button({ label: COPY.print, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'print', 'data-rp-key': 'print' }, disabled: !doc, onClick: () => doc && printReport(doc.markdown) }));
237
260
  acts.appendChild(button({ label: COPY.copyMd, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'copy', 'data-rp-key': 'copy' }, disabled: !doc, onClick: () => doc && copyMarkdown(doc.markdown) }));
238
261
  } else {
239
- acts.appendChild(button({ label: COPY.editNotes, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'notes', 'data-rp-key': 'notes' }, disabled: !doc,
262
+ acts.appendChild(button({ label: COPY.editNotes, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'notes', 'data-rp-key': 'notes' }, disabled: !doc || inst.locked,
240
263
  onClick: () => doc && openNotesSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, markdown: doc.markdown, onSaved: () => refresh(inst) }) }));
241
- acts.appendChild(button({ label: COPY.redraft, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'redraft', 'data-rp-key': 'redraft' },
264
+ acts.appendChild(button({ label: COPY.redraft, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'redraft', 'data-rp-key': 'redraft' }, disabled: inst.locked,
242
265
  onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence, slot: r.slot, redraft: true })) }));
243
266
  const more = iconButton({ icon: 'ellipsis', ariaLabel: COPY.more, expanded: false, size: 'sm', attrs: { 'data-rp-act': 'more', 'data-rp-key': 'more' }, onClick: (e) => {
244
267
  const items = [{ label: COPY.draftNow, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence })) }];
245
268
  for (const c of knownCadences(inst)) if (c !== r.cadence) items.push({ label: COPY.draftNow + ' · ' + c, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: c })) });
246
- items.push({ sep: true }, { label: COPY.print, onSelect: () => doc && printReport(doc.markdown) }, { label: COPY.copyMd, onSelect: () => doc && copyMarkdown(doc.markdown) });
269
+ items.push({ sep: true }, { label: COPY.openTab, onSelect: () => openReportTab(inst, r) }, { label: COPY.print, onSelect: () => doc && printReport(doc.markdown) }, { label: COPY.copyMd, onSelect: () => doc && copyMarkdown(doc.markdown) });
247
270
  menu({ anchor: e.currentTarget, items, ariaLabel: COPY.more });
248
271
  } });
249
272
  acts.appendChild(more);
250
273
  }
251
274
  bar.appendChild(acts);
252
275
  sec.appendChild(bar);
276
+ if (inst.locked) sec.appendChild(el('p', { class: 'rp-viewer__state' }, COPY.locked));
253
277
  if (r.issued) sec.appendChild(el('p', { class: 'rp-viewer__state' }, r.edited ? COPY.editedWhy : COPY.frozen));
254
278
  // the report is read as its slides (Run 6): the PM-OS deck, scaled to the column
255
279
  sec.appendChild(doc ? slideCards(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
256
280
  return sec;
257
281
  }
258
282
 
283
+ /** New report: a session that brainstorms one report and saves its structure as a template (skill report-new). */
284
+ function newReportButton(inst) {
285
+ return button({ label: COPY.newReport, variant: 'outline', attrs: { 'data-rp-act': 'new-report', 'data-rp-key': 'new' }, onClick: () => openNewReport(inst) });
286
+ }
287
+
288
+ async function openNewReport(inst) {
289
+ const { openTab } = await import('./panel-initiatives.js');
290
+ openTab({ id: 'skill:report-new:' + inst.ws, kind: 'skill', title: COPY.newReport, skillId: 'report-new', repo: inst.repo || null, slug: null, role: 'SKILL' });
291
+ const f = window.pmApp && window.pmApp.frame;
292
+ if (f) f.openDock();
293
+ }
294
+
295
+ /** A full report as a dock tab: the tab id is the report key, so opening it again focuses the tab that is already open. */
296
+ async function openReportTab(inst, r) {
297
+ const { openTab } = await import('./panel-initiatives.js');
298
+ openTab({ id: 'report:' + inst.ws + ':' + keyOf(r), kind: 'report', title: COPY.reportName(r.cadence, r.slot), ws: inst.ws, cadence: r.cadence, slot: r.slot, repo: null, role: 'REPORT' });
299
+ const f = window.pmApp && window.pmApp.frame;
300
+ if (f) f.openDock();
301
+ }
302
+
303
+ /** Full screen: the page is read-only while it is shown full, so the lock is set first. A refused request leaves the page as it is. */
304
+ function goFullScreen() {
305
+ try {
306
+ const p = document.documentElement.requestFullscreen();
307
+ if (p && typeof p.catch === 'function') p.catch(() => {});
308
+ } catch (_) { /* the browser has no Full screen here; the lock is already on */ }
309
+ }
310
+
259
311
  /** Rebuild the persistent root from the instance, if what it shows changed; keep the focus on the same control. */
260
312
  function draw(inst) {
261
313
  const root = inst.root;
262
314
  if (!inst.ws) { root.replaceChildren(empty({ text: COPY.noRepo })); inst.last = 'none'; return; }
263
- const fingerprint = JSON.stringify([inst.loaded, inst.error, inst.reports, inst.flags, inst.schedules, inst.sel, inst.doc && inst.doc.sha, inst.showAll, inst.otherOpen]);
315
+ const fingerprint = JSON.stringify([inst.loaded, inst.error, inst.reports, inst.flags, inst.schedules, inst.sel, inst.doc && inst.doc.sha, inst.showAll, inst.otherOpen, inst.locked]);
264
316
  if (fingerprint === inst.last && root.childNodes.length) return;
265
317
  inst.last = fingerprint;
266
318
  const keep = document.activeElement && root.contains(document.activeElement) ? document.activeElement.getAttribute('data-rp-key') : null;
267
319
  const scroll = root.querySelector('.rp-viewer') ? root.scrollTop : 0;
268
320
  if (!inst.loaded) { root.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
269
- const kids = [el('div', { class: 'rp-head' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.error ? null : primaryAction(inst))];
321
+ const kids = [el('div', { class: 'rp-head' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.error ? null : el('div', { class: 'rp-head__acts' }, primaryAction(inst), newReportButton(inst)))];
270
322
  if (inst.error) kids.push(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error));
271
323
  if (inst.flags && inst.flags.length) kids.push(el('p', { class: 'rp-flag' }, signal({ text: COPY.flagTitle, tone: 'warning', title: inst.flags[0].why })));
272
324
  if (!inst.error) kids.push(el('div', { class: 'rp-cols' }, el('div', { class: 'rp-col rp-col--list' }, schedulesBlock(inst), listBlock(inst)), el('div', { class: 'rp-col rp-col--doc' }, viewerBlock(inst))));
@@ -244,7 +244,7 @@ function go(screen) {
244
244
  }
245
245
 
246
246
  const frameOn = () => !!(ctx && ctx.frame);
247
- const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
247
+ const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest' || tab.kind === 'report')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
248
248
 
249
249
  /* The frame on a phone: the PM's own moves through the panels, the palette or Settings, read back as screens. A place
250
250
  * opened from elsewhere (⌘K, Settings › Show its initiatives) is a tab switch; Settings is pushed on top; an open
@@ -42,6 +42,7 @@ const liveId = () => (V ? V.listeningId() : null);
42
42
  const titleOf = () => { if (!active) return ''; const r = noteById(active.id); return (V && V.titleOf(active.id)) || (r && r.title) || active.title || 'Untitled note'; };
43
43
 
44
44
  export const activeId = () => (active ? active.id : null);
45
+ export const isRecording = () => !!(active && V && liveId() === active.id);
45
46
 
46
47
  export function mountDock() {
47
48
  if (host || FIXTURE || typeof document === 'undefined') return;
@@ -47,7 +47,7 @@ export const VIEWS = {
47
47
  // meeting-notes Run 1: typed notes, dictation, meeting transcripts and summaries; the PM's own, the same in every repository
48
48
  notes: { label: 'notes', mode: null, scoped: false, palette: true, hint: 'type, dictate or record a meeting' },
49
49
  // meeting-notes Run 11: notes menu with two-column view grouped by initiative (R47)
50
- notesmenu: { label: 'notes menu', mode: null, scoped: false, palette: true, hint: 'view and organize all notes by initiative' },
50
+ notesmenu: { label: 'notes menu', mode: null, scoped: false, hint: 'view and organize all notes by initiative' },
51
51
  };
52
52
 
53
53
  /** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
@@ -63,6 +63,11 @@ import('./panel-breakdown.js')
63
63
  .then((m) => { breakdownMod = m; if ((state.tabs || []).some((t) => t.kind === 'breakdown')) setState({}); })
64
64
  .catch(() => { breakdownMod = null; });
65
65
 
66
+ let reportTabMod = null;
67
+ import('./report-tab.js')
68
+ .then((m) => { reportTabMod = m; if ((state.tabs || []).some((t) => t.kind === 'report')) setState({}); })
69
+ .catch(() => { reportTabMod = null; });
70
+
66
71
  /** `{{ expr }}` — one interpolated value in its own span, the way the prototype's runtime emits it. */
67
72
  /* A dock control's face: a lucide icon from the vendored sprite, then its word (C6: no Unicode glyph controls). */
68
73
  const iconLabel = (name, text) => (text ? [icon(name), el('span', null, text)] : [icon(name)]);
@@ -1043,6 +1048,8 @@ function paneFor(tab) {
1043
1048
  if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
1044
1049
  } else if (tab.kind === 'web') {
1045
1050
  mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
1051
+ } else if (tab.kind === 'report') {
1052
+ if (reportTabMod && typeof reportTabMod.mountReportTab === 'function') reportTabMod.mountReportTab(host, tab);
1046
1053
  } else if (tab.kind === 'breakdown') {
1047
1054
  // pm-os-absorb Run 1 — an initiative's items (Tree, Board, item sheet); an empty pane until the module lands
1048
1055
  if (breakdownMod && typeof breakdownMod.mountBreakdown === 'function') breakdownMod.mountBreakdown(host, tab);
@@ -1205,7 +1212,7 @@ function cockpitTabNode(t, act, splitTab, vertical) {
1205
1212
  const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
1206
1213
  const meta = t.kind === 'term'
1207
1214
  ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
1208
- : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
1215
+ : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
1209
1216
  const br = tabBranch(t);
1210
1217
  return el('div', {
1211
1218
  'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
@@ -1242,7 +1249,7 @@ function cockpitTabRow(t, front) {
1242
1249
  const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
1243
1250
  const meta = t.kind === 'term'
1244
1251
  ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
1245
- : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
1252
+ : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
1246
1253
  const br = tabBranch(t);
1247
1254
  return el('div', {
1248
1255
  'data-tab': t.id, 'data-tab-active': front ? '1' : '0', 'data-tab-row': '',
@@ -1319,7 +1326,7 @@ function tabNode(t, act, splitTab) {
1319
1326
  * fixture render keeps the design's literal — the comparator reads this text node. */
1320
1327
  const meta = t.kind === 'term'
1321
1328
  ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
1322
- : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
1329
+ : (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
1323
1330
 
1324
1331
  return el('div', {
1325
1332
  style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
@@ -0,0 +1,33 @@
1
+ /* pm/report-tab.js — a full report, opened as a tab in the dock (reports-redesign follow-up).
2
+ *
3
+ * The dock tab reads the same draft the Reports viewer reads, and draws it whole: the report as a document,
4
+ * not its slide cards. Print and Copy are the viewer's own actions. The tab writes nothing; a report is read here
5
+ * and changed only from the Reports page, where the lock applies.
6
+ *
7
+ * mountReportTab(host, tab) tab: { id, ws, cadence, slot, title }
8
+ */
9
+ import { el } from './dom.js';
10
+ import { button, ensureComponentsCss } from './primitives.js';
11
+ import { pmGet } from './http.js';
12
+ import { COPY } from './reports-copy.js';
13
+ import { renderReport, printReport, copyMarkdown } from './report-view.js';
14
+
15
+ export function mountReportTab(host, tab) {
16
+ const root = host.firstElementChild;
17
+ if (root && root.getAttribute('data-report-tab') === tab.id) return;
18
+ ensureComponentsCss();
19
+ const page = el('div', { class: 'rp-page rp-tab', 'data-report-tab': tab.id, style: 'flex:1;min-height:0;overflow:auto;padding:12px 16px' });
20
+ host.replaceChildren(page);
21
+ page.appendChild(el('p', { class: 'rp-section__empty' }, COPY.loading));
22
+ const q = '/api/report/draft?ws=' + encodeURIComponent(tab.ws) + '&cadence=' + encodeURIComponent(tab.cadence) + '&slot=' + encodeURIComponent(tab.slot);
23
+ pmGet(q).then((out) => {
24
+ if (!page.isConnected) return;
25
+ if (!out || !out.ok || !out.exists) { page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError)); return; }
26
+ const acts = el('div', { class: 'rp-toolbar__acts' },
27
+ button({ label: COPY.print, variant: 'outline', size: 'sm', onClick: () => printReport(out.markdown) }),
28
+ button({ label: COPY.copyMd, variant: 'outline', size: 'sm', onClick: () => copyMarkdown(out.markdown) }));
29
+ page.replaceChildren(el('div', { class: 'rp-toolbar' }, el('h3', { class: 'rp-section__title rp-toolbar__title' }, tab.title || ''), acts), renderReport(out.markdown));
30
+ }).catch(() => {
31
+ if (page.isConnected) page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError));
32
+ });
33
+ }
@@ -48,6 +48,8 @@ export const COPY = {
48
48
  reportName: (cadence, slot) => cadenceName(cadence) + ' report, ' + shortDate(slot),
49
49
  rowTitle: (cadence, slot) => cadenceName(cadence) + ' · ' + shortDate(slot),
50
50
  draft: 'Draft',
51
+ issuedState: 'Issued',
52
+ typeSummary: (cadence, state) => 'Slides · ' + cadenceName(cadence) + ' · ' + state,
51
53
  issued: (at, by) => 'Issued ' + (dayOf(at) || '') + (by ? ' by ' + by : ''),
52
54
  edited: 'Edited after issue',
53
55
  editedWhy: 'This file changed after it was issued. The issued bytes are in the issues ledger.',
@@ -94,6 +96,10 @@ export const COPY = {
94
96
  schedulesFailed: 'The schedules were not changed.',
95
97
  pause: 'Pause',
96
98
  resume: 'Resume',
99
+ runNow: 'Run now',
100
+ nextRun: (iso) => (dayOf(iso) ? 'next ' + dayOf(iso) : ''),
101
+ oneOff: (cadence) => 'No schedule drafts this ' + cadence + ' report. Make it recurring to draft it on a schedule.',
102
+ makeRecurring: 'Make recurring',
97
103
  paused: 'Paused',
98
104
  pausedMsg: (name) => name + ' is paused.',
99
105
  resumed: (name) => name + ' runs again.',
@@ -105,7 +111,13 @@ export const COPY = {
105
111
 
106
112
  flagTitle: 'The issues ledger has a line that is not read',
107
113
 
114
+ openTab: 'Open as tab',
115
+ newReport: 'New report',
108
116
  present: 'Present',
117
+ lock: 'Lock',
118
+ unlock: 'Unlock',
119
+ locked: 'Locked. Edit notes, Redraft and Issue are off until you unlock.',
120
+ fullScreen: 'Full screen',
109
121
  };
110
122
 
111
123
  export const GLOSSARY = {
@@ -21,6 +21,7 @@
21
21
  import { el, setChildren } from './dom.js';
22
22
  import { button, needsDot, needsCount } from './primitives.js';
23
23
  import { COPY } from './frame-copy.js';
24
+ import { isRecording } from './note-dock.js';
24
25
 
25
26
  /** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
26
27
  * PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"). People joined after Docs when the
@@ -39,13 +40,19 @@ export function unsyncedOf(st) {
39
40
  /** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
40
41
  const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
41
42
 
42
- export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
43
+ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {}) {
43
44
  const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
44
45
 
45
46
  let repoBtn = null;
46
47
  let repoLabel = null;
47
48
  const repoSlot = el('div', { class: 'fr-bar__repo' });
48
49
 
50
+ const recordingIndicator = el('span', {
51
+ class: 'fr-recording-dot', 'data-fr-recording': '',
52
+ title: 'Notes are being recorded',
53
+ 'aria-hidden': 'true',
54
+ });
55
+
49
56
  const count = needsCount('0', { 'data-today-badge': '' });
50
57
  const dot = needsDot();
51
58
  const links = {};
@@ -80,6 +87,12 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
80
87
  });
81
88
  search.appendChild(el('kbd', { class: 'fr-kbd' }, COPY.searchKey));
82
89
 
90
+ const notes = button({
91
+ label: 'Notes', variant: 'outline', icon: 'notebook-pen',
92
+ attrs: { class: 'fr-notes', 'data-fr-view': 'notes', title: 'Open notes menu (all notes by initiative)' },
93
+ onClick: () => openView && openView('notesmenu'),
94
+ });
95
+
83
96
  let syncBtn = null;
84
97
  let syncLabel = null;
85
98
 
@@ -116,7 +129,8 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
116
129
  syncLabel = want;
117
130
  syncBtn = want ? syncButton(want, !!model.syncing) : null;
118
131
  }
119
- setChildren(node, [repoSlot, nav, search, syncBtn, links.settings]);
132
+ recordingIndicator.classList.toggle('fr-recording-dot--active', isRecording());
133
+ setChildren(node, [repoSlot, recordingIndicator, nav, search, notes, syncBtn, links.settings]);
120
134
  }
121
135
 
122
136
  update({});
@@ -181,10 +181,10 @@ export async function renderBoard(inner, ctx) {
181
181
  })()
182
182
  : el('div', { class: 'nt-menu-right' }, empty({ text: 'Select a note to view details' }));
183
183
 
184
- setChildren(root, leftPanel, rightPanel);
184
+ setChildren(root, [leftPanel, rightPanel]);
185
185
  }
186
186
 
187
- render();
187
+ await render();
188
188
  ctx.setState = (state) => { render(); };
189
189
  return root;
190
190
  }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: report-new
3
+ label: "New report (brainstorm, then save as a template)"
4
+ description: "Brainstorm one new recurring or ad hoc report with the PM: its purpose, audience, period, sections and format. Saves only the chosen structure as a template and registers its schedule. Never saves the sample artifacts."
5
+ run: agent
6
+ agent: claude
7
+ output:
8
+ - Template
9
+ - Schedule
10
+ - First draft
11
+ ---
12
+
13
+ /report-new — help the PM design one new report, then save its structure as a template. You brainstorm, then write one definition file and register one schedule. The sample text, the slides and any other draft from the session are not saved.
14
+
15
+ Rules that always hold (D71):
16
+ 1. DRAFT ONLY. A report is issued by the PM from the Reports page. Never send, post, share, email, publish or upload a report.
17
+ 2. READ-ONLY on every source. Never change mail, chat, Jira, Confluence, calendar or any other source.
18
+ 3. SAVE ONLY THE STRUCTURE. The template is the chosen structure: id, cadence, audience, sections, purpose and tone. Never save a sample, a slide or a draft as a template.
19
+
20
+ Procedure:
21
+ 1. Ask, one question at a time: what the report is for; who reads it; the sources it should draw on; the sections it needs; and the format (slides, a document, or a message). Offer a sensible default for each and keep going unless the PM corrects it.
22
+ 2. Ask for the period. Accept a cadence word (daily, weekly, bi-weekly, monthly), a single date, a range, or a phrase such as "every Tuesday", "every 2 weeks on Monday" or "the first Monday of the month". Map the phrase to the nearest schedule cadence and say what you chose. For a one-off report, ask for its date and set no recurring schedule.
23
+ 3. Propose a template id: lowercase, starting with a letter, up to 40 characters from a-z, 0-9 and hyphens. Confirm it with the PM.
24
+ 4. Write `<state>/reports/definitions/<id>.md`, where <state> is `pmos/state/` if it exists, otherwise `planning/`. Use this frontmatter, then the prose: `type: Reference`, `title`, `timestamp` (today), `cadence: <id>`, `audience`, and `sections:` as a list. Put the purpose, sources, length and tone in the body.
25
+ 5. If the period is recurring, register the schedule: `pmos schedule add --skill report-template --cadence <daily|weekly|bi-weekly|monthly> --args <id> --name "<Name>"`. Use the cadence from step 2. The schedule drafts the report; it never issues one.
26
+ 6. Run the first draft now: `pmos report draft <id>`. Then say where it is, and that the PM issues it from Reports.
27
+ 7. Report in one short message: the template path, the schedule (or the one-off date), and the first draft path.
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: report-template
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."
5
+ run: agent
6
+ agent: claude
7
+ output:
8
+ - Headline
9
+ - Sections from the template
10
+ - Needs your call
11
+ - Sources reached and not reached
12
+ ---
13
+
14
+ /report-template — prepare a saved report for THIS repository as a draft. The template id is the schedule's args, for example `quarterly-ops`. A script with no model writes the draft and every number in it; you write the words between the note markers.
15
+
16
+ Rules that always hold (D71):
17
+ 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
+ 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.
19
+ 3. SOURCED. Every claim carries a pointer the reader can open: an initiative id, a file path in this repository, or an acceptance. A claim you cannot point to is left out or marked "unverified".
20
+ 4. NOTHING INVENTED. The script lists the sources it reached and did not reach under "Sources reached and not reached": that section is not yours to edit.
21
+ 5. THE TEMPLATE'S RULES. The script shapes the file from the template: its sections, their order and its blocks. The template's purpose, audience, length and tone govern your words. Do not substitute your own format.
22
+ 6. THE SCRIPT WRITES EVERY NUMBER. In a note you type no count, ratio, percentage or date.
23
+ 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.
24
+
25
+ Procedure:
26
+ 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.
27
+ 2. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
28
+ 3. Read the template: `<state>/reports/definitions/<id>.md`. If it does not exist, stop and say the template is missing. Do not invent one.
29
+ 4. Run `pmos report draft <id>`. It writes `<state>/reports/<id>/YYYY-MM-DD.md`, with one empty note marker for each block.
30
+ - If it refuses because a draft for the slot exists, do not overwrite it. Write your notes into the file that exists.
31
+ - If it refuses because the slot is issued, stop. An issued report is frozen. Say so in one line.
32
+ - If it refuses for any other reason, stop and quote the refusal. Do not write the file yourself.
33
+ 5. Read the draft, then the sources it names. Write one or two sentences for each `<!-- pmos:note <block-id> -->`. Start with "Warning:" when a block needs attention. Leave any note that already has text.
34
+ 6. Run `pmos report draft <id> --redraft` so the script rewrites the file and keeps every note. Read the file once more.