create-pmos 0.134.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 (209) 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 +16 -1
  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-capture.js +9 -1
  83. package/workspace/lib/notes-final.js +38 -13
  84. package/workspace/lib/notes-replay.js +125 -0
  85. package/workspace/lib/notes-screen.js +180 -0
  86. package/workspace/lib/notes-whisper.js +10 -3
  87. package/workspace/lib/outbox-store.js +10 -6
  88. package/workspace/lib/pmos-verbs.js +3 -1
  89. package/workspace/lib/proactive-cli.js +7 -0
  90. package/workspace/lib/proactive.js +15 -2
  91. package/workspace/lib/repo-script.js +36 -0
  92. package/workspace/lib/report-dashboards.js +5 -2
  93. package/workspace/lib/report-frame.js +122 -0
  94. package/workspace/lib/report-schedules.js +113 -0
  95. package/workspace/lib/route-modules.js +9 -2
  96. package/workspace/lib/routes-boards.js +734 -0
  97. package/workspace/lib/routes-brief-page.js +13 -4
  98. package/workspace/lib/routes-briefing.js +22 -11
  99. package/workspace/lib/routes-c3-init.js +4 -3
  100. package/workspace/lib/routes-c3-life.js +2 -1
  101. package/workspace/lib/routes-c3-reports.js +96 -13
  102. package/workspace/lib/routes-c3-strategy.js +4 -3
  103. package/workspace/lib/routes-c3-timeline.js +3 -2
  104. package/workspace/lib/routes-calendar-import.js +3 -2
  105. package/workspace/lib/routes-calendar-views.js +9 -4
  106. package/workspace/lib/routes-doc-health.js +2 -1
  107. package/workspace/lib/routes-doc-sheet.js +2 -1
  108. package/workspace/lib/routes-forecast.js +2 -1
  109. package/workspace/lib/routes-hooks.js +3 -2
  110. package/workspace/lib/routes-initiatives.js +2 -1
  111. package/workspace/lib/routes-intake.js +2 -1
  112. package/workspace/lib/routes-notes.js +55 -9
  113. package/workspace/lib/routes-outbox.js +23 -3
  114. package/workspace/lib/routes-pm-items.js +19 -6
  115. package/workspace/lib/routes-records.js +3 -2
  116. package/workspace/lib/routes-releases.js +2 -1
  117. package/workspace/lib/routes-report-facts.js +4 -3
  118. package/workspace/lib/routes-report-families.js +2 -1
  119. package/workspace/lib/routes-roadmap-ops.js +5 -4
  120. package/workspace/lib/routes-runs.js +6 -5
  121. package/workspace/lib/routes-settings.js +51 -0
  122. package/workspace/lib/routes-signals.js +3 -2
  123. package/workspace/lib/routes-stage-tools.js +220 -0
  124. package/workspace/lib/run-answers.js +2 -1
  125. package/workspace/lib/settings-store.js +226 -0
  126. package/workspace/lib/stage-tools.js +208 -0
  127. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  128. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  129. package/workspace/lib/work-evidence.js +73 -0
  130. package/workspace/package-lock.json +2 -2
  131. package/workspace/package.json +1 -1
  132. package/workspace/public/pm/briefing-deck.js +63 -17
  133. package/workspace/public/pm/briefing-model.js +36 -7
  134. package/workspace/public/pm/c3-ambient.js +100 -0
  135. package/workspace/public/pm/c3-brief-needs.js +26 -6
  136. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  137. package/workspace/public/pm/c3-brief-today.js +56 -21
  138. package/workspace/public/pm/c3-brief.js +2 -1
  139. package/workspace/public/pm/c3-calendar.js +161 -42
  140. package/workspace/public/pm/c3-calpop.js +5 -5
  141. package/workspace/public/pm/c3-demo-boards.js +649 -0
  142. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  143. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  144. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  145. package/workspace/public/pm/c3-demo-data.js +17 -27
  146. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  147. package/workspace/public/pm/c3-demo.js +2 -1
  148. package/workspace/public/pm/c3-dock.js +1 -1
  149. package/workspace/public/pm/c3-ib-docs.js +80 -0
  150. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  151. package/workspace/public/pm/c3-ib-items.js +201 -0
  152. package/workspace/public/pm/c3-ib-releases.js +51 -0
  153. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  154. package/workspace/public/pm/c3-ib-triage.js +178 -0
  155. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  156. package/workspace/public/pm/c3-ini-records.js +197 -0
  157. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  158. package/workspace/public/pm/c3-ini-ui.js +229 -0
  159. package/workspace/public/pm/c3-ini-work.js +139 -0
  160. package/workspace/public/pm/c3-initiatives.css +218 -0
  161. package/workspace/public/pm/c3-initiatives.js +741 -0
  162. package/workspace/public/pm/c3-plan.js +2 -2
  163. package/workspace/public/pm/c3-reports.css +47 -47
  164. package/workspace/public/pm/c3-reports.js +223 -185
  165. package/workspace/public/pm/c3-settings.css +74 -0
  166. package/workspace/public/pm/c3-settings.js +703 -0
  167. package/workspace/public/pm/c3-shell.js +102 -36
  168. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  169. package/workspace/public/pm/c3-terminal.js +2 -2
  170. package/workspace/public/pm/calendar-views.js +1 -1
  171. package/workspace/public/pm/components.css +319 -213
  172. package/workspace/public/pm/console3.css +99 -23
  173. package/workspace/public/pm/dest-notes.js +26 -4
  174. package/workspace/public/pm/notes-screen-capture.js +165 -0
  175. package/workspace/public/pm/notes-screen-view.js +98 -0
  176. package/workspace/public/pm/panel-trees.js +5 -2
  177. package/workspace/public/pm/pm-work-view.js +233 -146
  178. package/workspace/public/pm/shell-bell.js +3 -2
  179. package/workspace/public/pm/shell-recording.js +29 -8
  180. package/workspace/public/pm/tokens.css +2 -2
  181. package/workspace/public/pm/view-notes.js +46 -9
  182. package/workspace/public/pm/welcome.js +197 -89
  183. package/workspace/public/welcome.html +6 -4
  184. package/workspace/server.js +105 -6
  185. package/workspace/skills/day-check.skill +1 -1
  186. package/workspace/skills/jira-sync.skill +6 -4
  187. package/workspace/skills/morning-brief.skill +1 -1
  188. package/workspace/test-files.json +12 -3
  189. package/workspace/test-lists/c3-ib-items.json +9 -0
  190. package/workspace/test-lists/c3-ib-more.json +9 -0
  191. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  192. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  193. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  194. package/workspace/test-lists/console3-briefing.json +9 -0
  195. package/workspace/test-lists/console3-foundation.json +9 -0
  196. package/workspace/test-lists/console3-reports.json +3 -3
  197. package/workspace/test-lists/meeting-notes.json +10 -4
  198. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  199. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  200. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  201. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  202. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  203. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  204. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  205. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  206. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  207. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  208. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  209. 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',
@@ -35,11 +35,18 @@
35
35
  * POST /api/notes/line { id, index, cat?, summary?, tags?, people?, text? } → { ok, segment, summary } the PM's correction (Run 3)
36
36
  * GET /api/notes/recording?id=… → audio/wav, with byte ranges the note's kept audio (Run 3, D92)
37
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
38
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)
39
45
  * GET /api/notes/engine?ws=… → { ok, whisper, jev } is Whisper set up; which Jev gates hold (booleans only)
40
46
  * POST /api/notes/switch { ws, jev?, check? } → { ok, jev } the two D91 switches, default off
41
- * { language?, model?, accuracy?, keep_audio? } → { ok, whisper } the language, the model, the decoding
42
- * ('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
43
50
  * POST /api/notes/whisper-setup { ws? } → { ok, started, session, command } a terminal with the setup typed in, not run
44
51
  * POST /api/notes/private { id, private } → { ok, private }
45
52
  * POST /api/notes/summarize { id, ws?, prefer? } → { ok, summary, initiative, source, agent? } the local summary; with ws and
@@ -78,6 +85,7 @@ const whisper = require('./notes-whisper.js');
78
85
  const notesCapture = require('./notes-capture.js');
79
86
  const notesRepo = require('./notes-repo.js');
80
87
  const notesAudio = require('./notes-audio.js');
88
+ const notesScreen = require('./notes-screen.js');
81
89
  const notesEcho = require('./notes-echo.js');
82
90
  const notesPolish = require('./notes-polish.js');
83
91
  const notesFinal = require('./notes-final.js');
@@ -95,7 +103,7 @@ const VOCAB_TTL_MS = 60 * 1000;
95
103
  const prefixes = ['/api/notes'];
96
104
  const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
97
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',
98
- '/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']);
99
107
 
100
108
  /* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
101
109
  const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
@@ -278,8 +286,12 @@ function handToAgent(ctx, id, note, b) {
278
286
  const h = notes.minuteHandoff(dir, { id, minute: b.minute, lines: b.lines, context: b.context, final: b.final, terms: repoTermsFor(ctx, note) });
279
287
  if (!h.ok) return false;
280
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;
281
292
  const w = note.ws ? ctx.getWs(note.ws) : null;
282
- 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] });
283
295
  const started = !!s && s.state !== 'exited';
284
296
  if (!started) {
285
297
  try { fs.unlinkSync(h.md); } catch { /* gone */ }
@@ -422,7 +434,7 @@ function engineInfo(ctx, w) {
422
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,
423
435
  language: whisper.getLanguage(ctx.runtimeDir), languages: whisper.LANGUAGES.map(([value, label]) => ({ value, label })),
424
436
  model: whisper.getModel(ctx.runtimeDir), models: whisper.MODELS.map((m) => ({ value: m.id, label: m.label })),
425
- 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(),
426
438
  final: finalInfo(ctx), wispr: wisprOn(ctx) };
427
439
  if (!w) return { whisper: out, jev: null };
428
440
  const repo = tsSettings.getRepo(ctx.runtimeDir, w.root);
@@ -615,12 +627,21 @@ async function handle(req, res, url, ctx) {
615
627
  if (r.ok && mergeWispr(ctx, nid)) r = notes.read(dir, nid, { resumeMs: resumeMsOf(ctx) });
616
628
  if (r.ok) {
617
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) };
618
631
  if (!Array.isArray(r.note.bullets)) r.note.bullets = [];
619
632
  r.bullets_state = bulletsState(ctx, r.note);
620
633
  }
621
634
  send(ctx, res, r);
622
635
  return true;
623
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
+ }
624
645
  if (p === '/api/notes/recording' && (req.method === 'GET' || req.method === 'HEAD')) {
625
646
  const nid = url.searchParams.get('id');
626
647
  const r = notes.read(dir, nid);
@@ -801,6 +822,23 @@ async function handle(req, res, url, ctx) {
801
822
  if (!r.ok) return done(r);
802
823
  return done({ ok: true, deleted: notesAudio.remove(dir, id) });
803
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
+ }
804
842
  if (p === '/api/notes/clean-audio') {
805
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
806
844
  const r = notes.read(dir, id);
@@ -817,6 +855,13 @@ async function handle(req, res, url, ctx) {
817
855
  if (body.wispr !== undefined) {
818
856
  if (typeof body.wispr !== 'boolean') { ctx.json(res, { ok: false, error: 'say wispr: true or wispr: false' }, 400); return true; }
819
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; }
820
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) {
821
866
  ctx.json(res, { ok: true, ...engineInfo(ctx, o.ok ? o.w : null) }); return true;
822
867
  }
@@ -860,7 +905,7 @@ async function handle(req, res, url, ctx) {
860
905
  if (p === '/api/notes/private') {
861
906
  vocabCache.delete(id);
862
907
  const r = notes.setPrivate(dir, { id, private: body.private });
863
- 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); }
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); }
864
909
  return done(r);
865
910
  }
866
911
  if (p === '/api/notes/summarize') {
@@ -889,7 +934,7 @@ async function handle(req, res, url, ctx) {
889
934
  }
890
935
  return done(r);
891
936
  }
892
- 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); } 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); }
893
938
  if (p === '/api/notes/highlight') {
894
939
  // the PM's highlight: the transcript of the window, then Jev's label and the repository check of its text
895
940
  const r = notes.addHighlight(dir, { id, from: body.from, to: body.to });
@@ -938,7 +983,8 @@ function startAgent(ctx, dir, id, w) {
938
983
  notes.agentStarted(dir, { id, started: false, reason: 'this workspace cannot start sessions' });
939
984
  return { ok: true, started: false, session: null, reason: 'this workspace cannot start sessions', handoff: h.handoff, out: h.out, withheld: h.withheld };
940
985
  }
941
- 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)])] });
942
988
  const started = !!s && s.state !== 'exited';
943
989
  const reason = started ? '' : ((s && s.note) || 'could not start a claude session');
944
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 } });