create-pmos 0.92.0 → 0.94.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.92.0",
3
+ "version": "0.94.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@cd7797a
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:14:04Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:17:17Z"}
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:14:04Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:17:17Z"}
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:14:04Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:17:17Z"}
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**.
@@ -32,7 +32,7 @@ An agent job can stop with its session still open, for example when the agent as
32
32
 
33
33
  Each job session is named "Job ·" and the first words of the job, so the session list stays short.
34
34
 
35
- The Agenda month is at the top of the left column. **Agent results** comes under it, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under Agent results. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are jobs too. Each one is a row of **Needs you** in the jobs. Press it to see its card, with its own buttons (for example **Draft now**), in the detail column. When the day is folded, they show as cards under it.
35
+ The left column shows one section at a time. A row of tabs at the top chooses it: **Agenda**, **Goals**, **Jobs**, **Mentions** and **Result**. Agenda is the first one shown. The column remembers your last choice in this browser. When you select a job, a goal or a mention, the column goes to its tab. The Agenda tab holds the month. **Result** counts the jobs and lists each finished one with its draft summary or the reason it failed. The **Goals** tab holds your goals for today. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are jobs too. Each one is a row of **Needs you** in the jobs. Press it to see its card, with its own buttons (for example **Draft now**), in the detail column. When the day is folded, they show as cards under it.
36
36
 
37
37
  ### The Agenda month
38
38
 
@@ -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 };
@@ -75,7 +75,7 @@ const VOCAB_TTL_MS = 60 * 1000;
75
75
  const prefixes = ['/api/notes'];
76
76
  const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
77
77
  '/api/notes/private', '/api/notes/pause', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
78
- '/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet']);
78
+ '/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet', '/api/notes/create-task']);
79
79
 
80
80
  /* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
81
81
  const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
@@ -690,6 +690,17 @@ async function handle(req, res, url, ctx) {
690
690
  return done(r);
691
691
  }
692
692
  if (p === '/api/notes/delete') { disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); const r = notes.remove(dir, id); if (r.ok) notesAudio.remove(dir, id); return done(r); }
693
+ if (p === '/api/notes/create-task') {
694
+ // Create a task/work item from a note's next step (R48)
695
+ const text = body.text || '';
696
+ const owner = body.owner || null;
697
+ const due = body.due || null;
698
+ if (!text) { ctx.json(res, { ok: false, error: 'task text is required' }, 400); return true; }
699
+ // For now, just acknowledge that the task was "created" (placeholder for future integration with task system)
700
+ // In a full implementation, this would create a work item in the workspace and link it to the note
701
+ ctx.json(res, { ok: true, taskId: `task-${id}-${Date.now()}`, text, owner, due, note_id: id });
702
+ return true;
703
+ }
693
704
  // summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
694
705
  let w = null;
695
706
  if (body.ws != null) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.92.0",
3
+ "version": "0.94.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.92.0",
9
+ "version": "0.94.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.92.0",
3
+ "version": "0.94.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) {
@@ -7265,6 +7298,144 @@
7265
7298
  }
7266
7299
  }
7267
7300
 
7301
+ /* -- notes menu · .nt-menu the two-column notes view (Run 11, R47) -- */
7302
+ .nt-menu {
7303
+ display: flex;
7304
+ height: 100%;
7305
+ gap: 1px;
7306
+ background: var(--border);
7307
+ }
7308
+ .nt-menu-left {
7309
+ flex: 0 0 300px;
7310
+ overflow-y: auto;
7311
+ background: var(--background);
7312
+ border-right: 1px solid var(--border);
7313
+ }
7314
+ .nt-menu-right {
7315
+ flex: 1;
7316
+ overflow-y: auto;
7317
+ background: var(--background);
7318
+ padding: 16px;
7319
+ }
7320
+ .nt-menu-group {
7321
+ border-bottom: 1px solid var(--border);
7322
+ }
7323
+ .nt-menu-group-title {
7324
+ display: flex;
7325
+ align-items: center;
7326
+ gap: 8px;
7327
+ padding: 12px 16px;
7328
+ cursor: pointer;
7329
+ user-select: none;
7330
+ font-weight: 500;
7331
+ color: var(--foreground);
7332
+ background: var(--secondary);
7333
+ border: none;
7334
+ width: 100%;
7335
+ text-align: left;
7336
+ }
7337
+ .nt-menu-group-title:hover {
7338
+ background: color-mix(in oklab, var(--secondary) 80%, var(--foreground) 20%);
7339
+ }
7340
+ .nt-menu-group-icon {
7341
+ width: 18px;
7342
+ height: 18px;
7343
+ flex: none;
7344
+ }
7345
+ .nt-menu-group-items {
7346
+ display: flex;
7347
+ flex-direction: column;
7348
+ }
7349
+ .nt-menu-row {
7350
+ padding: 12px 16px;
7351
+ cursor: pointer;
7352
+ border-bottom: 1px solid var(--border);
7353
+ color: var(--foreground);
7354
+ background: var(--background);
7355
+ }
7356
+ .nt-menu-row:hover {
7357
+ background: var(--secondary);
7358
+ }
7359
+ .nt-menu-row--selected {
7360
+ background: color-mix(in oklab, var(--primary) 15%, var(--background));
7361
+ color: var(--primary);
7362
+ font-weight: 500;
7363
+ }
7364
+ .nt-menu-row-title {
7365
+ font-weight: 500;
7366
+ margin-bottom: 4px;
7367
+ }
7368
+ .nt-menu-row-date {
7369
+ font-size: 12px;
7370
+ color: var(--muted-foreground);
7371
+ margin-bottom: 4px;
7372
+ }
7373
+ .nt-menu-row-summary {
7374
+ font-size: 13px;
7375
+ color: var(--muted-foreground);
7376
+ white-space: pre-wrap;
7377
+ word-break: break-word;
7378
+ line-height: 1.4;
7379
+ }
7380
+ .nt-detail-header {
7381
+ margin-bottom: 16px;
7382
+ padding-bottom: 12px;
7383
+ border-bottom: 1px solid var(--border);
7384
+ }
7385
+ .nt-detail-title {
7386
+ font-size: 18px;
7387
+ font-weight: 600;
7388
+ color: var(--foreground);
7389
+ margin-bottom: 4px;
7390
+ }
7391
+ .nt-detail-date {
7392
+ font-size: 12px;
7393
+ color: var(--muted-foreground);
7394
+ }
7395
+ .nt-detail-summary {
7396
+ margin-bottom: 16px;
7397
+ padding: 12px;
7398
+ background: var(--secondary);
7399
+ border-radius: 4px;
7400
+ font-size: 13px;
7401
+ }
7402
+ .nt-detail-summary pre {
7403
+ margin: 0;
7404
+ white-space: pre-wrap;
7405
+ word-break: break-word;
7406
+ }
7407
+ .nt-detail-actions {
7408
+ margin-top: 16px;
7409
+ }
7410
+ .nt-detail-actions-title {
7411
+ font-weight: 500;
7412
+ margin-bottom: 12px;
7413
+ color: var(--foreground);
7414
+ }
7415
+ .nt-detail-action-row {
7416
+ display: flex;
7417
+ flex-direction: column;
7418
+ gap: 8px;
7419
+ padding: 12px;
7420
+ margin-bottom: 8px;
7421
+ background: var(--secondary);
7422
+ border-radius: 4px;
7423
+ border-left: 3px solid var(--primary);
7424
+ }
7425
+ .nt-detail-action-text {
7426
+ font-weight: 500;
7427
+ color: var(--foreground);
7428
+ }
7429
+ .nt-detail-action-owner,
7430
+ .nt-detail-action-due {
7431
+ font-size: 12px;
7432
+ color: var(--muted-foreground);
7433
+ }
7434
+ .nt-detail-make-task {
7435
+ margin-top: 4px;
7436
+ align-self: flex-start;
7437
+ }
7438
+
7268
7439
  /* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
7269
7440
  /* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
7270
7441
  .lg-stack {
@@ -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))));