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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- 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 }
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
15
|
-
const
|
|
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
|
|
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
|
|
17
|
-
const
|
|
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('
|
|
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
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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 {
|
|
17
|
-
const {
|
|
18
|
-
const {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
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
|
|
13
|
-
const
|
|
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 } });
|