@forwardimpact/outpost 3.5.1 → 3.7.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": "@forwardimpact/outpost",
3
- "version": "3.5.1",
3
+ "version": "3.7.0",
4
4
  "description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
5
5
  "homepage": "https://www.forwardimpact.team",
6
6
  "repository": {
package/src/kb-manager.js CHANGED
@@ -189,7 +189,7 @@ export class KBManager {
189
189
  await this.copyBundledFiles(templateDir, dest);
190
190
 
191
191
  this.#logger.info(
192
- `Knowledge base initialized at ${dest}\n\nNext steps:\n 1. cd ${dest} && npx apm install\n 2. claude\n 3. Run the identify-user skill to populate your identity`,
192
+ `Knowledge base initialized at ${dest}\n\nNext steps:\n 1. cd ${dest} && npx apm install\n 2. claude\n 3. Run the person-identify skill to populate your identity`,
193
193
  );
194
194
  return { ok: true, value: { dest } };
195
195
  }
package/src/outpost.js CHANGED
@@ -26,7 +26,7 @@ import { StateManager } from "./state-manager.js";
26
26
  import { AgentRunner } from "./agent-runner.js";
27
27
  import { Scheduler, formatLocalTime } from "./scheduler.js";
28
28
  import { KBManager } from "./kb-manager.js";
29
- import { SocketServer, requestShutdown } from "./socket-server.js";
29
+ import { SocketServer, requestShutdown, requestWake } from "./socket-server.js";
30
30
  import {
31
31
  readPosture,
32
32
  writePosture,
@@ -436,17 +436,28 @@ export async function run(runtime, version) {
436
436
  cli.usageError("missing required argument <agent>");
437
437
  return 2;
438
438
  }
439
- const config = await loadConfig();
440
- const state = await stateManager.load();
441
- const agent = config.agents[args[0]];
442
- if (!agent) {
439
+ // Always route the wake through the running daemon. The daemon is the
440
+ // only spawn site that descends from fit-outpost.app, so a `claude`
441
+ // spawned there inherits the app as its TCC responsible process and a
442
+ // single grant to the app covers it. Spawning from this CLI process
443
+ // would attribute the access to the terminal instead, breaking the
444
+ // single-grant model. If no daemon is running there is nowhere to wake
445
+ // with correct attribution, so this errors rather than spawning locally.
446
+ const result = await requestWake(SOCKET_PATH, args[0], runtime);
447
+ if (result.ok) {
448
+ log(`Wake dispatched to daemon for "${args[0]}".`);
449
+ return 0;
450
+ }
451
+ if (result.reason === "not-running") {
443
452
  cli.error(
444
- `agent "${args[0]}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
453
+ "daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before waking an agent.",
445
454
  );
446
- return 1;
455
+ } else if (result.reason === "timeout") {
456
+ cli.error("daemon did not respond to the wake request.");
457
+ } else {
458
+ cli.error(result.message);
447
459
  }
448
- await agentRunner.wake(args[0], agent, state, config.env);
449
- return 0;
460
+ return 1;
450
461
  },
451
462
  init: async () => {
452
463
  if (!args[0]) {
@@ -323,6 +323,71 @@ export class SocketServer {
323
323
  }
324
324
  }
325
325
 
326
+ /**
327
+ * Connect to the running daemon and ask it to wake an agent.
328
+ *
329
+ * The wake runs inside the daemon process, which is the only spawn site that
330
+ * descends from fit-outpost.app — so the spawned `claude` inherits the app as
331
+ * its TCC responsible process. Routing every wake through the daemon is what
332
+ * keeps the single-grant model intact; a wake spawned from this CLI process
333
+ * would be attributed to the terminal instead. The daemon acknowledges
334
+ * (`ack`) once it has accepted the request and then runs the wake
335
+ * asynchronously, so this resolves on the ack rather than on completion.
336
+ *
337
+ * @param {string} socketPath
338
+ * @param {string} agent - Agent name to wake.
339
+ * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
340
+ * Injected runtime bag (uses `fsSync` and `clock`).
341
+ * @returns {Promise<{ ok: boolean, reason?: "not-running"|"timeout"|"error", message?: string }>}
342
+ */
343
+ export async function requestWake(socketPath, agent, runtime) {
344
+ if (!runtime?.fsSync) throw new Error("runtime.fsSync is required");
345
+ if (!runtime?.clock) throw new Error("runtime.clock is required");
346
+ if (!runtime.fsSync.existsSync(socketPath)) {
347
+ return { ok: false, reason: "not-running" };
348
+ }
349
+
350
+ return new Promise((resolve) => {
351
+ const timeout = runtime.clock.setTimeout(() => {
352
+ socket.destroy();
353
+ resolve({ ok: false, reason: "timeout" });
354
+ }, 5000);
355
+
356
+ const socket = createConnection(socketPath, () => {
357
+ socket.write(JSON.stringify({ type: "wake", agent }) + "\n");
358
+ });
359
+
360
+ let buffer = "";
361
+ socket.on("data", (data) => {
362
+ buffer += data.toString();
363
+ const idx = buffer.indexOf("\n");
364
+ if (idx === -1) return;
365
+ runtime.clock.clearTimeout(timeout);
366
+ let msg = null;
367
+ try {
368
+ msg = JSON.parse(buffer.slice(0, idx));
369
+ } catch {}
370
+ socket.destroy();
371
+ if (msg && msg.type === "ack" && msg.command === "wake") {
372
+ resolve({ ok: true });
373
+ } else {
374
+ resolve({
375
+ ok: false,
376
+ reason: "error",
377
+ message: msg?.message || "daemon rejected wake request",
378
+ });
379
+ }
380
+ });
381
+
382
+ // A stale socket file (daemon crashed) refuses the connection; treat it
383
+ // the same as a missing daemon.
384
+ socket.on("error", () => {
385
+ runtime.clock.clearTimeout(timeout);
386
+ resolve({ ok: false, reason: "not-running" });
387
+ });
388
+ });
389
+ }
390
+
326
391
  /**
327
392
  * Connect to the daemon socket and request graceful shutdown.
328
393
  * @param {string} socketPath
@@ -20,7 +20,7 @@ same way `extract-entities` processes emails and calendar events.
20
20
 
21
21
  - Anarlog installed; sessions at
22
22
  `~/Library/Application Support/anarlog/sessions/`.
23
- - User identity from running the `identify-user` skill, which writes
23
+ - User identity from running the `person-identify` skill, which writes
24
24
  `~/.cache/fit/outpost/state/identity.md`.
25
25
 
26
26
  ## Inputs
@@ -31,7 +31,7 @@ same way `extract-entities` processes emails and calendar events.
31
31
  - `~/.cache/fit/outpost/state/graph_processed` — processed-file index (TSV,
32
32
  shared with `extract-entities`).
33
33
  - `~/.cache/fit/outpost/state/identity.md` — user identity for self-exclusion
34
- (written by the `identify-user` skill).
34
+ (written by the `person-identify` skill).
35
35
 
36
36
  ## Outputs
37
37
 
@@ -62,7 +62,7 @@ same way `extract-entities` processes emails and calendar events.
62
62
  ### 0. Set up
63
63
 
64
64
  Read the user's identity from `~/.cache/fit/outpost/state/identity.md` (run the
65
- `identify-user` skill first if it is missing or stale). Scan unprocessed
65
+ `person-identify` skill first if it is missing or stale). Scan unprocessed
66
66
  sessions:
67
67
 
68
68
  ```bash
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: changelog
3
+ description: Record the knowledge-graph changes made during the current session into a single shared Knowledge/CHANGELOG.md so the team can see what changed and why. Use when the user asks to log, record, or write up the changes they just made to the knowledge base — typically at the end of a session of edits.
4
+ ---
5
+
6
+ # Changelog
7
+
8
+ Record the changes made to the **knowledge graph** (`Knowledge/`) during the
9
+ current working session in one shared `Knowledge/CHANGELOG.md`, newest first, so
10
+ teammates syncing the same filesystem can see what changed and why.
11
+
12
+ This tracks **graph content** — notes under `Knowledge/People/`,
13
+ `Organizations/`, `Projects/`, `Topics/`, `Candidates/`, `Priorities/`, and the
14
+ other subdirectories. It does **not** track changes to instructions (`CLAUDE.md`,
15
+ agents, skills) — that is the `upstream-instructions` skill's job.
16
+
17
+ ## Trigger
18
+
19
+ - The user asks to write, update, or record a changelog after editing the KB.
20
+ - A session has added, modified, removed, or renamed notes in `Knowledge/` and
21
+ the user wants those changes documented for the team.
22
+
23
+ ## Inputs
24
+
25
+ - **The edits made in the current session** — the source of truth. The KB lives
26
+ on a synced filesystem and is not version-controlled, so there is no commit
27
+ history to diff. Recall every note created, edited, removed, or renamed under
28
+ `Knowledge/` during this conversation.
29
+ - `~/.cache/fit/outpost/state/identity.md` — the current user's identity. Its
30
+ **Name** is the author recorded on each entry. The KB is shared, so every
31
+ change must be attributed to the team member who made it. Resolve `~` to
32
+ `$HOME` before reading.
33
+ - `Knowledge/CHANGELOG.md` — the existing changelog, to see what's already
34
+ recorded and avoid duplicates.
35
+
36
+ ## Outputs
37
+
38
+ - `Knowledge/CHANGELOG.md` — a **single** reverse-chronological changelog
39
+ covering all graph subdirectories. No per-folder or per-note changelogs.
40
+
41
+ ## Ethics
42
+
43
+ `Knowledge/` is shared with the team. Every entry obeys the KB's integrity rules:
44
+ objective and factual, work-relevant, no personal judgments. Assume the person a
45
+ note is about will read its changelog entry. Describe **what changed in the
46
+ graph**, not opinions about the people in it.
47
+
48
+ <do_confirm_checklist goal="Verify the changelog is accurate and shareable">
49
+
50
+ - [ ] Exactly one `Knowledge/CHANGELOG.md`; no stray per-folder changelogs.
51
+ - [ ] Every entry names its **Scope** — the specific note(s) or folder(s) touched,
52
+ by full path.
53
+ - [ ] Each entry has **Who** (author, from identity), **What**, and **Why**.
54
+ - [ ] Descriptions are specific enough to be useful (not "updated some notes").
55
+ - [ ] Dates are the date the change was actually made, not guessed.
56
+ - [ ] No duplicate entries for a change already in the changelog.
57
+ - [ ] Entries are factual and would be fine for the subject to read.
58
+
59
+ </do_confirm_checklist>
60
+
61
+ ## Procedure
62
+
63
+ ### 1. Find what's already recorded, and who you are
64
+
65
+ ```bash
66
+ head -30 Knowledge/CHANGELOG.md 2>/dev/null # newest date already logged, if any
67
+ cat "$HOME/.cache/fit/outpost/state/identity.md" # Name → the author for this session's entries
68
+ ```
69
+
70
+ If `identity.md` is missing or stale, run the `person-identify` skill to refresh
71
+ it before logging — don't guess the author.
72
+
73
+ ### 2. Reconstruct this session's changes
74
+
75
+ Recall every change made to `Knowledge/` during the current conversation:
76
+ creations, edits, removals, renames. Group them by note. If you are unsure a
77
+ change landed, confirm it before logging:
78
+
79
+ ```bash
80
+ rg --files Knowledge/ | rg "<note name>" # confirm a note exists
81
+ cat "Knowledge/People/Doe, Jane.md" # confirm content landed
82
+ ```
83
+
84
+ Optionally surface anything edited recently that you might have missed:
85
+
86
+ ```bash
87
+ find Knowledge -name '*.md' -newermt '-1 day' -not -path '*/.*'
88
+ ```
89
+
90
+ Use `Knowledge/CHANGELOG.md` only to avoid duplicating an entry already there.
91
+
92
+ ### 3. Classify each change
93
+
94
+ | Type | Description |
95
+ | ---------- | ------------------------------------ |
96
+ | `added` | New note created |
97
+ | `modified` | Existing note updated |
98
+ | `removed` | Note deleted |
99
+ | `renamed` | Note renamed or moved |
100
+
101
+ Related changes that form one logical edit (e.g. a new project note plus the
102
+ backlinks added to the people it involves) are recorded as **one entry** whose
103
+ Scope lists every note touched.
104
+
105
+ ### 4. Write the changelog
106
+
107
+ Create or update `Knowledge/CHANGELOG.md` (newest first). Group entries under one
108
+ heading per day; one bullet per logical change:
109
+
110
+ ```markdown
111
+ # Knowledge Changelog
112
+
113
+ Changes to the shared knowledge graph, newest first. Maintained by hand at the
114
+ end of editing sessions via the `changelog` skill. The KB is not
115
+ version-controlled, so this is the record of what changed and why.
116
+
117
+ ## <YYYY-MM-DD>
118
+
119
+ - **<added | modified | removed | renamed>** — _<Scope: full path(s)>_ · <Who>
120
+ **What:** <one-line summary of the change.>
121
+ **Why:** <the reason — the email, meeting, or request that prompted it.>
122
+ ```
123
+
124
+ Use the real date the change was made (today's date is in context) and the
125
+ **Name** from `identity.md` as `<Who>`. Because the file is shared, the author
126
+ travels on each entry — not just the day's heading — so a day with edits from
127
+ more than one teammate stays unambiguous. Keep each entry to its What and Why —
128
+ this is a ledger, not a diff.
129
+
130
+ ## Notes
131
+
132
+ - This skill **documents only** — it records changes already made; it does not
133
+ make or undo edits.
134
+ - One `Knowledge/CHANGELOG.md` at the graph root, never per-folder.
135
+ - For changes to instructions (`CLAUDE.md`, agents, skills), use
136
+ `upstream-instructions` instead.
@@ -67,9 +67,49 @@ Defaults: input = `/tmp/outpost-presentation.html`, output =
67
67
  5. **No footers or headers** — No fixed/absolute positioned footer/header
68
68
  elements
69
69
 
70
+ ## Interactive HTML Decks — Navigation & Event Standards
71
+
72
+ When the deck is delivered as a **standalone interactive HTML file** (animated /
73
+ navigable in the browser) rather than a static PDF, keep input handling
74
+ deliberately minimal. Rich event handling fights with two things the user needs:
75
+ selecting/copying text on a slide, and typing into overlay tools (e.g. the
76
+ `slide-annotator.js` review overlay).
77
+
78
+ **Required:**
79
+
80
+ 1. **Arrow keys are the only navigation.** `→` / `ArrowRight` = next,
81
+ `←` / `ArrowLeft` = previous. Nothing else advances slides.
82
+ 2. **No click-to-advance.** Do NOT add click regions on the slide/stage that
83
+ navigate (e.g. "click left/right third"). They fire on the mouse-up that ends
84
+ a text-selection drag and jump the slide unexpectedly.
85
+ 3. **No spacebar, PageUp/PageDown, or other global key bindings.** Space conflicts
86
+ with typing in overlay inputs; the rest are redundant and surprising.
87
+ 4. **A progress indicator may be clickable**, but it must live in the footer/chrome
88
+ and never overlap slide content.
89
+ 5. **Expose `window.deckGoto(index)`** (0-based) right after the slide-show
90
+ function, so review/overlay tools can jump to a slide without simulating clicks
91
+ or keys:
92
+
93
+ function go(n) { /* ...show slide n... */ }
94
+ window.deckGoto = go;
95
+
96
+ 6. **Keep the hint honest** — the on-screen nav hint should read `← → to navigate`
97
+ (don't advertise click/space).
98
+ 7. **Use stable structural hooks.** Make each slide one element with class
99
+ `.slide`, and put the slide-number label (if any) in a `.slide-num` element.
100
+ The review overlay defaults to these selectors to detect and index slides.
101
+
102
+ These rules keep decks compatible with the **`deck-review`** skill, which installs
103
+ the `slide-annotator.js` review overlay (highlight text on a slide → sidecar JSON
104
+ of feedback that an agent acts on). After producing an interactive HTML deck, you
105
+ can offer to run `deck-review` to make it reviewable; see that skill for the
106
+ install steps and the sidecar JSON schema.
107
+
70
108
  ## Constraints
71
109
 
72
110
  - Always use the knowledge base for context when available
73
111
  - Output to `~/Desktop/presentation.pdf` unless user specifies otherwise
74
112
  - Keep slides clean and readable — max 5-6 bullet points per slide
75
113
  - Use consistent styling throughout
114
+ - For interactive HTML decks, follow the navigation & event standards above
115
+ (arrow-keys-only; no click-to-advance or spacebar)
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: deck-review
3
+ description: Add a lightweight text-highlight review overlay to an HTML deck. Lets you highlight text on slides and capture feedback as a sidecar JSON (with source line/column + context) that an agent can act on in small iterations. Use when the user asks to add review/annotation/highlight/comment capability to a deck, make a deck "reviewable", or wants to mark up slides for revision. Pairs with the deck-create skill.
4
+ compatibility: Standalone HTML deck opened in a Chromium-based browser (Chrome/Edge). No build step, no server, no dependencies.
5
+ ---
6
+
7
+ # Add a Review Overlay to a Deck
8
+
9
+ Install the self-contained `slide-annotator.js` overlay onto an HTML deck so the
10
+ user can **highlight text on a slide and save the feedback as a sidecar JSON**.
11
+ Each annotation carries a robust anchor (exact text + surrounding context + slide)
12
+ and, once the folder is connected, the resolved **source line, column and context
13
+ lines** — so an agent can locate and edit the exact text in small iterations.
14
+
15
+ This is the companion to **`deck-create`**: decks produced by `deck-create`
16
+ already follow the navigation/structure standards this overlay needs, and this
17
+ overlay is designed to drop onto them with one script tag.
18
+
19
+ ## Trigger
20
+
21
+ Run when the user asks to add review / annotation / highlight / comment / markup
22
+ capability to a deck, "make this deck reviewable", or to set up a feedback loop on
23
+ slides.
24
+
25
+ ## Inputs
26
+
27
+ - Path to the target deck `.html` file (ask, or default to the most recently
28
+ edited `*.html` in `Drafts/`).
29
+ - The bundled tool at `assets/slide-annotator.js` (this skill's own copy is the
30
+ source of truth — edit it here, then re-install to update decks).
31
+
32
+ ## Outputs
33
+
34
+ - `slide-annotator.js` copied next to the deck.
35
+ - One `<script>` tag injected into the deck before `</body>`.
36
+ - At review time, a sidecar `‹deck›.annotations.json` written next to the deck.
37
+
38
+ ---
39
+
40
+ ## Install steps
41
+
42
+ 1. **Resolve the deck path** (absolute). Confirm it is an HTML deck, not a PDF.
43
+
44
+ 2. **Check compatibility** (see *Compatibility contract* below). The two things
45
+ that matter:
46
+ - **Slide selector** — each slide is one element with a stable class
47
+ (default `.slide`). If the deck uses a different class, note it for step 4.
48
+ - **Navigation hook** — the deck exposes `window.deckGoto(index)` (0-based).
49
+ If it has a slideshow function (e.g. `go(n)`) but no hook, add one line right
50
+ after it: `window.deckGoto = go;`. Without it the overlay still works (the
51
+ panel's *Go* button falls back to `scrollIntoView`), but it can't jump to a
52
+ hidden slide precisely.
53
+ - Optionally a slide-number label element (default `.slide-num`) for nicer
54
+ labels in the panel — purely cosmetic.
55
+
56
+ 3. **Install the tool**: copy this skill's `assets/slide-annotator.js` into the
57
+ **same directory as the deck**. Resolve `~` to `$HOME`; pass the Write/copy a
58
+ full path.
59
+
60
+ 4. **Inject the script tag** immediately before `</body>` (idempotent — skip if a
61
+ `slide-annotator` script tag is already present):
62
+
63
+ ```html
64
+ <!-- Review overlay: highlight text on a slide → sidecar JSON. Self-contained, optional. -->
65
+ <script src="slide-annotator.js" defer
66
+ data-slide-selector=".slide"
67
+ data-label-selector=".slide-num"></script>
68
+ ```
69
+
70
+ Set `data-slide-selector` / `data-label-selector` to match the deck if it
71
+ differs from the defaults. If there are no slide elements at all, the tool
72
+ treats the whole `<body>` as one container.
73
+
74
+ 5. **Tell the user how to use it** (see *Using the overlay*). Do **not** add any
75
+ other dependency or framework — the tool is plain JS and must stay that way.
76
+
77
+ ## Compatibility contract (must match `deck-create`)
78
+
79
+ The overlay relies only on these conventions, which `deck-create` decks already
80
+ follow:
81
+
82
+ | Convention | Default | Why the overlay needs it |
83
+ |---|---|---|
84
+ | One element per slide with a stable class | `.slide` | locate which slide a highlight is on; index slides |
85
+ | Slide-number label element (optional) | `.slide-num` | human-friendly panel labels |
86
+ | Navigation hook | `window.deckGoto(index)` (0-based) | panel "Go" jumps to the right slide |
87
+ | Arrow-keys-only navigation, **no** click-to-advance / spacebar | — | text selection + typing in the overlay must not move slides |
88
+
89
+ If a deck violates the last row (has click-to-advance), the overlay's click guard
90
+ only suppresses the click that ends a text-selection drag, so it degrades
91
+ gracefully — but the correct fix is to make the deck arrow-keys-only per
92
+ `deck-create`'s *Navigation & Event Standards*.
93
+
94
+ ## Using the overlay (tell the user)
95
+
96
+ 1. Open the deck in Chrome and click **✎ Review** (bottom-left).
97
+ 2. **Select text** on a slide → a popover lets you add an optional note → **Add**.
98
+ The highlight appears and **autosaves to `localStorage`** immediately.
99
+ 3. Click **Connect folder** once and pick the deck's folder. From then on **Save**
100
+ writes a real `‹deck›.annotations.json` next to the deck, and the tool reads the
101
+ deck's own source to fill in **source line / column / context** for each
102
+ highlight. (If the browser blocks folder access on `file://`, **Save**
103
+ downloads the JSON instead — move it next to the deck.)
104
+ 4. Navigation while reviewing is the deck's normal **← / →** (the overlay's own
105
+ keystrokes never leak to the deck).
106
+
107
+ ## Acting on the feedback (the review loop)
108
+
109
+ When the user says "work the annotations":
110
+
111
+ 1. Read `‹deck›.annotations.json` next to the deck.
112
+ 2. For each `status: "open"` annotation, locate the text in the deck source:
113
+ - Prefer `source.line` / `source.column` when present.
114
+ - Otherwise search the source for `quote` (disambiguate with `prefix` /
115
+ `suffix`, scoped to `slideId`). The quote is the underlying DOM text, so it
116
+ matches the source even across inline tags / entities.
117
+ 3. Make the edit, honoring the user's `note`.
118
+ 4. Optionally set the annotation's `status` to `"done"` in the JSON so the panel
119
+ shows it resolved.
120
+ 5. Re-render / re-screenshot to verify, then report what changed per annotation.
121
+
122
+ ### Sidecar JSON schema
123
+
124
+ ```jsonc
125
+ {
126
+ "version": 1, "tool": "slide-annotator",
127
+ "target": "‹deck›.html", "updatedAt": "‹iso›",
128
+ "annotations": [{
129
+ "id", "createdAt", "status": "open" | "done", "note",
130
+ "slideId", "slideIndex", "slideLabel", "slideTitle",
131
+ "quote", // exact highlighted text (the anchor)
132
+ "prefix", "suffix", // ~60 rendered chars either side
133
+ "renderedStart", "renderedEnd",// char offsets within the slide's text
134
+ "domPath", // CSS-ish path to the containing element
135
+ "source": { // best-effort; null until folder connected
136
+ "file", "line", "column",
137
+ "match": "exact" | "normalized" | "none",
138
+ "contextBefore": [".."], "contextLine": "..", "contextAfter": [".."]
139
+ }
140
+ }]
141
+ }
142
+ ```
143
+
144
+ ## Removing the overlay (for final delivery)
145
+
146
+ To hand off a clean presentation, delete the injected `<script src="slide-annotator.js" …>`
147
+ line and the `slide-annotator.js` file. Leaving the `window.deckGoto = go;` line in
148
+ the deck is harmless.
149
+
150
+ ## Constraints
151
+
152
+ - Keep `slide-annotator.js` **dependency-free and host-agnostic** — it must work
153
+ on any static HTML page, not just `deck-create` output.
154
+ - Edit the tool **here** (`assets/slide-annotator.js`) as the source of truth, then
155
+ re-install onto decks. Don't fork per-deck copies with divergent behavior.
156
+ - Never auto-send or upload annotations anywhere — the sidecar JSON stays local.