create-pmos 0.107.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.107.0",
3
+ "version": "0.109.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@5953acb
1
+ kernel-source: pmos@3e0aefb
2
2
  assembled: 2026-10-08
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T03:57:21Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T03:57:21Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T03:57:21Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: layout"
4
4
  description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and lists; the keys; and the older layout."
5
- timestamp: 2026-10-07
5
+ timestamp: 2026-10-08
6
6
  ---
7
7
 
8
8
  # Guide: layout
@@ -31,7 +31,7 @@ Settings holds settings only, in eight sections. On a wide screen they are cards
31
31
 
32
32
  - **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
33
33
  - **Profiles**: the Claude profiles a session can start with. These are the descriptors in `~/.claude/profiles`, and also the profiles you already run: a shell command that sets `CLAUDE_CONFIG_DIR` (for example `alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'`), a launcher script named `claude-*`, or a config folder such as `~/.claude-work`. Each profile is a row with a **Detected** badge when PMOS found it rather than wrote it, and a **Default** badge when it is the default. Its ⋯ menu has **Use as default**. A profile found this way says where it was found, and a session under it sets only `CLAUDE_CONFIG_DIR`. The list reads again every few seconds while Settings is open, and **Look again** in the Claude profiles menu reads it at once. When nothing is found, the note names the start-up files and folders PMOS looked at. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
34
- - **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts.
34
+ - **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts. The default profile is one setting for the whole workspace. PMOS keeps it on the server, so every browser, port and device starts a Claude session with the same profile. Choose it here or with "make … the default" in the New session form. Both change the same setting.
35
35
  - **Validate consent**: the consent of the current repository. Use its switch to turn it on or off. **Open Validate** opens the Validate page as a tab.
36
36
  - **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
37
37
  - **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then turn on the **Online** switch. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
@@ -7,12 +7,14 @@ timestamp: 2026-10-08
7
7
 
8
8
  # Guide: notes
9
9
 
10
+ **Notes** is a page in the top bar, between Docs and People. The list on the left shows every note by initiative, with the time or day and an amber dot when a note conflicts with a file. The note you pick opens on the right: its title, the **Resume** button and the **⋯** menu, then the conflicts, the highlights, the overview, the decisions, the next steps (each with **Make task**), the open questions and a **Discussion** fold with the topics and the transcript. Press the title, the overview or any decision, next step or question to edit it. PMOS keeps the words the agent wrote. At the top right of the bar, **Record** starts a new note and listens at once. While it listens, the bar shows the red dot and the clock, a **Highlight** button and **Pause**. When it is paused, the bar shows **Resume**. Press the clock to open the note as a pop-up. Each note below is shown in the pop-up and on the page in the same way.
11
+
10
12
  The Notes view shows one meeting, top to bottom. A bar at the top shows whether notes are being taken, with **Stop** or **Listen** and the **⋯** menu. Under it are the title, the day, the meeting, the initiative and the people. Then come the overview, the key points, the decisions, the next steps, the open questions and your own notes, each item with the meeting time it was said or written. The transcript folds away at the end. Notes are kept on this machine, outside every repository.
11
13
 
12
14
  ## Start a meeting
13
15
 
14
- 1. Press the **Notes** button in the top bar, or press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository. The **Notes** button shows a red dot when notes are being recorded.
15
- 2. Press **New meeting**. Type a title.
16
+ 1. Press **Record** in the top bar, or press **Notes** to see all notes (or press ⌘K, type **notes** and press Enter). The page is the same in every repository.
17
+ 2. Type a title. A note made with **Record** has none until you add one.
16
18
  3. Under **Your notes**, type a note and press Enter. Each note is its own line, with the meeting time you added it at. To remove a line, press the **x** beside it. Each line also shows in the transcript as **You, typed** and is labelled like a spoken line, so a decision or a next step you type goes into its section. Your notes steer the summary, and they save at once.
17
19
  4. Press **All notes** to open an earlier note. The **⋯** menu has Copy as Markdown, New meeting, the meeting and the initiative, Summary by agent, Mark private, Settings and **Delete note**.
18
20
 
@@ -125,7 +127,7 @@ To stop keeping audio, clear **Keep the audio for replay** in the settings. To d
125
127
 
126
128
  ## Capture highlights during the meeting
127
129
 
128
- While listening, press **Highlight** to capture the last 30 seconds of conversation. A pop-up shows the highlight's progress:
130
+ While listening, press the highlighter in the top bar (or in the note) to capture the last 30 seconds of conversation. A pop-up shows the highlight's progress:
129
131
 
130
132
  1. **Capturing**: the last 30 seconds are being recorded.
131
133
  2. **Listening**: the highlight listens forward. Choose how long: **30 seconds**, **1 minute** or **2 minutes**. The remaining time counts down.
@@ -165,7 +167,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
165
167
 
166
168
  When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
167
169
 
168
- To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The note stays small at the bottom right of the workspace, with a red dot, its title and its time, on every page. Press it to open the note as a pop-up: add your notes, read the key points, and press **Stop** when the meeting ends. The pop-up stays steady while it listens: new lines are added at the end, and your place in the pop-up, the line you are typing and its cursor do not move. If the transcript is scrolled to its end, it follows the new lines. Press **Minimize** or Esc to make it small again. **Open in Notes** in the **⋯** menu shows it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays at the bottom right after a reload. When it no longer listens, press the x beside it to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
170
+ To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The top bar shows the red dot and the clock on every page. Press the clock to open the note as a pop-up: add your notes, read the key points, and press **Stop** when the meeting ends. The pop-up stays steady while it listens: new lines are added at the end, and your place in the pop-up, the line you are typing and its cursor do not move. If the transcript is scrolled to its end, it follows the new lines. Press **Minimize** or Esc to make it small again. **Open in Notes** in the **⋯** menu shows it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays in the top bar after a reload. Press **Stop** in the note to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
169
171
 
170
172
  ## Dictate into your notes
171
173
 
@@ -109,9 +109,29 @@ function rowOf(n) {
109
109
  return { id: n.id, title: n.title || 'Untitled note', created_at: n.created_at, updated_at: n.updated_at, private: !!n.private,
110
110
  words: wordsOf(n.body), segments: (n.transcript || []).length, summary: !!(n.summary || (n.agent && n.agent.markdown)),
111
111
  meeting: n.meeting || null, initiative: n.initiative || null, ws: n.ws || null,
112
+ // the meeting time so far (a paused note's clock) and how many statements differ from a repository file (the Notes menu)
113
+ meeting_ms: n.recorded_from ? meetingMs(n) : 0, conflicts: n.private ? 0 : conflictsOf(n).length,
112
114
  // a private note shows its title on the calendar and the initiative, never its summary
113
115
  summary_lines: n.private ? [] : summaryLines(n) };
114
116
  }
117
+ /** The statements of a note that a repository file says otherwise: transcript lines, the agent's bullets and the PM's
118
+ * highlights whose check verdict is 'differs'. → [{ text, doc, line, t, from }] */
119
+ function conflictsOf(n) {
120
+ const out = [];
121
+ const seen = new Set();
122
+ const add = (x, from) => {
123
+ const c = x && !x.dropped && x.check;
124
+ if (!c || c.verdict !== 'differs' || !c.doc) return;
125
+ const k = c.doc + ':' + (c.line || 0);
126
+ if (seen.has(k)) return;
127
+ seen.add(k);
128
+ out.push({ text: x.text, doc: c.doc, line: c.line || null, words: c.words || null, t: typeof x.t === 'number' ? x.t : null, from });
129
+ };
130
+ (n.highlights || []).forEach((x) => add(x, 'highlight'));
131
+ (n.transcript || []).forEach((x) => add(x, 'line'));
132
+ (n.bullets || []).forEach((x) => add(x, 'bullet'));
133
+ return out;
134
+ }
115
135
  /** Up to three lines of the note's summary: the agent's, else the local overview. Plain text. */
116
136
  function summaryLines(n) {
117
137
  if (n.agent && n.agent.markdown) {
@@ -1418,7 +1438,84 @@ function editBullet(dir, { id, bid, cat, drop, text, now = Date.now() } = {}) {
1418
1438
  return { ok: true, bullet: b, summary: note.summary || null };
1419
1439
  }
1420
1440
 
1421
- module.exports = { setFinal, setFinalState, renameFinalSpeaker, finalSpeaker, transcriptLines, cleanNotes, notesMarkdown, groundQuote, parseTime, NOTES_SHAPE,
1441
+ /* ── highlights and the PM's edits to the final notes (Notes menu) ──────────────────────────────── */
1442
+ /* A HIGHLIGHT is the PM's mark on a stretch of the meeting: the last 30 seconds before the press and the time it listened
1443
+ * after (30 s, 1 or 2 min). It is made from the transcript lines said in that window, so it is only as good as what was
1444
+ * transcribed: its text is those lines, its label is the strongest label among them (decision, then action, question,
1445
+ * fact, statement), and the caller adds the repository check. A window with no line makes no highlight. */
1446
+ const MAX_HIGHLIGHTS = 50;
1447
+ const HL_ORDER = ['decision', 'action', 'question', 'fact', 'statement'];
1448
+ const HL_WORD = { decision: 'Decision', action: 'Next step', question: 'Question', fact: 'Fact', statement: 'Statement' };
1449
+ function addHighlight(dir, { id, from, to, now = Date.now() } = {}) {
1450
+ const note = load(dir, id);
1451
+ if (!note) return err(404, 'no such note');
1452
+ if (note.private) return err(409, 'this note is private, so it keeps no highlights');
1453
+ const a = Number(from); const b = Number(to);
1454
+ if (!Number.isFinite(a) || !Number.isFinite(b) || b <= a || b - a > 5 * 60 * 1000) return err(400, 'from and to must be meeting times in ms, at most 5 minutes apart');
1455
+ const lines = (note.transcript || []).map((x, index) => ({ ...x, index })).filter((x) => x && !x.dropped && x.text && typeof x.t === 'number' && x.t >= Math.max(0, a) && x.t <= b);
1456
+ if (!lines.length) return err(422, 'nothing was transcribed in that window');
1457
+ const cats = lines.map((x) => (isFact(x) ? 'fact' : x.cat));
1458
+ const cat = HL_ORDER.find((c) => cats.includes(c)) || 'statement';
1459
+ const differs = lines.find((x) => x.check && x.check.verdict === 'differs');
1460
+ const h = { id: 'h-' + crypto.randomBytes(4).toString('hex'), at: iso(now), from: Math.max(0, Math.round(a)), to: Math.round(b), t: Math.max(0, Math.round(a)),
1461
+ text: clip(lines.map((x) => x.text).join(' '), 1200), cat, label: HL_WORD[cat], lines: lines.map((x) => x.index),
1462
+ check: differs ? { ...differs.check } : null };
1463
+ if (!Array.isArray(note.highlights)) note.highlights = [];
1464
+ note.highlights.push(h);
1465
+ if (note.highlights.length > MAX_HIGHLIGHTS) note.highlights = note.highlights.slice(-MAX_HIGHLIGHTS);
1466
+ note.updated_at = iso(now);
1467
+ store(dir, note);
1468
+ return { ok: true, highlight: h };
1469
+ }
1470
+ /** The repository check (or Jev's label) of one highlight, written after it was made. */
1471
+ function updateHighlight(dir, { id, hid, patch, now = Date.now() } = {}) {
1472
+ const note = load(dir, id);
1473
+ if (!note) return err(404, 'no such note');
1474
+ const h = (note.highlights || []).find((x) => x.id === hid);
1475
+ if (!h) return err(404, 'no such highlight');
1476
+ if (patch && patch.check !== undefined) h.check = patch.check;
1477
+ if (patch && patch.cat && HL_WORD[patch.cat]) { h.cat = patch.cat; h.label = HL_WORD[patch.cat]; }
1478
+ note.updated_at = iso(now);
1479
+ store(dir, note);
1480
+ return { ok: true, highlight: h };
1481
+ }
1482
+ function removeHighlight(dir, { id, hid, now = Date.now() } = {}) {
1483
+ const note = load(dir, id);
1484
+ if (!note) return err(404, 'no such note');
1485
+ const before = (note.highlights || []).length;
1486
+ note.highlights = (note.highlights || []).filter((x) => x.id !== hid);
1487
+ if (note.highlights.length === before) return err(404, 'no such highlight');
1488
+ note.updated_at = iso(now);
1489
+ store(dir, note);
1490
+ return { ok: true };
1491
+ }
1492
+ /** The PM's correction to the final notes: the overview, or the text of one decision, next step or open question. The
1493
+ * first words the agent wrote are kept as `orig_text`. */
1494
+ const FINAL_SECS = { decision: 'decisions', action: 'actions', question: 'questions' };
1495
+ function editFinal(dir, { id, sec, i, text, now = Date.now() } = {}) {
1496
+ const note = load(dir, id);
1497
+ if (!note) return err(404, 'no such note');
1498
+ const f = note.final_notes;
1499
+ if (!f) return err(409, 'this note has no final notes yet');
1500
+ const t = clip(text, sec === 'overview' ? 2000 : 400);
1501
+ if (!t) return err(400, 'the text is empty');
1502
+ if (sec === 'overview') {
1503
+ if (f.overview_orig === undefined && f.overview !== t) f.overview_orig = f.overview || '';
1504
+ f.overview = t;
1505
+ } else {
1506
+ const list = FINAL_SECS[sec] ? f[FINAL_SECS[sec]] : null;
1507
+ const x = Array.isArray(list) ? list[Number(i)] : null;
1508
+ if (!x) return err(404, 'no such item');
1509
+ if (x.orig_text === undefined && x.text !== t) x.orig_text = x.text;
1510
+ x.text = t;
1511
+ }
1512
+ keepVersion(note, now);
1513
+ note.updated_at = iso(now);
1514
+ store(dir, note);
1515
+ return { ok: true, final_notes: f };
1516
+ }
1517
+
1518
+ module.exports = { addHighlight, updateHighlight, removeHighlight, editFinal, conflictsOf, setFinal, setFinalState, renameFinalSpeaker, finalSpeaker, transcriptLines, cleanNotes, notesMarkdown, groundQuote, parseTime, NOTES_SHAPE,
1422
1519
  wisprHandoff, wisprPrompt, setWispr, importWispr, notesOutOf, wisprOutOf, list, read, create, link, findByEvent, sameEvent, cleanMeeting, summaryLines, save, remove, record, pause, meetingMs, appendTranscript, updateSegment, setLabel, editLine, versionsOf, renameSpeaker, speakerOf, categoryOf, isFact, CATS, PM_CATS, VERSION_FOLD_MS, setPrivate, dictation, cleanSentences, spokenPunctuation,
1423
1520
  summarize, localSummary, agentHandoff, agentStarted, toMarkdown, promptFor, fmtTime, ownerOf, dueOf, ID_RE, notesDir, handoffOf, agentOutOf,
1424
1521
  meetsBar, checkable, vocabOf, termsIn, namesIn, SECTION_CAP, JOIN_GAP_MS,
@@ -39,6 +39,10 @@
39
39
  * without `ok`, or { started: false, reason } when no agent was asked
40
40
  * POST /api/notes/summarize-agent { id, ws? } → { ok, started, session, handoff, out, withheld, reason }
41
41
  * POST /api/notes/delete { id } → { ok }
42
+ * POST /api/notes/highlight { id, from, to } → { ok, highlight, checked, jev } the PM's highlight: the transcript lines said
43
+ * between the meeting times from and to, labelled, then checked against the repository
44
+ * POST /api/notes/highlight-remove { id, hid } → { ok }
45
+ * POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text } → { ok, final_notes } the PM's correction
42
46
  *
43
47
  * Notes are the PM's own and live in the per-user runtime directory (ctx.runtimeDir), never in a repository, so no route here
44
48
  * writes into a registered workspace. Every POST goes through pm-guard `requirePagePress` (a body that is not an object or carries
@@ -75,7 +79,7 @@ const VOCAB_TTL_MS = 60 * 1000;
75
79
  const prefixes = ['/api/notes'];
76
80
  const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
77
81
  '/api/notes/private', '/api/notes/pause', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
78
- '/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet', '/api/notes/create-task']);
82
+ '/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet', '/api/notes/highlight', '/api/notes/highlight-remove', '/api/notes/final-edit']);
79
83
 
80
84
  /* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
81
85
  const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
@@ -690,17 +694,34 @@ async function handle(req, res, url, ctx) {
690
694
  return done(r);
691
695
  }
692
696
  if (p === '/api/notes/delete') { disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); const r = notes.remove(dir, id); if (r.ok) notesAudio.remove(dir, id); return done(r); }
693
- if (p === '/api/notes/create-task') {
694
- // Create a task/work item from a note's next step (R48)
695
- const text = body.text || '';
696
- const owner = body.owner || null;
697
- const due = body.due || null;
698
- if (!text) { ctx.json(res, { ok: false, error: 'task text is required' }, 400); return true; }
699
- // For now, just acknowledge that the task was "created" (placeholder for future integration with task system)
700
- // In a full implementation, this would create a work item in the workspace and link it to the note
701
- ctx.json(res, { ok: true, taskId: `task-${id}-${Date.now()}`, text, owner, due, note_id: id });
702
- return true;
697
+ if (p === '/api/notes/highlight') {
698
+ // the PM's highlight: the transcript of the window, then Jev's label and the repository check of its text
699
+ const r = notes.addHighlight(dir, { id, from: body.from, to: body.to });
700
+ if (!r.ok) return done(r);
701
+ const note = stillOpen(dir, id);
702
+ const w = note && note.ws ? ctx.getWs(note.ws) : null;
703
+ const g = gatesFor(ctx, w ? w.root : null);
704
+ let h = r.highlight;
705
+ if (g.ask) {
706
+ const l = await jevNotes.label({ ask: g.ask, key: g.key, askOpts: g.askOpts, text: h.text, root: g.root });
707
+ if (l.cat && notes.meetsBar(l.cat, h.text)) { const u = notes.updateHighlight(dir, { id, hid: h.id, patch: { cat: l.cat } }); if (u.ok) h = u.highlight; }
708
+ }
709
+ if (g.root && !(h.check && h.check.verdict === 'differs')) {
710
+ const pass = notesRepo.closest(g.root, h.text, { exclude: g.exclude });
711
+ if (pass) {
712
+ let check = { doc: pass.path, line: pass.line, verdict: 'related', by: 'local' };
713
+ if (g.ask && g.check) {
714
+ const c = await jevNotes.compare({ ask: g.ask, key: g.key, askOpts: g.askOpts, text: h.text, passage: pass, root: g.root });
715
+ if (c.verdict) check = { doc: pass.path, line: pass.line, verdict: c.verdict, words: c.words || null, by: 'jev' };
716
+ }
717
+ const u = notes.updateHighlight(dir, { id, hid: h.id, patch: { check } });
718
+ if (u.ok) h = u.highlight;
719
+ }
720
+ }
721
+ return done({ ok: true, highlight: h, checked: !!g.root, jev: !!g.ask });
703
722
  }
723
+ if (p === '/api/notes/highlight-remove') return done(notes.removeHighlight(dir, { id, hid: body.hid }));
724
+ if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text }));
704
725
  // summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
705
726
  let w = null;
706
727
  if (body.ws != null) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.107.0",
3
+ "version": "0.109.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.107.0",
9
+ "version": "0.109.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.107.0",
3
+ "version": "0.109.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -406,7 +406,7 @@ function render() {
406
406
 
407
407
  const FRAME = !FIXTURE && !LEGACY_LAYOUT_FLAG;
408
408
  const FRAME_KEY = 'pmos.workspace.pm.frame';
409
- const FRAME_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'people', 'settings']);
409
+ const FRAME_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'notes', 'people', 'settings']);
410
410
 
411
411
  function readFrame() {
412
412
  if (!FRAME) return {};
@@ -696,7 +696,7 @@ function pressIn(r, key) {
696
696
  if (b && !b.disabled) { b.click(); return true; }
697
697
  return false;
698
698
  }
699
- const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs', p: 'people', w: 'mywork' };
699
+ const G_KEYS = { t: 'today', i: 'initiatives', b: 'board', r: 'reports', d: 'docs', n: 'notes', p: 'people', w: 'mywork' };
700
700
 
701
701
  function frameKey(e) {
702
702
  const mod = e.metaKey || e.ctrlKey;