create-pmos 0.133.0 → 0.135.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 (212) hide show
  1. package/package.json +1 -1
  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/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +35 -2
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -2,7 +2,7 @@
2
2
  /* routes-notes.js — the Notes view's routes (meeting-notes, Runs 1 to 4; lib/route-modules.js).
3
3
  *
4
4
  * GET /api/notes → { ok, notes: [row] } newest first; a row never carries the body
5
- * GET /api/notes/one?id=… → { ok, note, bullets_state, audio, recording } imports an agent summary and the agent's
5
+ * GET /api/notes/one?id=… → { ok, note, bullets_state, audio, recording, screen: { frames: [{ t, file, w, h, bytes, why?, text? }] } } imports an agent summary and the agent's
6
6
  * minute bullets written since the last read (Run 8). note.bullets:
7
7
  * [{ id, t, minute, text, owner, cat, cat_by, by: 'agent', open?, check?, dropped? }];
8
8
  * an open bullet has cat null. bullets_state: { agent: 'on'|'off'|'failed',
@@ -15,7 +15,7 @@
15
15
  * POST /api/notes/save { id, title?, body? } → { ok, updated_at, versions, added } each new line of the notes is also
16
16
  * a transcript line, source 'typed' (`added`, Run 8)
17
17
  * POST /api/notes/dictate { text } → { ok, ops } one dictated phrase as insert operations (lib/notes.js dictation)
18
- * POST /api/notes/record { id, ws? } → { ok, recorded_from, clock_from, meeting_ms } `ws` is the repository the note's lines are checked against;
18
+ * POST /api/notes/record { id, ws? } → { ok, recorded_from, clock_from, meeting_ms, capture: { token, base_ms } } `ws` is the repository the note's lines are checked against;
19
19
  * clock_from is the first Listen moved on by the time listening was stopped
20
20
  * POST /api/notes/pause { id } → { ok, meeting_ms, state, paused_at, resume_until, ended_at, ended_by, sessions }
21
21
  * a resumable pause (Run 13, R51): the clock stops, the meeting stays open for one hour
@@ -28,17 +28,25 @@
28
28
  * ended. A row and a note carry state ('idle'|'recording'|'paused'|'ended'), paused_at, resume_until, ended_at, ended_by, sessions
29
29
  * and meeting.context (the snapshot of the meeting, lib/notes.js cleanContext; R59).
30
30
  * POST /api/notes/transcript { id, text, source? } → { ok, added: [{ at, t, text, source, cat, index }] }
31
- * POST /api/notes/audio { id, source, pcm, final? } → { ok, cut } 16 kHz mono 16-bit samples, base64; local Whisper (D91)
31
+ * POST /api/notes/audio { id, source, pcm, final?, cap?, seq?, off? } → { ok, cut, dup? } 16 kHz mono 16-bit samples, base64; local Whisper (D91);
32
+ * cap/seq/off name the chunk's capture, sequence number and sample offset (lib/notes-capture.js)
32
33
  * POST /api/notes/label { id, index, cat } → { ok, segment } the PM's own label for a line
33
34
  * POST /api/notes/speaker { id, source, name } → { ok, speakers }
34
35
  * POST /api/notes/line { id, index, cat?, summary?, tags?, people?, text? } → { ok, segment, summary } the PM's correction (Run 3)
35
36
  * GET /api/notes/recording?id=… → audio/wav, with byte ranges the note's kept audio (Run 3, D92)
36
37
  * POST /api/notes/recording-delete { id } → { ok } deletes the note's audio and keeps the rest
38
+ * POST /api/notes/screen { id, cap, t, img, w, h, why? } → { ok, frame } one screenshot of the shared screen (notes-screen-capture): img is the
39
+ * picture as base64 (JPEG, PNG or WebP, 2 MB at most), t its meeting time in ms, cap the capture's token.
40
+ * Refused: a private note (409), a note not listening (409), "Capture the shared screen" switched off (409),
41
+ * a token that is not the live capture's (409), a bad t or picture (400), a full note (409)
42
+ * GET /api/notes/screen?id=…&t=… → image/jpeg|png|webp the shot kept at meeting time t; 404 for a private note or no such shot
43
+ * POST /api/notes/screen-delete { id } → { ok, deleted } deletes the note's screenshots and keeps the rest
37
44
  * POST /api/notes/clean-audio { id } → { ok, started } makes the cleaned copy of the replay now (Run 3 of notes-audio-quality; needs ffmpeg)
38
45
  * GET /api/notes/engine?ws=… → { ok, whisper, jev } is Whisper set up; which Jev gates hold (booleans only)
39
46
  * POST /api/notes/switch { ws, jev?, check? } → { ok, jev } the two D91 switches, default off
40
- * { language?, model?, accuracy?, keep_audio? } → { ok, whisper } the language, the model, the decoding
41
- * ('fast' | 'accurate') and keeping audio, for this machine
47
+ * { language?, model?, accuracy?, keep_audio?, screen? } → { ok, whisper } the language, the model, the decoding
48
+ * ('fast' | 'accurate'), keeping audio and capturing the shared screen, for this machine.
49
+ * whisper.screen is that switch; whisper.ocr says whether tesseract was found
42
50
  * POST /api/notes/whisper-setup { ws? } → { ok, started, session, command } a terminal with the setup typed in, not run
43
51
  * POST /api/notes/private { id, private } → { ok, private }
44
52
  * POST /api/notes/summarize { id, ws?, prefer? } → { ok, summary, initiative, source, agent? } the local summary; with ws and
@@ -74,8 +82,10 @@
74
82
  const guard = require('./pm-guard.js');
75
83
  const notes = require('./notes.js');
76
84
  const whisper = require('./notes-whisper.js');
85
+ const notesCapture = require('./notes-capture.js');
77
86
  const notesRepo = require('./notes-repo.js');
78
87
  const notesAudio = require('./notes-audio.js');
88
+ const notesScreen = require('./notes-screen.js');
79
89
  const notesEcho = require('./notes-echo.js');
80
90
  const notesPolish = require('./notes-polish.js');
81
91
  const notesFinal = require('./notes-final.js');
@@ -93,7 +103,7 @@ const VOCAB_TTL_MS = 60 * 1000;
93
103
  const prefixes = ['/api/notes'];
94
104
  const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
95
105
  '/api/notes/private', '/api/notes/pause', '/api/notes/stop', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
96
- '/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/clean-audio', '/api/notes/link', '/api/notes/bullet', '/api/notes/highlight', '/api/notes/highlight-remove', '/api/notes/final-edit', '/api/notes/wispr-fetch']);
106
+ '/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/screen', '/api/notes/screen-delete', '/api/notes/clean-audio', '/api/notes/link', '/api/notes/bullet', '/api/notes/highlight', '/api/notes/highlight-remove', '/api/notes/final-edit', '/api/notes/wispr-fetch']);
97
107
 
98
108
  /* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
99
109
  const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
@@ -276,8 +286,12 @@ function handToAgent(ctx, id, note, b) {
276
286
  const h = notes.minuteHandoff(dir, { id, minute: b.minute, lines: b.lines, context: b.context, final: b.final, terms: repoTermsFor(ctx, note) });
277
287
  if (!h.ok) return false;
278
288
  if (m.agent === 'on' && m.session && typeof ctx.sendToSession === 'function' && ctx.sendToSession(m.session, notes.bulletsFollowUp(h))) return true;
289
+ // workspace-headless-jobs R6: a headless bullets run takes no follow-up. While one is still working, this minute waits
290
+ // for the next run, which writes every earlier minute that has no bullets yet. The last minute never waits.
291
+ if (m.agent === 'on' && m.session && !b.final && typeof ctx.headlessBusy === 'function' && ctx.headlessBusy(m.session)) return true;
279
292
  const w = note.ws ? ctx.getWs(note.ws) : null;
280
- const s = ctx.spawnSession({ name: 'notes: bullets', agent: cmd, ws: w ? w.id : undefined, origin: 'notes-bullets:' + id, initialInput: notes.bulletsPrompt(h) + '\r' });
293
+ const s = ctx.spawnSession({ name: 'notes: bullets', agent: cmd, ws: w ? w.id : undefined, origin: 'notes-bullets:' + id, initialInput: notes.bulletsPrompt(h) + '\r',
294
+ addDirs: [h.folder] });
281
295
  const started = !!s && s.state !== 'exited';
282
296
  if (!started) {
283
297
  try { fs.unlinkSync(h.md); } catch { /* gone */ }
@@ -381,6 +395,7 @@ function liveRun(ctx, args) {
381
395
  }
382
396
  let transcriber = null; let transcriberDir = null;
383
397
  const audioErrors = new Map(); // note id → the last thing that went wrong, for the page
398
+ const fedHere = new Set(); // `${id}|${source}|${token}` this process has had chunks of: the first one after a restart picks up what was lost
384
399
  function transcriberFor(ctx) {
385
400
  if (transcriber && transcriberDir === ctx.runtimeDir) return transcriber;
386
401
  transcriberDir = ctx.runtimeDir;
@@ -389,11 +404,27 @@ function transcriberFor(ctx) {
389
404
  detect: () => liveDetect(ctx),
390
405
  run: (args) => liveRun(ctx, args),
391
406
  vocab: ({ id }) => vocabFor(ctx, id),
407
+ // the note's meeting lines of the last minute, for the live echo check
408
+ recent: (id) => {
409
+ const r = notes.read(ctx.runtimeDir, id);
410
+ if (!r.ok) return [];
411
+ const tr = r.note.transcript || []; const last = tr.length ? tr[tr.length - 1].t || 0 : 0;
412
+ return tr.filter((x) => x && x.source === 'meeting' && (x.t || 0) >= last - 60000).map((x) => ({ source: 'meeting', t: x.t || 0, end: Number.isFinite(x.t_end) ? x.t_end : (x.t || 0), text: x.text }));
413
+ },
414
+ // the last line of a source, for the repetition guard after a restart
415
+ lastLine: (id, source) => {
416
+ const r = notes.read(ctx.runtimeDir, id);
417
+ const tr = r.ok ? r.note.transcript || [] : [];
418
+ for (let k = tr.length - 1; k >= 0; k--) if (tr[k] && tr[k].source === source) return { text: tr[k].text, t: tr[k].t || 0 };
419
+ return null;
420
+ },
392
421
  onText: async (c) => {
393
- const r = notes.appendTranscript(ctx.runtimeDir, { id: c.id, text: c.text, source: c.source, t: c.t, ms: c.ms });
422
+ const r = notes.appendTranscript(ctx.runtimeDir, { id: c.id, text: c.text, source: c.source, t: c.t, ms: c.ms, heard_as: c.heard_as });
394
423
  if (r.ok) { audioErrors.delete(c.id); arm(ctx, c.id); }
395
424
  },
396
425
  onError: (c) => { audioErrors.set(c.id, { at: new Date().toISOString(), error: c.error }); },
426
+ // how far the live transcription has cut each source, kept with the capture so a restart picks up what it had buffered
427
+ onCut: (c) => { try { notesCapture.setCut(ctx.runtimeDir, c.id, { source: c.source, sample: Math.round(c.until * whisper.RATE / 1000) }); } catch { /* best effort */ } },
397
428
  });
398
429
  return transcriber;
399
430
  }
@@ -403,7 +434,7 @@ function engineInfo(ctx, w) {
403
434
  const out = { ready: live.ready, live: live.ready ? live.engine : null, missing: d.missing, setup: d.setup, model_dir: d.model_dir, model_size: d.model_size,
404
435
  language: whisper.getLanguage(ctx.runtimeDir), languages: whisper.LANGUAGES.map(([value, label]) => ({ value, label })),
405
436
  model: whisper.getModel(ctx.runtimeDir), models: whisper.MODELS.map((m) => ({ value: m.id, label: m.label })),
406
- accuracy: whisper.getAccuracy(ctx.runtimeDir), accuracies: whisper.ACCURACIES.map((a) => ({ value: a.id, label: a.label })), keep_audio: notesAudio.getKeep(ctx.runtimeDir), clean_audio: notesAudio.getClean(ctx.runtimeDir), ffmpeg: !!notesPolish.detect(),
437
+ accuracy: whisper.getAccuracy(ctx.runtimeDir), accuracies: whisper.ACCURACIES.map((a) => ({ value: a.id, label: a.label })), keep_audio: notesAudio.getKeep(ctx.runtimeDir), clean_audio: notesAudio.getClean(ctx.runtimeDir), ffmpeg: !!notesPolish.detect(), screen: notesScreen.getOn(ctx.runtimeDir), ocr: !!notesScreen.ocrDetect(),
407
438
  final: finalInfo(ctx), wispr: wisprOn(ctx) };
408
439
  if (!w) return { whisper: out, jev: null };
409
440
  const repo = tsSettings.getRepo(ctx.runtimeDir, w.root);
@@ -596,12 +627,21 @@ async function handle(req, res, url, ctx) {
596
627
  if (r.ok && mergeWispr(ctx, nid)) r = notes.read(dir, nid, { resumeMs: resumeMsOf(ctx) });
597
628
  if (r.ok) {
598
629
  r.audio = { pending: transcriber ? transcriber.pending() : 0, error: audioErrors.get(nid) || null }; r.recording = notesAudio.info(dir, nid); if (r.recording) r.recording = { ...r.recording, cleaned: notesAudio.hasClean(dir, nid) };
630
+ r.screen = { frames: r.note.private ? [] : notesScreen.list(dir, nid) };
599
631
  if (!Array.isArray(r.note.bullets)) r.note.bullets = [];
600
632
  r.bullets_state = bulletsState(ctx, r.note);
601
633
  }
602
634
  send(ctx, res, r);
603
635
  return true;
604
636
  }
637
+ if (p === '/api/notes/screen' && (req.method === 'GET' || req.method === 'HEAD')) {
638
+ const nid = url.searchParams.get('id');
639
+ const r = notes.read(dir, nid);
640
+ if (!r.ok) { send(ctx, res, r); return true; }
641
+ if (r.note.private) { ctx.json(res, { ok: false, error: 'this note is private and keeps no screenshots' }, 404); return true; }
642
+ notesScreen.serve(req, res, dir, nid, url.searchParams.get('t'));
643
+ return true;
644
+ }
605
645
  if (p === '/api/notes/recording' && (req.method === 'GET' || req.method === 'HEAD')) {
606
646
  const nid = url.searchParams.get('id');
607
647
  const r = notes.read(dir, nid);
@@ -669,8 +709,13 @@ async function handle(req, res, url, ctx) {
669
709
  const rm = resumeMsOf(ctx);
670
710
  // a pause that ran out ends the meeting first (final notes and all), then the record is refused
671
711
  endMeeting(ctx, id, 'timeout');
712
+ // one capture per note: another tab or window that is sending this note's audio keeps it (notes-recording-reliability)
713
+ if (notesCapture.busy(dir, id)) return done({ ok: false, status: 409, error: 'this meeting is already being recorded in another tab or window' });
672
714
  const r = notes.record(dir, { id, ws: body.ws == null ? undefined : o.w.id, resumeMs: rm });
673
715
  if (r.ok) {
716
+ // the page names each chunk's place from here: the capture's token, and the meeting time it starts at
717
+ const c = notesCapture.start(dir, id, { baseMs: r.meeting_ms });
718
+ if (c.ok) r.capture = { token: c.token, base_ms: c.base_ms };
674
719
  clearExpiry(id); swept.delete(id);
675
720
  // a new Listen tries the agent again when it could not start last time
676
721
  const cur = notes.read(dir, id);
@@ -685,6 +730,7 @@ async function handle(req, res, url, ctx) {
685
730
  }
686
731
  if (p === '/api/notes/pause') {
687
732
  const r = notes.pause(dir, { id, resumeMs: resumeMsOf(ctx) });
733
+ notesCapture.close(dir, id);
688
734
  // listening stopped: the minute timer stops, and what was said since the last minute is one more batch.
689
735
  // The meeting stays open: Wispr Flow and the final pass wait for Stop or for the hour to run out.
690
736
  if (r.ok) { disarm(id); runMinute(ctx, id, { final: true }); if (r.state === 'paused') armExpiry(ctx, id, r.resume_until); }
@@ -692,6 +738,7 @@ async function handle(req, res, url, ctx) {
692
738
  }
693
739
  if (p === '/api/notes/stop') {
694
740
  const r = notes.stop(dir, { id, resumeMs: resumeMsOf(ctx) });
741
+ notesCapture.close(dir, id);
695
742
  if (r.ok) { clearExpiry(id); swept.add(id); disarm(id); runMinute(ctx, id, { final: true }); scheduleWispr(ctx, id); }
696
743
  return done(r);
697
744
  }
@@ -718,15 +765,41 @@ async function handle(req, res, url, ctx) {
718
765
  if (pcm24 && pcm24.length % 2) return done({ ok: false, status: 400, error: 'the audio must be 16-bit samples' });
719
766
  if (!liveDetect(ctx).ready) return done({ ok: false, status: 409, error: 'local transcription is not set up' });
720
767
  const ms = (pcm.length / 2 / whisper.RATE) * 1000;
721
- const t = Math.max(0, notes.meetingMs(note.note) - ms);
768
+ // A chunk that names its capture (notes-recording-reliability) is placed at the capture's start plus its offset, and a sequence
769
+ // number already written is answered as done and neither written nor transcribed again. A chunk with no token keeps the old
770
+ // placement by arrival time, unless a capture with a token is live: then it comes from a second tab and is refused.
771
+ let t; let sample = null; let cap = null;
772
+ if (body.cap !== undefined) {
773
+ cap = notesCapture.check(dir, id, { token: String(body.cap), source, seq: body.seq });
774
+ if (!cap.ok) return done(cap);
775
+ if (cap.dup) return done({ ok: true, cut: false, dup: true });
776
+ const off = Number(body.off);
777
+ if (!Number.isInteger(off) || off < 0) return done({ ok: false, status: 400, error: 'a chunk needs its sample offset' });
778
+ sample = Math.round(cap.base_ms * whisper.RATE / 1000) + off;
779
+ t = (sample / whisper.RATE) * 1000;
780
+ } else {
781
+ if (notesCapture.tokened(dir, id)) return done({ ok: false, status: 409, error: 'this meeting is already being recorded in another tab or window' });
782
+ t = Math.max(0, notes.meetingMs(note.note) - ms);
783
+ }
722
784
  // the audio is kept for replay unless the PM switched that off (D92); a private note never reaches this line
723
785
  arm(ctx, id);
724
786
  // each source is also kept apart for the final pass after Stop, which deletes it when the PM keeps no audio (Run 9)
725
787
  // a 24 kHz chunk must cover the same time as the 16 kHz chunk (to a tenth of a second); one that does not is not kept
726
788
  const sameTime = !!pcm24 && pcm24.length > 0 && Math.abs(pcm24.length / 2 / notesAudio.HI_RATE - pcm.length / 2 / whisper.RATE) <= 0.1;
727
789
  if (pcm24 && pcm24.length && !sameTime) audioErrors.set(id, { at: new Date().toISOString(), error: 'the 24 kHz audio of one chunk did not match its 16 kHz audio, so the replay has a gap there' });
728
- { try { notesAudio.write(dir, { id, source, pcm, pcm24: sameTime ? pcm24 : null, t, mixed: notesAudio.getKeep(dir) }); } catch (e) { audioErrors.set(id, { at: new Date().toISOString(), error: 'the audio was not kept: ' + ((e && e.message) || e) }); } }
729
- return done(transcriberFor(ctx).push({ id, source, pcm, t, final: body.final === true }));
790
+ { try { notesAudio.write(dir, { id, source, pcm, pcm24: sameTime ? pcm24 : null, t, sample, mixed: notesAudio.getKeep(dir) }); } catch (e) { audioErrors.set(id, { at: new Date().toISOString(), error: 'the audio was not kept: ' + ((e && e.message) || e) }); } }
791
+ const tr = transcriberFor(ctx);
792
+ if (cap && !fedHere.has(id + '|' + source + '|' + body.cap)) {
793
+ fedHere.add(id + '|' + source + '|' + body.cap);
794
+ if (fedHere.size > 400) fedHere.delete(fedHere.values().next().value);
795
+ // after a restart, what this source said since its last cut was written but never transcribed: transcribe it first
796
+ const gap = notesCapture.untranscribed(dir, id, { source, before: sample });
797
+ const back = gap ? notesAudio.readSource(dir, id, source, gap.from, gap.to) : null;
798
+ if (back && back.pcm.length) tr.push({ id, source, pcm: back.pcm, t: (back.from / whisper.RATE) * 1000, exact: true });
799
+ }
800
+ const pushed = tr.push({ id, source, pcm, t, final: body.final === true, exact: !!cap });
801
+ if (cap) notesCapture.mark(dir, id, { source, seq: body.seq, end: sample + pcm.length / 2 });
802
+ return done(pushed);
730
803
  }
731
804
  if (p === '/api/notes/label') return done(notes.setLabel(dir, { id, index: Number(body.index), cat: body.cat, drop: body.drop === true }));
732
805
  if (p === '/api/notes/speaker') {
@@ -749,6 +822,23 @@ async function handle(req, res, url, ctx) {
749
822
  if (!r.ok) return done(r);
750
823
  return done({ ok: true, deleted: notesAudio.remove(dir, id) });
751
824
  }
825
+ if (p === '/api/notes/screen') {
826
+ const note = notes.read(dir, id);
827
+ if (!note.ok) return done(note);
828
+ if (note.note.private) return done({ ok: false, status: 409, error: 'this note is marked private, so nothing more is kept in it' });
829
+ if (!note.note.recorded_from) return done({ ok: false, status: 409, error: 'press Listen first' });
830
+ if (!notesScreen.getOn(dir)) return done({ ok: false, status: 409, error: 'Capture the shared screen is switched off in Settings' });
831
+ const own = notesCapture.owns(dir, id, { token: body.cap });
832
+ if (!own.ok) return done(own);
833
+ if (typeof body.t !== 'number' || !Number.isFinite(body.t) || body.t < 0) return done({ ok: false, status: 400, error: 'a screenshot needs its meeting time' });
834
+ if (typeof body.img !== 'string' || !body.img) return done({ ok: false, status: 400, error: 'there is no picture to keep' });
835
+ return done(notesScreen.add(dir, { id, t: body.t, buf: Buffer.from(body.img, 'base64'), w: body.w, h: body.h, why: body.why }));
836
+ }
837
+ if (p === '/api/notes/screen-delete') {
838
+ const r = notes.read(dir, id);
839
+ if (!r.ok) return done(r);
840
+ return done({ ok: true, deleted: notesScreen.remove(dir, id) });
841
+ }
752
842
  if (p === '/api/notes/clean-audio') {
753
843
  // the PM asks for the cleaned copy of one note now; the work goes on in the background and the replay changes when it ends
754
844
  const r = notes.read(dir, id);
@@ -765,6 +855,13 @@ async function handle(req, res, url, ctx) {
765
855
  if (body.wispr !== undefined) {
766
856
  if (typeof body.wispr !== 'boolean') { ctx.json(res, { ok: false, error: 'say wispr: true or wispr: false' }, 400); return true; }
767
857
  setWisprOn(dir, body.wispr);
858
+ if (body.language === undefined && body.model === undefined && body.accuracy === undefined && body.keep_audio === undefined && body.clean_audio === undefined && body.jev === undefined && body.check === undefined && body.screen === undefined) {
859
+ ctx.json(res, { ok: true, ...engineInfo(ctx, o.ok ? o.w : null) }); return true;
860
+ }
861
+ }
862
+ if (body.screen !== undefined) {
863
+ const sc = notesScreen.setOn(dir, body.screen);
864
+ if (!sc.ok) { ctx.json(res, { ok: false, error: sc.error }, 400); return true; }
768
865
  if (body.language === undefined && body.model === undefined && body.accuracy === undefined && body.keep_audio === undefined && body.clean_audio === undefined && body.jev === undefined && body.check === undefined) {
769
866
  ctx.json(res, { ok: true, ...engineInfo(ctx, o.ok ? o.w : null) }); return true;
770
867
  }
@@ -808,7 +905,7 @@ async function handle(req, res, url, ctx) {
808
905
  if (p === '/api/notes/private') {
809
906
  vocabCache.delete(id);
810
907
  const r = notes.setPrivate(dir, { id, private: body.private });
811
- if (r.ok && r.private) { clearExpiry(id); disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); notesAudio.remove(dir, id); }
908
+ if (r.ok && r.private) { clearExpiry(id); disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); notesAudio.remove(dir, id); notesCapture.remove(dir, id); notesScreen.remove(dir, id); }
812
909
  return done(r);
813
910
  }
814
911
  if (p === '/api/notes/summarize') {
@@ -837,7 +934,7 @@ async function handle(req, res, url, ctx) {
837
934
  }
838
935
  return done(r);
839
936
  }
840
- if (p === '/api/notes/delete') { clearExpiry(id); swept.delete(id); 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); }
937
+ if (p === '/api/notes/delete') { clearExpiry(id); swept.delete(id); 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); notesCapture.remove(dir, id); notesScreen.remove(dir, id); } return done(r); }
841
938
  if (p === '/api/notes/highlight') {
842
939
  // the PM's highlight: the transcript of the window, then Jev's label and the repository check of its text
843
940
  const r = notes.addHighlight(dir, { id, from: body.from, to: body.to });
@@ -886,7 +983,8 @@ function startAgent(ctx, dir, id, w) {
886
983
  notes.agentStarted(dir, { id, started: false, reason: 'this workspace cannot start sessions' });
887
984
  return { ok: true, started: false, session: null, reason: 'this workspace cannot start sessions', handoff: h.handoff, out: h.out, withheld: h.withheld };
888
985
  }
889
- const s = ctx.spawnSession({ name: 'notes: summary', agent: 'claude', ws: w ? w.id : undefined, origin: 'notes-summary:' + id, initialInput: h.prompt + '\r' });
986
+ const s = ctx.spawnSession({ name: 'notes: summary', agent: 'claude', ws: w ? w.id : undefined, origin: 'notes-summary:' + id, initialInput: h.prompt + '\r',
987
+ addDirs: [...new Set([path.dirname(h.handoff), path.dirname(h.out)])] });
890
988
  const started = !!s && s.state !== 'exited';
891
989
  const reason = started ? '' : ((s && s.note) || 'could not start a claude session');
892
990
  notes.agentStarted(dir, { id, session: s ? s.id : null, started, reason });
@@ -5,6 +5,7 @@
5
5
  * claims: { ok, refused: [line] }, last_result }], channels }
6
6
  * POST /api/outbox/draft { channel, recipient, subject?, body } → { ok, entry } origin is 'pm' (a page press)
7
7
  * POST /api/outbox/edit { id, body } → { ok, entry } a new hash; the old hash is void
8
+ * POST /api/outbox/discard { id, undo? } → { ok } hides a draft (undo: true brings it back); a sent entry is refused
8
9
  * POST /api/outbox/confirm { id, hash, reason? } → { ok:false, reason:'no channel connected' } entry stays queued
9
10
  * → 422 { ok:false, state:'blocked', refused } when a claim fails the trust rules
10
11
  * and no written reason is given; with a reason it is an override (recorded)
@@ -17,7 +18,8 @@
17
18
  * The handlers are pure over (req, body, runtimeDir) so a test needs no server.
18
19
  */
19
20
  const guard = require('./pm-guard.js');
20
- const outbox = require('../../scripts/outbox.js');
21
+ const { repoScript } = require('./repo-script.js');
22
+ const outbox = require(repoScript('outbox.js'));
21
23
  const store = require('./outbox-store.js');
22
24
  const crypto = require('crypto');
23
25
 
@@ -57,6 +59,24 @@ function edit(req, body, runtimeDir, now = new Date()) {
57
59
  return { status: 200, body: { ok: true, entry: view({ ...e, last_result: null }), voided_hash: cur.hash } };
58
60
  }
59
61
 
62
+ /* Discard one draft from the queue (Reports > Drafts), or take the discard back (undo: true). The line stays in the log; the fold hides the entry.
63
+ * A sent entry is not discarded: it is a record of what left. */
64
+ function discard(req, body, runtimeDir, now = new Date()) {
65
+ const bad = guarded(req, body); if (bad) return bad;
66
+ const all = store.readLines(store.outboxFile(runtimeDir));
67
+ const id = String(body.id || '');
68
+ if (body.undo === true) {
69
+ if (!all.some((l) => l && l.id === id && l.event === 'discarded')) return fail(404, 'no discarded outbox entry with that id');
70
+ store.appendEvent(runtimeDir, { event: 'restored', id }, now);
71
+ return { status: 200, body: { ok: true, id, restored: true } };
72
+ }
73
+ const cur = store.readOutbox(runtimeDir).find((e) => e.id === id);
74
+ if (!cur) return fail(404, 'no such outbox entry');
75
+ if (cur.state === 'sent') return fail(409, 'a sent entry is a record and is not discarded');
76
+ store.appendEvent(runtimeDir, { event: 'discarded', id }, now);
77
+ return { status: 200, body: { ok: true, id, discarded: true } };
78
+ }
79
+
60
80
  function confirm(req, body, runtimeDir, now = new Date(), senders) {
61
81
  const bad = guarded(req, body); if (bad) return bad;
62
82
  const cur = store.readOutbox(runtimeDir).find((e) => e.id === body.id);
@@ -78,11 +98,11 @@ function confirm(req, body, runtimeDir, now = new Date(), senders) {
78
98
  return { status: 200, body: { ok: true, hash: r.hash, override: r.override, state: 'sent' } };
79
99
  }
80
100
 
81
- const ROUTES = { 'POST /api/outbox/draft': draft, 'POST /api/outbox/edit': edit, 'POST /api/outbox/confirm': confirm };
101
+ const ROUTES = { 'POST /api/outbox/draft': draft, 'POST /api/outbox/edit': edit, 'POST /api/outbox/discard': discard, 'POST /api/outbox/confirm': confirm };
82
102
 
83
103
  module.exports = {
84
104
  prefixes: ['/api/outbox'],
85
- claimCheck, list, draft, edit, confirm, ROUTES,
105
+ claimCheck, list, draft, edit, discard, confirm, ROUTES,
86
106
  handle: async (req, res, url, ctx) => {
87
107
  const p = url.pathname;
88
108
  if (p === '/api/outbox' && req.method === 'GET') { const r = list(ctx.runtimeDir); ctx.json(res, r.body, r.status); return true; }
@@ -5,7 +5,7 @@
5
5
  * GET /api/pm-items → { ok, today, lists: { waiting, today, week, triage, later }, jobs, items,
6
6
  * work: { inbox, today, upcoming, waiting, someday } open items, placed once here
7
7
  * review, review_days: 7 open items untouched 7 days, most stale first
8
- * focus: { suggestions, accepted } at most 3 suggestions: { key, title, why, item_ids, job }
8
+ * focus: { suggestions, accepted, dismissed } at most 3 suggestions: { key, title, why, item_ids, job }; dismissed = the keys marked not today
9
9
  * counts: { inbox, today, upcoming, waiting, someday, review } }
10
10
  * POST /api/pm-items/add { line } → { ok, item } one line: the parser (lib/pm-items.js) reads the parts
11
11
  * POST /api/pm-items/edit { id, fields }→ { ok, item } fields: title, due, priority, waiting_on, job,
@@ -45,7 +45,9 @@ const guard = require('./pm-guard.js');
45
45
  const pmItems = require('./pm-items.js');
46
46
  const jobs = require('./jobs-to-be-done.js');
47
47
  const workFocus = require('./work-focus.js');
48
+ const workEvidence = require('./work-evidence.js');
48
49
  const steps = require('./item-steps.js');
50
+ const { repoScript } = require('./repo-script.js');
49
51
 
50
52
  const FIELDS = ['title', 'due', 'priority', 'waiting_on', 'job', 'defer', 'flagged', 'resolves', 'kind', 'person', 'evidence', 'outcome'];
51
53
  const STRING_OR_NULL = ['resolves', 'person', 'evidence', 'outcome'];
@@ -75,10 +77,11 @@ function resolvesTitles(items, ctx, url) {
75
77
  } catch { return {}; }
76
78
  }
77
79
 
78
- function view(itemLines, jobLines, now, stepLines = [], focusLines = []) {
80
+ function view(itemLines, jobLines, now, stepLines = [], focusLines = [], proof = null) {
79
81
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
80
82
  const byItem = steps.foldSteps(stepLines).byItem;
81
- const items = pmItems.fold(itemLines).map((i) => ({ ...i, steps: byItem[i.id] || [] }));
83
+ const folded = pmItems.fold(itemLines).map((i) => ({ ...i, steps: byItem[i.id] || [] }));
84
+ const items = proof ? workEvidence.withEvidence(folded, { ...proof, now: now.getTime() }) : folded;
82
85
  const currentJobs = jobs.foldJobs(jobLines).map((j) => ({ ...j, ...jobs.progress(j.id, items) }));
83
86
  const lists = Object.fromEntries(pmItems.LISTS.map((l) => [l, []]));
84
87
  for (const it of items) lists[pmItems.placement(it, today)].push(it);
@@ -88,11 +91,21 @@ function view(itemLines, jobLines, now, stepLines = [], focusLines = []) {
88
91
  for (const l of pmItems.WORK_LISTS) work[l] = pmItems.sortWorkList(l, work[l], today);
89
92
  const review = pmItems.reviewQueue(items, now);
90
93
  const state = workFocus.foldFocus(focusLines, todayIso);
91
- const focus = { suggestions: workFocus.focusSuggestions(work.today, currentJobs, state, todayIso), accepted: state.accepted };
94
+ const focus = { suggestions: workFocus.focusSuggestions(work.today, currentJobs, state, todayIso), accepted: state.accepted, dismissed: [...state.dismissed] };
92
95
  const counts = { ...Object.fromEntries(pmItems.WORK_LISTS.map((l) => [l, work[l].length])), review: review.length };
93
96
  return { ok: true, today: todayIso, lists, jobs: currentJobs, items, work, review, review_days: pmItems.REVIEW_DAYS, focus, counts };
94
97
  }
95
98
 
99
+ // The calendar events and notes that can prove an offline item happened. Read only when an open offline item exists. Best effort: a failed read gives no proof.
100
+ function proofSources(ctx, itemLines) {
101
+ if (!ctx.runtimeDir || !pmItems.fold(itemLines).some((i) => i.kind === 'offline' && !i.done)) return null;
102
+ let events = [];
103
+ let notes = [];
104
+ try { events = require('./routes-calendar-import.js').readEvents(ctx.runtimeDir); } catch { /* no calendar cache */ }
105
+ try { const l = require('./notes.js').list(ctx.runtimeDir); notes = l && l.ok ? l.notes : []; } catch { /* no notes */ }
106
+ return { events, notes };
107
+ }
108
+
96
109
  // Each operation returns { status, body, events } and writes nothing itself; the caller appends `events`.
97
110
  function checkFields(fields, allowed) {
98
111
  if (!fields || typeof fields !== 'object') return 'fields must be an object';
@@ -226,7 +239,7 @@ function planSteps(b, itemLines) {
226
239
  // With an agent's reply (from the reply path), parse it: a step that sends something to a person is forced to
227
240
  // agent-drafts by the planner. Without one, fall back to the single review step for the PM.
228
241
  if (b.reply !== undefined) {
229
- const plan = require('../../scripts/step-planner.js').parsePlanReply(b.reply);
242
+ const plan = require(repoScript('step-planner.js')).parsePlanReply(b.reply);
230
243
  if (!plan.ok) return { status: 400, body: { ok: false, error: plan.error } };
231
244
  const added = plan.steps.map((st) => steps.addStep(b.id, st.text, st.who, newId('step')));
232
245
  return { status: 200, body: { ok: true, steps: added.map((r) => ({ id: r.event.id, text: r.event.text, who: r.event.who })) }, events: added.map((r) => r.event) };
@@ -299,7 +312,7 @@ module.exports = {
299
312
  const p = url.pathname;
300
313
  if (p === '/api/pm-items' && req.method === 'GET') {
301
314
  const c = prepare(ctx);
302
- const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines);
315
+ const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines, proofSources(ctx, c.itemLines));
303
316
  v.resolves_titles = resolvesTitles(v.items, ctx, url);
304
317
  ctx.json(res, v);
305
318
  return true;
@@ -11,8 +11,9 @@
11
11
  */
12
12
  const path = require('path');
13
13
  const guard = require('./pm-guard.js');
14
- const records = require(path.join(__dirname, '..', '..', 'scripts', 'records', 'index.js'));
15
- const ledger = require(path.join(__dirname, '..', '..', 'scripts', 'trust', 'ledger.js'));
14
+ const { repoScript } = require('./repo-script.js');
15
+ const records = require(repoScript('records/index.js'));
16
+ const ledger = require(repoScript('trust/ledger.js'));
16
17
 
17
18
  const rootOf = (ctx) => (ctx && ctx.recordsDir) || path.join(ctx.repoRoot || path.join(__dirname, '..', '..'), 'planning', 'records');
18
19
  const UI = () => ledger.actorFrom({ ui: true });
@@ -18,7 +18,8 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const crypto = require('crypto');
20
20
  const guard = require('./pm-guard.js');
21
- const R = require('../../scripts/releases.js');
21
+ const { repoScript } = require('./repo-script.js');
22
+ const R = require(repoScript('releases.js'));
22
23
 
23
24
  const ROOT = path.resolve(__dirname, '..', '..');
24
25
  const dirOf = (ctx) => path.join(ctx.runtimeDir, 'releases');
@@ -13,8 +13,9 @@
13
13
  * Read-only. The repository read is slow (it derives each initiative's stage from git), so a successful read is kept 5 minutes. */
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const RF = require('../../scripts/report-families.js');
17
- const { foldReleases } = require('../../scripts/releases.js');
16
+ const { repoScript } = require('./repo-script.js');
17
+ const RF = require(repoScript('report-families.js'));
18
+ const { foldReleases } = require(repoScript('releases.js'));
18
19
 
19
20
  const prefixes = ['/api/report-families/facts'];
20
21
  const TTL = 5 * 60 * 1000;
@@ -24,7 +25,7 @@ function repoInis(root, opts) {
24
25
  const hit = memo.get(root);
25
26
  const now = (opts && opts.clock) ? opts.clock() : Date.now();
26
27
  if (hit && now - hit.at < TTL) return hit.inis;
27
- const { readRepoFacts } = require('../../scripts/signals/facts.js');
28
+ const { readRepoFacts } = require(repoScript('signals/facts.js'));
28
29
  const inis = (readRepoFacts(root, {}).inis || []).map((i) => ({ id: i.id, title: i.title, promised: i.promised, done: !!i.done }));
29
30
  memo.set(root, { at: now, inis });
30
31
  return inis;
@@ -5,7 +5,8 @@
5
5
  * POST /api/report-families/check { family, variant?, sections, known_keys?, absent_keys?, stale_keys? }
6
6
  * → { ok, blocked, reasons, warnings, sections, ... }
7
7
  * Pure. The check reads nothing from disk and writes nothing. There is no send and no issue route in this wave. */
8
- const RF = require('../../scripts/report-families.js');
8
+ const { repoScript } = require('./repo-script.js');
9
+ const RF = require(repoScript('report-families.js'));
9
10
 
10
11
  const prefixes = ['/api/report-families'];
11
12
  const strs = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined);
@@ -19,10 +19,11 @@
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
  const guard = require('./pm-guard.js');
22
- const records = require(path.join(__dirname, '..', '..', 'scripts', 'records', 'index.js'));
23
- const ledger = require(path.join(__dirname, '..', '..', 'scripts', 'trust', 'ledger.js'));
24
- const roadmap = require(path.join(__dirname, '..', '..', 'scripts', 'roadmap-ops.js'));
25
- const breakdown = require(path.join(__dirname, '..', '..', 'scripts', 'breakdown-ops.js'));
22
+ const { repoScript } = require('./repo-script.js');
23
+ const records = require(repoScript('records/index.js'));
24
+ const ledger = require(repoScript('trust/ledger.js'));
25
+ const roadmap = require(repoScript('roadmap-ops.js'));
26
+ const breakdown = require(repoScript('breakdown-ops.js'));
26
27
 
27
28
  const REPO = path.join(__dirname, '..', '..');
28
29
  const rootOf = (ctx) => (ctx && ctx.recordsDir) || path.join((ctx && ctx.repoRoot) || REPO, 'planning', 'records');
@@ -13,11 +13,12 @@ const path = require('path');
13
13
  const guard = require('./pm-guard.js');
14
14
  const actor = require('./actor.js');
15
15
  const runAnswers = require('./run-answers.js');
16
- const { groupSessions } = require('../../scripts/sessions-list.js');
17
- const { cardFor, correctionEvent } = require('../../scripts/card-state.js');
18
- const { readLines, runFor, foldReplies } = require('../../scripts/replies.js');
19
- const C = require('../../scripts/reply-contract/reply-contract.js');
20
- const { appendOutbox } = require('../../scripts/outbox.js');
16
+ const { repoScript } = require('./repo-script.js');
17
+ const { groupSessions } = require(repoScript('sessions-list.js'));
18
+ const { cardFor, correctionEvent } = require(repoScript('card-state.js'));
19
+ const { readLines, runFor, foldReplies } = require(repoScript('replies.js'));
20
+ const C = require(repoScript('reply-contract/reply-contract.js'));
21
+ const { appendOutbox } = require(repoScript('outbox.js'));
21
22
 
22
23
  const prefixes = ['/api/runs'];
23
24
  const RUN_RE = /^sess-\d+$/;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+ /* routes-settings.js — the console3 settings store over HTTP (initiative console3-settings).
3
+ *
4
+ * GET /api/settings?ws=… → { ok, settings, overrides, categories, state_dir }
5
+ * POST /api/settings { key, value, ws } → { ok, key, value } page press; 400 for a value the setting refuses
6
+ * POST /api/settings { key, reset: true, ws } → { ok, key, value } back to the default
7
+ *
8
+ * Only the settings in lib/settings-store.js live here. A setting with a store of its own (the recording language, Online,
9
+ * the TypeSafe key, validate consent, profiles) keeps its own route, and the Settings page calls that one. Writes pass
10
+ * pm-guard requirePagePress, so an agent token is refused. Nothing here makes a network request. */
11
+ const store = require('./settings-store.js');
12
+ const guard = require('./pm-guard.js');
13
+
14
+ /** The workspace a request names, only when it is registered under exactly that id (getWs falls back to the home one). */
15
+ function wsOf(ctx, id) {
16
+ if (id == null || id === '') return null;
17
+ const w = typeof ctx.getWs === 'function' ? ctx.getWs(String(id)) : null;
18
+ return w && w.id === String(id) ? w.id : false;
19
+ }
20
+
21
+ function read(ctx, wsParam) {
22
+ const ws = wsOf(ctx, wsParam);
23
+ if (ws === false) return { status: 404, body: { ok: false, error: 'This workspace is not registered.' } };
24
+ return { status: 200, body: { ok: true, ws: ws || null, ...store.all(ctx.runtimeDir, ws, { env: ctx.env || process.env }), state_dir: ctx.runtimeDir } };
25
+ }
26
+
27
+ function write(ctx, req, body) {
28
+ const g = guard.requirePagePress(req, body);
29
+ if (!g.ok) return { status: g.status, body: { ok: false, error: g.error } };
30
+ const key = typeof body.key === 'string' ? body.key : '';
31
+ const ws = wsOf(ctx, body.ws);
32
+ if (ws === false) return { status: 404, body: { ok: false, error: 'This workspace is not registered.' } };
33
+ const r = body.reset === true ? store.reset(ctx.runtimeDir, key, ws) : store.set(ctx.runtimeDir, key, body.value, ws);
34
+ if (!r.ok) return { status: r.status || 400, body: { ok: false, key, error: r.error } };
35
+ return { status: 200, body: { ok: true, key, value: r.value } };
36
+ }
37
+
38
+ module.exports = {
39
+ prefixes: ['/api/settings'],
40
+ read, write,
41
+ handle: async (req, res, url, ctx) => {
42
+ if (url.pathname !== '/api/settings') return false;
43
+ if (req.method === 'GET') { const r = read(ctx, url.searchParams.get('ws')); ctx.json(res, r.body, r.status); return true; }
44
+ if (req.method !== 'POST') { ctx.json(res, { ok: false, error: '/api/settings takes GET or POST' }, 405); return true; }
45
+ const got = await ctx.readJson(req);
46
+ if (!got.ok) { ctx.json(res, { ok: false, error: 'invalid JSON body' }, 400); return true; }
47
+ const r = write(ctx, req, got.body || {});
48
+ ctx.json(res, r.body, r.status);
49
+ return true;
50
+ },
51
+ };
@@ -9,8 +9,9 @@
9
9
  * File: <runtime>/signals/signals.jsonl (append only, scripts/trust/ledger.js). */
10
10
  const path = require('path');
11
11
  const guard = require('./pm-guard.js');
12
- const S = require('../../scripts/signal-rules.js');
13
- const L = require('../../scripts/trust/ledger.js');
12
+ const { repoScript } = require('./repo-script.js');
13
+ const S = require(repoScript('signal-rules.js'));
14
+ const L = require(repoScript('trust/ledger.js'));
14
15
 
15
16
  const fileOf = (runtimeDir) => path.join(runtimeDir, 'signals', 'signals.jsonl');
16
17
  const fail = (status, error) => ({ status, body: { ok: false, error } });