slidemind 0.3.19 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
package/AGENTS.md CHANGED
@@ -141,11 +141,12 @@ Teammates pick up the release with `npm install -g slidemind@latest` (or `slidem
141
141
 
142
142
  **The update nudge + background self-update** (`src/update-check.mjs`). The version floor above only fires once a CLI is too old to talk to the add-in at all — by then the user is hard blocked. The softer signal runs after **every** command: the npm dist-tag (`https://registry.npmjs.org/slidemind/latest`, override with `SLIDEMIND_REGISTRY_URL`) is fetched at most once per 6h per config dir (`SLIDEMIND_UPDATE_CHECK_INTERVAL_MS`), cached in `<configDir>/update-check.json`, and a newer version prints one line on **stderr** (never stdout — `--json` payloads stay clean). On the npm channel it also spawns a detached `npm install -g slidemind@latest` (log: `<configDir>/update.log`), so the next run is current without anyone typing anything.
143
143
 
144
- Four rules the implementation exists to keep:
144
+ Six rules the implementation exists to keep:
145
145
 
146
146
  - **After the command, never before.** A background global install replaces the package directory; running it mid-command could pull the rug out from under a dynamic import.
147
147
  - **Only the npm channel auto-updates.** `updateChannel()` returns `plugin` (only `/plugin` replaces that binary — a global install lands a second, unused copy), `npx` (nothing to update), `dev` (a checkout, i.e. `npm link` — an install would clobber the developer's symlink, so it is neither updated nor nudged; this is also why the test suite is silent by default, with `SLIDEMIND_UPDATE_CHECK=1` to force it on), or `npm`.
148
- - **One auto-update attempt per released version per interval.** A root-owned npm prefix makes the install fail forever; the cache records the attempt and the next run says the update did not take instead of respawning it every command.
148
+ - **A write it can't make, it doesn't attempt — it asks for escalated rights instead.** `globalInstallBlock()` (`src/install-channel.mjs`) pre-flights whether a global `npm install -g` could actually replace THIS binary by probe-writing the node_modules that holds slidemind. It can't under a **Codex sandbox** (seatbelt/landlock, detected by `CODEX_SANDBOX` — a confined session that is also the statworx default, where full access is forbidden org-wide) or behind a **root-owned prefix**. In a sandbox the config dir is unwritable too, so the one-attempt record below can never persist and the doomed install would respawn on *every* command while the nudge claimed "updating in the background" — the exact misfire this rung fixes. So when the install is unwritable the CLI does **not** spawn it: the notice (and every version-floor remediation, via `updateCliInstruction(block)`) names `slidemind update` **run with escalated permissions** — approved outside the sandbox, or under `sudo` for a root-owned prefix. The CLI can't escalate itself (any child inherits the sandbox); the message is aimed at the agent/user who can. Do not reintroduce an unconditional background spawn.
149
+ - **One auto-update attempt per released version per interval** (when the install IS writable). The cache records the attempt and the next run says the update did not take instead of respawning it every command.
149
150
  - **A version-floor hit forces the check.** When a command dies with `cli_update_required`, `bin/slidemind.mjs` passes `force: true`, which bypasses the 6h cache (it may predate the release that raised the floor) and the one-attempt guard, so the auto-update starts on the very command that hit the floor.
150
151
  - **Never fatal, never slow.** Fetch timeout 2s (`SLIDEMIND_UPDATE_CHECK_TIMEOUT_MS`), every failure swallowed. Opt out with `SLIDEMIND_NO_UPDATE_CHECK=1` (silence) or `SLIDEMIND_NO_AUTO_UPDATE=1` (nudge only); `CI` disables the auto-install by itself.
151
152
 
@@ -183,15 +184,25 @@ slidemind/
183
184
  ├── src/ implementation of the slidemind CLI
184
185
  │ ├── cli/ the subcommands, one module per job (each <= ~600 lines; ready.mjs is the one 783-line function): common.mjs (paths, fail, deck resolution, gates, lazy loaders) · help · designs · connection (the two readiness ladders) ·
185
186
  │ │ deck (plan/review/imagen/inspect-template-deck/build-pptx/view-draft) · concepts · workspace (new/adopt/push) · engine-helpers + search (search/ls) + slide-reuse (insert-slide/add-slide) + engine-results (view-slide/read-source/download-result) ·
186
- │ │ download-bundle · ready · diagnose · read · session (login/whoami/logout) · install (init/uninstall/update) · setup
187
+ │ │ download-bundle · ready · diagnose · read · record (create/open/sync/status — a shared JSON file, merged by the backend) · session (login/whoami/logout) · install (init/uninstall/update) · setup ·
188
+ │ │ workspace-ui (workspace start/status/stop/add/discover/open-remote/select/connect — NOT workspace.mjs, which is adoption) · tasks (list/request/get/wait)
189
+ │ ├── workspace/ the persistent local workspace (opt-in SLIDEMIND_WORKSPACE=1; docs/slidemind-cli-connect/21-persistent-workspace.md): lifecycle + daemon (detached process, descriptor, log rotation), registry + discovery (deck library), server + auth + routes + events (loopback API, bootstrap cookie, SSE), operations (per-deck mutation wrapper over acquireOwner), adapters (installed pack subprocess protocol), edit (local save generations), tasks + task-actions (durable review tasks), sync (record queue, refresh, conflicts, shared approval), remote-links + remote-status (link intake, destination), ui/ (vanilla browser page + plan-bridge)
187
190
  │ ├── bridge/ add-in local-bridge: client.mjs (the public surface; implementation split by job under client/: transport, presentations, pane-ops, brands, render, engine-routing, engine-search, engine-slides), bundle.mjs (materialize), build-and-push.mjs (server render; native-slide splicing), engine-download.mjs + recolor.mjs (image/icon fetch + recolour),
188
191
  │ │ setup-claim.mjs (decode/redeem the add-in's one-command claim — the only connect path) + http-json.mjs + config.mjs (per-backend sessions)
189
- │ ├── builders/ layout-jobs.mjs (a layout's job — cover/agenda/divider/quote/closing/content — from the bundle's PLACEHOLDERS `job` or the add-in's name table; the deck frame is composed from it, docs/slidemind-cli-connect/17-deck-frame.md), build.mjs (HTML preview; the page itself is preview.html), review.mjs (spatial preview-review gate + local server; stable per-deck port, graceful fallback, tab reuse across rounds), plan.mjs (plan sign-off gate) + plan-overlay.js + plan-md.mjs (markdown→HTML) + gate-server.mjs (shared gate primitives), resolve-skill.mjs (→ bundle), lint.mjs (runs the add-in's engine)
192
+ │ ├── builders/ layout-jobs.mjs (a layout's job — cover/agenda/divider/quote/closing/content — from the bundle's PLACEHOLDERS `job` or the add-in's name table; the deck frame is composed from it, docs/slidemind-cli-connect/17-deck-frame.md), build.mjs (HTML preview; the page itself is preview.html), review.mjs (spatial preview-review gate + local server; stable per-deck port, graceful fallback, tab reuse across rounds) + review-overlay.js, plan.mjs (plan sign-off gate) + plan-overlay.js (rail | stage | aside, laid out like the preview gate) + plan-md.mjs (markdown→HTML) + gate-server.mjs (shared gate primitives) + gate-session.js (the ledger/round machinery BOTH gates share) + gate-shell.js (the chrome BOTH gates share: brand bar, buttons, workspace strips, hand-off dialog, veil, rail card styling — never copy any of it into an overlay), resolve-skill.mjs (→ bundle), lint.mjs (runs the add-in's engine)
190
193
  │ ├── pptx/ host-free local-.pptx support (fflate-based): read-local.mjs (read a file's slides — output-identical to the host read), slide-swap.mjs (fold a picked slide into a deck as native .pptx — swaps it into the lowest slot the splicer reads, then prunes the package to that slide's dependency closure so a slice carries only what it needs), zip.mjs + ooxml.mjs (shared unzip/order helpers)
191
194
  │ ├── lib/ deck-workspace bookkeeping: adopt.mjs (rebuild a deck from a file's embedded source + convert its foreign slides through the add-in's convert-pptx route), conversions.mjs (the deck's `conversions/<stem>.json` identity+evidence records and the preserve source package push threads back), slide-guids.mjs, slide-order.mjs, reused-slides.mjs
192
195
  │ └── installers/ per-harness installers: claude.mjs, codex.mjs, opencode.mjs, lib.mjs
193
196
  ├── archive/ retired code/assets (local exporter, icon library, brand design files) — NOT shipped
194
- ├── decks/ the user's working decks; sources are slides/*.jsx, design via .bundle-ref.json
197
+ ├── decks/ the user's working decks: <slug>.pptx + plan.html at the root,
198
+ │ everything the CLI works with in <slug>/.slidemind/ (slides/, assets/,
199
+ │ conversions/, concepts/, state/, preview/) — src/lib/deck-paths.mjs,
200
+ │ which also owns the three rules a deck lives by: bundle ownership is the
201
+ │ bundle manifest PLUS matching bytes (never a filename prefix — a user's
202
+ │ assets/master-data.png is the deck's), plan.html has ONE owner declared in
203
+ │ state/plan-owner.json (core or an extension; neither overwrites the other's
204
+ │ page), and a deck without .slidemind/ is its own workspace — until the
205
+ │ workspace registers it: migrateLegacyDeck then moves the flat layout in
195
206
  ├── docs/slidemind-cli-connect/ the connect architecture (bridge contract, bundle, engine, phasing)
196
207
  └── test/ node:test suites (bridge client/bundle/build-and-push/resolve-skill/lint-wrapper/packaging/plugin-manifests/engine-search/native-slides/cli-dispatch/cli-download-result)
197
208
  ```
@@ -208,9 +219,9 @@ slidemind ready --diagnose # read-only report on TWO independent ladders
208
219
  slidemind read [presId|file.pptx] # pull an open presentation's content (presId; HOST REQUIRED) OR read a local .pptx on disk (file path; host-free) — same per-slide output (--mode summary|raw)
209
220
  slidemind download-bundle [deck] # materialize a corporate design's bundle locally — pane-free for a bound deck or --brand <presFP>; a window is only consulted when the design isn't named yet (or --to targets one)
210
221
  slidemind new <name> # scaffold ./decks/<name>/ (auto-binds to the one open design, else the one cached design, when its bundle is already materialized; --brand <presFP> binds to the design the user chose instead; --to/--no-bind)
211
- slidemind plan <deck> # plan sign-off gate on <deck>/plan.md; markdown editor + live preview + overall feedback, blocks for approve/comments (host-free)
212
- slidemind review <deck> # spatial preview-review gate; blocks for approve/comments (host-free)
213
- slidemind build-pptx <deck> # compose: add-in, pane-free (deck binding → the one cached design → the one listed design; a window only with --to or when none of those answers); template pack: host-free with --pack <dir>; never overwrites manual PowerPoint edits — text edits fold back, the rest refuses, --force discards (docs/slidemind-cli-connect/16-manual-edit-reconcile.md)
222
+ slidemind plan <deck> # plan sign-off gate on <deck>/plan.md: the storyline as a slide rail (drag to reorder, + to insert, × to remove, click the title to rename), the selected storyboard on stage, its layout gallery + details in the aside, a design dropdown over the rail (every design cached on this machine, shown as its name + accent colours; a pick moves the plan's layouts onto that design and rebinds the deck when the answer is sent, and the DECISION output tells the agent to re-read its design.md), one comment box + Send / Approve plan (Approve off while a comment waits; the click copies the message and ONE dialog says to paste it); asks in the workspace and returns at once — ASK + SAY, the agent ends its turn (host-free)
223
+ slidemind review <deck> # spatial preview-review gate; asks in the workspace and returns at once — ASK + SAY (host-free)
224
+ slidemind build-pptx <deck> # compose: add-in, pane-free (deck binding → the one cached design → the one listed design; a window only with --to or when none of those answers); template pack: host-free with --pack <dir>; never overwrites manual PowerPoint edits — text edits fold back, the rest refuses, --force discards (docs/slidemind-cli-connect/16-manual-edit-reconcile.md); a compose deck connected to SharePoint (bound file, or a folder connected in the workspace) is PUBLISHED right after the build — `--no-publish` keeps it local, a publish that did not save exits 2
214
225
  slidemind inspect-template-deck <deck> # normalized pack model for pack-owned validation; --pack <dir> --json
215
226
  slidemind view-draft <deck> # lint + build the HTML preview (this is where `slidemind build` went), then render the deck's slides to per-slide PNGs so the agent can look before review; --fix applies the deterministic lint autofixes; --lint-only stops after the lint; no push, no .pptx (host-free once the bundle is materialized); --only NN for one slide; the server's export gate (slot-fit/diagram-geometry/connector-endpoints) runs on the captured slides BY DEFAULT — verdict only, skipped with a note when the backend is unreachable (--translate insists, --no-translate skips) — so a slide is proven buildable before review; --json
216
227
  slidemind push <deck> # render + push slides into a running PowerPoint (HOST REQUIRED); --replace reconciles the LIVE deck first (same doc), insert pushes never do
@@ -218,16 +229,410 @@ slidemind ls engines # list the live engine catalog (image + slide
218
229
  slidemind search "<query>" # search an engine for imagery or slides (--engine/--intent/--top/--deck) (pane-free on add-ins with engineRoutes; HOST REQUIRED on older ones)
219
230
  slidemind insert-slide [opts] # reuse found slide(s) live into a window (--search/--pick/--to/--at) (HOST REQUIRED)
220
231
  slidemind imagen "<prompt>" [opts] # generate ONE image via the add-in's image model into <deck>/assets/ (--deck/--out/--size/--quality; connected backend, pane-free)
221
- slidemind setup <claim> # one-command onboarding from the add-in's Connect CLI button: redeem the claim + npm install -g + skills + best-effort Microsoft 365 sign-in (TTY only) + ready (--for/--no-install/--no-ready/--no-login/--json)
232
+ slidemind concepts <deck> # after plan sign-off: one mockup PNG per body slide + (add-ins with `conceptSpecs`) its concept SPEC — concept-slide-NN.spec.json (machine, slot px) and .spec.txt (the element inventory an agent reads); --force regenerates
233
+ slidemind transcribe <deck> # write each slide's JSX skeleton from its concept spec: one element per spec line at the spec's slot coordinates, connectors by literal id, icons as ./assets/NN-<icon id>.svg; existing slide files kept (--force), --only NN, --json
234
+ slidemind assets <sub> # the deck's image registry: import <deck> <file> copies a file in, identifies it from its BYTES (sniffed mime + intrinsic size), verifies the digest, dedupes within the deck and records it in state/image-assets.json under assets/local/<assetId>.<ext>; list <deck> reports it. File paths only — no URL fetch — and it infers NO intent: what an image is FOR is an assetUses record in the plan (--json on both)
235
+ slidemind download-result [opts] # download engine result(s) into <deck>/assets/ (--search/--pick/--deck/--icon-color); --name <file> fixes the asset's file name so a slide can reference it before the download runs
236
+ slidemind setup <claim> # one-command onboarding from the add-in's Connect CLI button: redeem the claim + npm install -g + skills + best-effort Microsoft 365 sign-in (TTY only) + starts the workspace and registers its login item + ready (--for/--no-install/--no-ready/--no-login/--json)
222
237
  slidemind add-slide [opts] # fold slide(s) into a deck as native .pptx — from an engine (--search/--pick/--deck/--to/--at/--json; pane-free on add-ins with engineRoutes) or a local file (--from <file.pptx>/--pick/--deck/--at; host-free, mutually exclusive with --search)
223
- slidemind adopt <file.pptx> # rebuild a deck workspace from a FILE's embedded source; foreign slides convert to editable jsx through the add-in's convert-pptx route where it is served (--deck/--brand/--no-convert) — live decks are add-in-only in v1 (docs/slidemind-cli-connect/14-foreign-slide-conversion.md)
238
+ slidemind adopt <file.pptx> # rebuild a deck workspace from a file's CURRENT content — embedded source is reused only when proved to still describe the slide, the rest converts or stays lossless native .pptx (--deck/--brand/--no-convert/--slides 2,7|2-4|all). Repeatable: same file is a no-op, a newer one reconciles a clean deck and refuses `needs_reconcile` over unpublished edits (docs 14 + 19)
239
+ slidemind revision <sub> <deck> # the adopted deck's own state — begin (take ownership for a whole edit session; export the printed token as SLIDEMIND_OWNER so nested commands re-enter) · finish · status [--json] (base, generation, dirty slides, candidates, staged, conflicts) · accept --candidate <id> (local-only; refuses stale, conflicted and remote-bound)
240
+ slidemind rebase <deck> # move an adopted deck onto a newer save of the same file (--incoming <file.pptx|revisionId>/--dry-run/--json): a conflict is ONLY a slide both sides changed — their edits, adds, deletes and moves apply, reorders on both sides combine, a design change is taken, local edits stay unpublished against the new base. "Changed" is read by meaning (`src/pptx/content-digest.mjs`), never bytes: a PowerPoint save rewrites most parts of a file nobody edited. A conflict activates NOTHING and writes a hash-bound resolution skeleton for --resolve <file>. No generic force flag, on purpose (doc 19)
241
+ slidemind open <link|file.pptx> # open a SharePoint presentation as a deck — the LINK selects the item (metadata never redirects it), one consistent snapshot is adopted through the same route as `adopt`, and the deck binds to that item. A local path adopts only and infers no destination (--deck/--slides/--rebind/--keep/--detach/--json); doc 20
242
+ slidemind pull <deck> # check the bound item AND every independently mutable input it names; unchanged = one metadata call and zero content requests. A newer save stages immutably and merges at an owned checkpoint — around unpublished local edits too; only a slide both sides changed stops it (--check/--json)
243
+ slidemind publish <deck> # (build-pptx runs it for a connected deck; this command retries one that did not save, or does a first --folder create) recover → pull → rebase → required extension release checks on the FINAL bytes → conditional replace under a server-enforced expected revision. Only a confirmed receipt advances the base; a 412 stages their save (--candidate/--manual/--shared-workbook/--json). Every write route is live (proved against the tenant 2026-09-16, doc 20 → Routes); the manual export branch is opt-in (`--manual`), and a deployment with `sharepointPublish` off refuses with `remote_saving_disabled`
244
+ slidemind rename <deck> --name <f> # conditionally rename the bound item: same item, no render, no second file, an existing name refuses
245
+ slidemind record <sub> # share ONE JSON file through the backend's record service, which owns the three-way merge: create <deck> --folder <link> --name <file.json> --file <path> (prints the permanent link) · open <link> --deck <slug> --file <path> · sync <deck> --file <path> [--approval] [--resolve <file>] · status <deck>. State in `<deck>/.slidemind/state/record.json` (url, eTag, versionLabel, the plan as last RECEIVED, the link, the local file); a same-field clash writes `state/record-conflicts.json` and exits 3. Generic on purpose — the CLI never validates, never merges and never learns what the document means; whichever pack owns the schema does all three
224
246
  slidemind login [--backend <url|env>] # sign in to Microsoft 365 (browser + PKCE); usually unnecessary — the first engine command runs this flow ITSELF and blocks until the human finishes (graph-auth `ensureSignedIn`, once per process). One sign-in mints Graph + SharePoint tokens from one refresh token; docs/slidemind-cli-connect/15-graph-delegated-cli.md
225
- slidemind whoami # list every connected environment (* = default) + its Microsoft 365 sign-in state
247
+ slidemind whoami [--json] # list every connected environment (* = default) + its Microsoft 365 sign-in state, named as the account that signed in (`msGraphUsername`, written at login, dropped with the token). `--json` → `{ environments: [{ label, default, oid, tid, microsoft365: { signedIn, username } }] }` — storage only for the sign-in half, no probe
226
248
  slidemind logout [url|env] # unpair one environment (--all unpairs every one; also drops its Microsoft 365 sign-in)
227
249
  slidemind init --for <target> # install skill into claude | codex | opencode | all | auto (also clears the bundle cache); `setup` already does this
228
- slidemind uninstall --for <target> # remove the skill (also clears the bundle cache)
250
+ slidemind uninstall --for <target> # remove the skill (also clears the bundle cache; --for all also removes the workspace's login item)
229
251
  ```
230
252
 
253
+ **A build proposes; acceptance is deliberate.** For an adopted deck `build-pptx`
254
+ renders only the slides that changed and applies them to the accepted base, so
255
+ every part the change does not touch keeps its exact bytes — notes, comments,
256
+ animations, embeddings and another add-in's tag parts included. What it writes is
257
+ an immutable CANDIDATE: the build never advances the accepted base and never
258
+ clears the dirty set, which is why `edit → build → build` keeps the first edit.
259
+ Moving the base is the separate `slidemind revision accept <deck> --candidate
260
+ <id>`, and `push` does not do it either — inserting slides into a window certifies
261
+ nothing was saved anywhere. A deck connected to SharePoint is the exception that
262
+ proves the rule: its acceptance door is a confirmed publish receipt, and the build
263
+ walks through it itself (below). The protocol (the three states, ownership, the
264
+ conflict classes, what blocks rather than degrades) is
265
+ `docs/slidemind-cli-connect/19-pptx-first-adoption.md` — read it there, don't
266
+ restate it.
267
+
268
+ **Four operations, and what each one means today.** An edit names what it is
269
+ allowed to do, so the kind is recorded rather than inferred from the diff it
270
+ produced (`src/lib/edit-operations.mjs`, `OPERATION_KINDS`):
271
+
272
+ | kind | what it is | how it starts |
273
+ |---|---|---|
274
+ | `create` | genuinely new slides | a build's own inference; **never** something a session claims, so `revision begin --operation create` is refused |
275
+ | `targeted-edit` | change slides that already exist, within a declared scope | `slidemind revision begin <deck> --operation targeted-edit --slides <sel>` |
276
+ | `repair` | bounded geometry/fit repair of existing source | `revision begin <deck> --operation repair --slides <sel>`, then `transcribe --repair` |
277
+ | `redesign` | rebuild a slide against the same identity | `revision begin --operation redesign --slides <sel>` |
278
+
279
+ `--operation` always needs `--slides`: a scope with no slides would silently
280
+ claim the whole deck.
281
+
282
+ **`transcribe --repair` is where "an operation is a permission" is clearest.**
283
+ Nothing about a file's own bytes authorizes rewriting it — the recorded `repair`
284
+ scope does, and a `targeted-edit` scope is refused by name. Three consequences
285
+ worth not re-litigating:
286
+
287
+ - **The scope is the unit.** `--only NN` may confirm a single-slide scope, never
288
+ carve a subset out of a wider one: a partial run would commit the operation
289
+ record as done for slides it never touched. The refusal points at
290
+ `--slides NN` instead.
291
+ - **It is deliberately NOT gated on plan approval.** Repair authors nothing from
292
+ the plan, the recorded scope is the authorization, and an adopted deck being
293
+ repaired may have no plan at all — `runRepair` returns before
294
+ `requirePlanApproved` on purpose. Do not "fix" that later.
295
+ - **It adds no approval step, and re-arms none.** The preview review is
296
+ one-time per deck (below), so repaired bytes go `view-draft` → deliver like
297
+ any other change after the approval.
298
+
299
+ Everything it refuses, it refuses **before staging anything**: an unrecorded
300
+ slide (no `state/transcribed.json` entry — an ancestry question, not a syntax
301
+ one), converted provenance, a current representation the validator cannot parse,
302
+ a bundle with no capture engine, and `--force`/`--discard-edits` alongside it.
303
+ Blocked-not-malformed exits 2, the same code `deck_busy` and a rebase conflict
304
+ use. The budget (`REPAIR_BUDGET`, `src/builders/settle.mjs`) is cumulative **from
305
+ the original source**, not pass to pass: ≤3 mutating passes, ≤8 slot-px travel
306
+ and ≤10% size change per element, zero text/asset/element-count/endpoint change,
307
+ and no font change unless the operation permits it. What the operation authorizes is a **permission**, not a
308
+ detection — it is what lets a named element be removed
309
+ (`permittedChanges`/`applyRequirementAmendment`), and it is read by the draft
310
+ verdict for exactly that. It is **not** how the CLI decides that a build is a
311
+ replacement.
312
+
313
+ **What a scope does not authorize is refused before anything renders.**
314
+ `view-draft`, `build-pptx` and `push` refuse a slide changed outside the open
315
+ scopes (compared by slide key against `preparedSourceHashes`, so a `-2.jsx`
316
+ sibling reads as appeared) and an import whose pragma was dropped outside a
317
+ redesign (`src/lib/edit-transitions.mjs`). The rule is the shared
318
+ `editTransitionViolation` (`src/lib/edit-operations.mjs`, mirrored) the add-in
319
+ enforces at every write; the CLI's agent writes files itself, so the next
320
+ command that reads the deck is the earliest seam. An authorized restyle flips
321
+ the conversion record to `dialect: "semantic"`, the counterpart of the add-in's
322
+ dialect-sync. View-draft's autofix stays inside an open scope for the same
323
+ reason.
324
+
325
+ **A targeted edit may restructure, not only retype.** Adding elements,
326
+ regrouping them and changing their relationships are supported inside the
327
+ operation's scope; what is protected is the identities and properties the
328
+ preservation record names, which is a different thing from "text only". Do not
329
+ write guidance that implies an existing slide can only have its words changed.
330
+
331
+ **Preservation obligations protect a slide whether or not anyone opened a
332
+ scope.** `state/slide-preservation.json` records, per slide identity, what must
333
+ survive an edit. `view-draft` seeds it for every rendered slide that has none,
334
+ from current evidence (its concept spec while transcribe still owns the bytes) —
335
+ the add-in seeds at every render the same way (`preservation-seed.ts`), as guide
336
+ task 5.1 asks for new source too. `revision begin`, `adopt` (restored from the
337
+ package's embedded tags, `src/lib/preservation-tags.mjs`) and `transcribe
338
+ --repair` capture it earlier.
339
+
340
+ So the gate keys on **the record**, never on an open operation
341
+ (`src/builders/draft-check.mjs` → `preservationBlocker`): a user does NOT have
342
+ to run `revision begin` for an adopted deck to be protected. The two
343
+ moments differ on purpose:
344
+
345
+ - `view-draft` **reports**: one `[preserve:warn]` per unmet obligation naming
346
+ the element id and the repair, and a `[preserve:error]` when the record
347
+ itself cannot be read. Both are measured defects (they hold `review`, and the
348
+ same line holds the add-in's preview gate). What a draft cannot answer prints
349
+ nothing: the agent cannot act on it, and delivery names it. The tags, their
350
+ wording (`preservationFailureLines` / `preservationUnreadableLine`) and their split (`draftLineTier`) live in
351
+ `draft-lines.mjs`, mirrored into the add-in, so both clients print and
352
+ classify the same lines.
353
+ - `build-pptx` and `push` **refuse** (`requireSlidesPreserved`, next to
354
+ `requireReviewApproved` in `src/cli/common.mjs`), naming the slide and the
355
+ unmet elements.
356
+
357
+ Delivery refuses on three things: an obligation that is **violated**; bytes that
358
+ have **not been checked** (no verdict, or one bound to earlier source — the
359
+ verdict carries the source hash + requirement-set hash it is bound to, so an edit
360
+ after a draft goes stale rather than certifying the new bytes); and an obligation
361
+ that **could have been proved and was not**.
362
+
363
+ That third rung is the package channel. `src/pptx/package-evidence.mjs`
364
+ (`composeSlideEvidence`) reads a slide's import (`conversions/<stem>.slide.pptx`)
365
+ and the deck as last delivered (`<deck>/<slug>.pptx`, matched by the GUID in its
366
+ baked source tags, never by position), and `comparePackageEvidence` turns the
367
+ pair into the evidence a connector's endpoints and a native's dependency closure
368
+ actually need. Both packages or neither: comparing a package with itself proves
369
+ only that it exists.
370
+
371
+ Two things that pairing taught, both load-bearing:
372
+
373
+ - **A requirement's relationship targets live in three namespaces.** `start`/`end`
374
+ are element ids the package resolves; `package-part` is a WORKSPACE path the
375
+ filesystem resolves; `native-slot` is an index into the conversion record's
376
+ `preserved[]`. Resolving them all against the package's own targets reported
377
+ `relationship_broken` for a file sitting right there — a **false deletion**,
378
+ strictly worse than the `unknown` it replaced. `workspaceResolvedTargets`
379
+ resolves each in the namespace it was minted in, and anything that does not
380
+ resolve still fails.
381
+ - **The source half must not veto the package.** `checkRequirements` reports every
382
+ non-`media` scope UNVERIFIED because source bytes cannot prove a text, fill or
383
+ geometry unchanged. That is "source cannot say", not "nobody can": once the
384
+ package has spoken, only the source's VIOLATIONS still count (a missing
385
+ element, a repointed connector, a swapped asset — facts no package makes
386
+ untrue). Merging its silence in capped every verdict at `unknown` forever.
387
+
388
+ A slide with **no import baseline** (anything authored rather than converted) has
389
+ no package to be proved against at all, so it ships with its unproven obligations
390
+ NAMED on stderr and its record still reading `unknown` — never a pass. Refusing
391
+ there would demand evidence that cannot exist, which is the outage this rung was
392
+ briefly relaxed to avoid.
393
+
394
+ > **Closed departure (recorded, not deleted) — and closed PER SLIDE, not
395
+ > wholesale.** Between 5.4 and the package channel landing, `unknown` did not
396
+ > refuse at all. Gating on `passed` alone made **every adopted deck permanently
397
+ > unbuildable, a clean one included**, because `passed` was unreachable for
398
+ > connectors and natives. The relaxation was deliberate and temporary; the
399
+ > channel closed it *wherever the channel reaches* — `packageEvidenceReachable`
400
+ > is evaluated per slide, so one deck can legitimately hold a slide that refuses
401
+ > and a slide that ships with a note. The no-baseline case above is not a
402
+ > leftover to be tidied away: it is the rule's other half.
403
+
404
+ **Both surfaces follow one rule — *proof is demanded where proof is possible* —
405
+ and they agree on more than the difference suggests.** Each refuses `unknown`
406
+ exactly where `passed` is reachable FOR IT. The CLI's reachability predicate is
407
+ per slide (`packageEvidenceReachable`): true for an adopted slide, **false for a
408
+ concept-authored one**, because `packagePair` finds no
409
+ `conversions/<stem>.slide.pptx` for source that was never imported. The add-in's
410
+ is constantly false — it composes no package evidence at all. So:
411
+
412
+ | deck | CLI | add-in |
413
+ |---|---|---|
414
+ | authored from scratch, with a connector | ships with the note | ships with the note — **identical** |
415
+ | adopted from a `.pptx` | can prove, and refuses | ships with the limitation |
416
+
417
+ Only the imported path differs. Do not describe this as "the CLI is stricter";
418
+ describe it as one rule over two different sets of available evidence, and never
419
+ as a way around the CLI's refusal.
420
+
421
+ Two consequences worth stating rather than rediscovering:
422
+
423
+ - **An authored slide's connector obligations cannot be proved on either client**,
424
+ and that is not a gap to close by relaxing the CLI. There is no baseline to
425
+ compare an authored slide against — one has never existed for source nobody
426
+ imported.
427
+ - **The add-in will not be catching up soon, for a structural reason.** It
428
+ deliberately does not persist the conversion-time source package, and the bytes
429
+ it holds at render time are **pre-splice**, so native objects are still
430
+ placeholders there. The one obligation package evidence exists to answer is not
431
+ in those bytes; composing evidence there would look authoritative while being
432
+ unable to see the thing it checks.
433
+
434
+ `test/preservation-gate.test.mjs` holds the invariant that keeps this honest:
435
+ *"TRIPWIRE: draft-only evidence must NEVER reach `passed` — a render cannot speak
436
+ for the package"*. That is a permanent property, not a phase gate: a render shows
437
+ that a box painted, never that the thing it stands for survived into the
438
+ delivered file. Do not relax it to make a build green.
439
+
440
+ The limitation names the slide and the element it could not prove, never a
441
+ generic caveat — a shipped-with-note slide must not be mistakable for a verified
442
+ one.
443
+
444
+ **Three channels the verdict does NOT bear, stated so nobody assumes them.**
445
+ None is a regression — none was ever covered — but each is a place the gate can
446
+ pass something a person would call a loss.
447
+
448
+ - **A table's grid structure is not verdict-bearing.** `tableGrid` composes rows,
449
+ columns and merge continuations as *sibling* evidence, and deliberately not as
450
+ a property scope: `PROPERTY_SCOPES` is a shared contract
451
+ (`src/lib/edit-operations.mjs`) and a grid is not one of its seven. So a merged
452
+ cell that absorbed its neighbour, or a row removed **without losing text**,
453
+ passes. A row that takes text with it — the ordinary case — IS caught, because
454
+ `text` is a scope and the package compares it per shape.
455
+ - **Connector endpoint pairing is imprecise with two or more connectors on a
456
+ slide.** `resolvedTargets` is one global set, so the verdict asks "is this
457
+ endpoint still on the slide", never "is it still THIS connector's endpoint".
458
+ Two connectors that swap ends both resolve and both pass. The precise pairing
459
+ exists — `composeSlideEvidence` returns `connectors[elementId]` as
460
+ `{start, end}` — and the verdict simply does not consult it. Closing it means
461
+ changing how `preservationVerdict` resolves relationships, which is a consumed
462
+ contract; it is not closable from the gate.
463
+ - **`shape.chart` is not the same quantity in both clients.** The CLI's is a
464
+ DATA-RESIDUE digest (series names, category and value caches, and the formulas
465
+ they came from — `chartResidue`), chosen because PowerPoint re-serializes a
466
+ chart on every save and an XML hash reports drift on a deck nobody touched.
467
+ The add-in's is a hash over its canonical ChartSpec. Both answer "did this
468
+ chart's data change" and they are **not interchangeable**. Safe today only
469
+ because a verdict is always computed from ONE composer's own before and after.
470
+ If a cross-client chart verdict is ever wanted, the add-in adopts the residue —
471
+ the two must never be quietly assumed comparable.
472
+
473
+ **Preservation is per ELEMENT, so sub-element content rides on a property
474
+ scope.** A deleted table row leaves `<table id="50">` exactly where it was, so
475
+ identity evidence never sees it. The package's per-shape `text` does —
476
+ `parseTextRuns` walks `a:tbl` — so E09's "drops a table row" **is caught on a
477
+ converted slide**, proved by `test/preservation-gate.test.mjs` → *"E09: a
478
+ dropped table ROW fails on a CONVERTED slide"*. No new scope was needed; `text`
479
+ already covers a table's cells and `chart` a chart's series.
480
+
481
+ Catching it needs two things at once, and the two places it still fails are
482
+ different from each other:
483
+
484
+ | origin | protects `text`? | has an import baseline? | row caught? |
485
+ |---|---|---|---|
486
+ | `converted` | yes | yes (`conversions/<stem>.slide.pptx`) | **yes** |
487
+ | `concept` | yes | **no** | no — for want of a baseline, not a scope |
488
+ | `legacy-current` | **no** | no | no — neither |
489
+
490
+ So the residue is two separate things, neither a missing mechanism: an authored
491
+ slide has no package to be compared against, and what a `legacy-current` slide
492
+ may claim is a **product decision** (`PROTECTED_BY_ORIGIN` grants it only
493
+ `media`/`native` on purpose — its bytes prove an element is present, never which
494
+ of its properties were intended). `test/preservation-gate.test.mjs` →
495
+ *"E09 residue: a table row is NOT caught where no package can be compared"* pins
496
+ both reasons separately, so closing one does not read as closing both.
497
+
498
+ An intentional removal is cleared by superseding the obligation in a scope,
499
+ never by deleting the record.
500
+
501
+ **Publishing back to SharePoint is gated on evidence, not on optimism
502
+ (2026-09-15).** `open`/`pull`/`publish`/`rename` talk to the tenant's SharePoint
503
+ origin **directly**, with the delegated SharePoint token `slidemind login`
504
+ already mints — the one documented exception to doc 15's "the token rides the
505
+ bridge wire" doctrine, because a conditional write cannot be proxied honestly and
506
+ the backend has nothing to add to replacing a file the user already named. The
507
+ protocol is `docs/slidemind-cli-connect/20-sharepoint-pull-publish.md`; read it
508
+ there. Four things not to undo:
509
+
510
+ - **Every write carries the revision it expects to replace.** `If-Match` on the
511
+ small PUT, on upload-session creation and on the final chunk; a 412 is the
512
+ mechanism working and is staged for a rebase, never retried. The pull before
513
+ the write is what catches a foreign save in practice (observed live
514
+ 2026-09-16); the in-upload 412 itself has not been observed. Do not add an
515
+ unconditional write path, and do not retry a write whose outcome is unknown.
516
+ - **The bearer goes to the configured origin and nowhere else.** Download
517
+ redirects and upload-session URLs are preauthenticated: they carry their own
518
+ credential in the query string, so they get no Authorization header and their
519
+ query is never printed (`redactUrl`).
520
+ - **Reads may retry; a write whose outcome is unknown may not.** It goes to
521
+ `inspectWriteOutcome`, which answers from content identity. A later save by
522
+ someone else is never proof that ours failed.
523
+ - **Only a confirmed receipt advances the base.** `acceptCandidate` still refuses
524
+ a remote-bound deck; `acceptCandidateFromReceipt` is the other door and it
525
+ checks that the receipt names that candidate's bytes.
526
+
527
+ **A connected deck publishes at the build, not when an agent remembers to
528
+ (2026-10-12).** A user connected a workspace folder mid-build; the agent had read
529
+ `ready` before the folder existed, built a local `.pptx` and stopped, and nothing
530
+ ever reached SharePoint. A session-start report cannot carry a fact that appears
531
+ later, so the connection is read where the deck CHANGES: `build-pptx` (compose
532
+ path) calls `publishAfterBuild` (`src/cli/remote.mjs`), which publishes when the
533
+ deck has `state/remote.json` or a saved `state/destination.json`, through the same
534
+ `runPublish` the command uses — every gate stays. It never exits mid-build: a
535
+ publish that did not save is printed next to the build and exits 2 (`--json`:
536
+ `publish: { status: "not_published", reason }`). A folder connected AFTER the last
537
+ build is the other seam: the workspace's connect route uploads an approved,
538
+ current candidate at once (`uploadToDestination`, `slidemind publish` as a child)
539
+ and records the outcome in `destination.json`'s `upload` for the card. Template
540
+ packs save no candidate, so their builds do not publish. Two write answers live
541
+ in a destination, never one: `writes` is the shared PLAN record (record service AND
542
+ policy), `publishWrites` the PRESENTATION (`sharepointPublish` alone) — prod ships
543
+ no record service, and the folded flag labelled a publishable deck "read-only".
544
+
545
+ **The machine reads the image (2026-09-19).** Fourteen sim sessions (x19b–x26) showed a
546
+ coding agent cannot be instructed into transcribing a mockup: the image is opened once,
547
+ elements are missing from the FIRST write, and every self-declared contract (inventory
548
+ form, `// image:` count line) gets edited down instead of honoured (29 of 32 `[image:warn]`s).
549
+ So the see-step moved into the add-in and the first write into the CLI:
550
+
551
+ - `POST /api/cli/brands/:id/concept-specs` (capability `conceptSpecs`; add-in
552
+ `concept-spec.ts`) runs the design model over a mockup and returns every element with
553
+ its rect (mapped from the image's body onto the layout's `[CONTENT]` slot), verbatim
554
+ text, type role, fill, silhouette, icon query + glyph colour, chart series, and every
555
+ connector between element ids. `slidemind concepts` writes the complete machine JSON under
556
+ `state/concept-specs/`; no second editable spec or outline is required. A spec is
557
+ bound to the mockup bytes it was read from (`imageSha256`, `src/lib/concept-spec-binding.mjs`):
558
+ transcribe, view-draft and the next `concepts` run take it only while the PNG on disk
559
+ still hashes to it (an unbound or stale spec is read again, never reused), `concepts`
560
+ generates only the mockups the plan lacks (a kept mockup keeps its spec; `--force`
561
+ remakes all), and a replaced mockup takes its spec with it in the same step (x112 GTM:
562
+ a partial set regenerated whole, slides 1–3 transcribed from pictures that no longer
563
+ existed). `src/builders/concept-spec.mjs` normalises the spec to the layout's
564
+ background tone from design.md (a black mockup on a white layout: light text flips to
565
+ dark, a white card gets `shadow`) and adds `~Npx`, the font size the image shows.
566
+ - `slidemind transcribe <deck>` (`src/builders/transcribe.mjs`, `settle.mjs`) is the
567
+ first write AND most of the finishing: one JSX element per spec element with three
568
+ helpers (`B` box, `T` text, `I` icon), every line as a native line directive (below),
569
+ filled silhouettes as svg polygons, type at the image's measured even-pixel size with single lines no-wrap.
570
+ Then it **settles**: renders the slides, measures them, and applies three rules (a text
571
+ past its card grows the card and pushes what sits below; texts on the same pixels move
572
+ apart; anything past the slot lifts or shrinks its fonts) for up to three passes. Then
573
+ it **fetches every icon itself** (one engine search per motif, first result, retried
574
+ with shorter queries, the pre-named file; a miss keeps the unresolved reference and is reported).
575
+ **The JSX is the only editable artifact.** The spec is a machine intermediate under
576
+ `state/concept-specs/` (no outline file, nobody edits it); the agent compares PNG pairs
577
+ and edits the slide JSX like any other slide. `transcribe` stamps what it wrote
578
+ (`state/transcribed.json`): a slide whose bytes still match is machine-owned (the
579
+ spec's counts apply, `--force` may rebuild it); one that differs is hand-owned —
580
+ `transcribe --only NN` then only re-settles it in place (`settle.mjs` reads the
581
+ helpers back and rewrites the numbers), `--force` refuses unless `--discard-edits`,
582
+ and `view-draft` skips the count check with a `[spec:note]`. Colours: the spec carries
583
+ the hex the model saw (`fillHex`, `colorHex`); `design-tokens.mjs` snaps it to the
584
+ nearest token in `corporate-design.css` (`fillVar` / `colorVar`), and a colour the
585
+ design lacks (a green status dot on a blue palette) falls back to the tone word and is
586
+ named in a `note:` — the design's palette is never duplicated in the CLI. The body box
587
+ is the elements' own bounding box, not the model's, so no empty band opens under a
588
+ headline. `--no-settle` / `--no-icons` switch the steps off.
589
+ - **Every stroke is one native line (nativeLinesV1, 2026-09-25).** A connector is
590
+ `<div data-connector>` (`cxnSp`: `data-prst` straight/bent/curved, `data-adj`, `data-rot`,
591
+ a glued end `#id` + `data-*-site` = an index into the node preset's real cxnLst, a free
592
+ end `x,y`), a free shape line `<div data-sm-line>` (`arc`, brackets, braces, `blockArc`,
593
+ `donut`, `ellipse`), a chain `<div data-sm-line-group>` (free pieces only); never svg,
594
+ never custGeom. `line-native.mjs` `fitNative` picks preset + adjusts by MEASUREMENT with
595
+ the shared evaluator (`preset-eval.mjs`); a glued relation is always ONE connector at the
596
+ nearest real site, its deviation a `[line:warn]`. Width and colour are design tokens
597
+ only: the reader's declared width snaps to `LINE_WEIGHT_RUNGS_PX` (`--bw-*`), never a
598
+ pixel measurement, and the agent sets final weights by proportion. Settle re-solves only
599
+ adjusts, turn and a glued end's site; nodes never move. `transcribe` refuses (exit 2) an
600
+ add-in that answered without `nativeLinesV1` — there is no older markup to fall back to.
601
+ The directive contract is the add-in's; the replaced attributes (`data-route`,
602
+ `data-arrow`, `data-head`, `data-bend`, `data-*-side`, `data-sm-glyph`) are refused, not read.
603
+ - Spec extraction runs as a **polled job** (`POST /api/cli/brands/:id/concept-spec-jobs`,
604
+ capability `conceptSpecJobs`): a sync request died at the ingress's ~240 s on dense
605
+ slides; the job runs eight slides at once with an 8-minute budget each, the CLI writes
606
+ each spec as it arrives and prints a heartbeat every 30 s. The model's placeholder
607
+ grammar for "no text" (`/null`, `><null>`, `/slide`, `s:null`, "There is no text.") is
608
+ dropped in `cleanText`; it changes between runs, so the filter is by shape, not by list.
609
+ - Result (x35, sol@low, both fixture decks): 0.825 / 0.77 fidelity, both built and
610
+ review-approved, $3.8, ~19 min each, no subagent, no agent-written JSX. The stock
611
+ skill's baseline for the same model was 0.69 / 0.62 at $5–8 with 5–20 draft rounds.
612
+ Hold-out on fresh topics with the general skill: 0.77 / 0.72, both built. Each spec
613
+ records the backend tokens it cost (`usage`); `scripts/codex-sim-report.mjs` prices
614
+ them in a `spec` column, so a run's cost is agent + specs (+ mockups via `--price-image`).
615
+ - `view-draft` counts the RENDER (boxes with `imgSrc`, non-native `connector`, `chart`)
616
+ against the spec's counts — nothing the agent can edit. Without a spec the agent's own
617
+ `// image:` line is the fallback contract, first count binding. It also names every
618
+ `./assets/<file>` a slide references that is not on disk (`[asset:error]`, blocking): a
619
+ transcribed slide references its icons before the fetcher delivers them, and one
620
+ undelivered file used to surface only as a 422 at `build-pptx`, after review (x28).
621
+ - The transcriber's own lessons, each from a judged run: the image's line breaks are kept
622
+ (a bullet, number or capital starts a line; lower-case or indented text continues), the
623
+ model's no-text stand-ins ("/null", ">", "There is no text.") are dropped, a bold lead-in
624
+ the model lists twice folds into its sentence as `<b>`, chevron/triangle/trapezoid become
625
+ `clip-path` polygons, a chart without readable values becomes a placeholder plus a
626
+ `note:` instead of a broken `<Chart>`, thumbnail charts carry no labels, and a plan
627
+ layout the bundle lacks falls back to a content layout with a `note:` (an agent once
628
+ spent 11 minutes editing the approved plan over that).
629
+
630
+ Three add-in gate rules were also corrected on the same evidence: a computed style value
631
+ (ternary/variable) warns as unreadable instead of erroring as "not in the capability
632
+ table" (agents deleted the divider); the arrow-glyph rule is a warning (an axis label in
633
+ the reference is faithful copy); an svg carrying `<text>`, or a lone axis-aligned
634
+ markerless `<line>`, is a figure or a rule — never a bare connector whose ends are gated.
635
+
231
636
  **Bundle freshness is automatic (2026-09-04).** A corporate design can change
232
637
  while a session runs, and every local step afterwards — HTML build, lint,
233
638
  capture, the draft checks, and the etag the server render is validated against —
@@ -270,6 +675,26 @@ folded names (`build`, `doctor`, `presentations`, `status`, `connection`, `use`,
270
675
  `upgrade`) still resolve to a one-line redirect naming the owner — stale skills
271
676
  and habits type them — never to a bare "unknown command".
272
677
 
678
+ **`--json` stdout carries the payload and nothing else — and that is a test,
679
+ not vigilance.** Three separate leaks of this one class have been found and
680
+ fixed (`ASSETS: n/m fetched`, `Autofixed N lint issue(s)`, and
681
+ `freshenDeckBundle`'s "design bundle updated", which every `--json` command
682
+ calls — so one helper was corrupting `view-draft`, `build-pptx`, `concepts` and
683
+ `push` alike). Three instances is a class, so:
684
+
685
+ - **Any new human-readable line goes to stderr.** Not "unless `--json`" — to
686
+ stderr, unconditionally. A line that is only wrong in one mode gets written in
687
+ the other mode by the next person.
688
+ - **The test for a `--json` command is that its stdout `JSON.parse`s as a single
689
+ document**, run against a real deck. Asserting that a string moved streams
690
+ proves the edit; parsing the output proves the property — it is what showed
691
+ there were two leaks rather than the one that was reported.
692
+ - The same holds for a **withheld mutation**: every guard that declines to
693
+ rewrite something says what it withheld and why, on stderr. `view-draft --fix`
694
+ and the `src/builders/build.mjs` dev script both print `[fix:withheld]`; the
695
+ duplication is deliberate, because sharing the printer would mean reshaping a
696
+ seam four production commands depend on.
697
+
273
698
  **Agent-facing conventions (2026-09-05).** Every command takes `--help` (its
274
699
  own usage block, `src/cli/help.mjs` → `commandBlock`); the deck commands take
275
700
  `--json` (`ready`, `view-draft`, `concepts`, `build-pptx`, `download-result`,
@@ -285,6 +710,112 @@ CLI's rendering changes. A fanned-out `ls engines` never opens the browser
285
710
  sign-in for an environment nobody asked about (`signIn: false` on the
286
711
  multi-environment listing; the refusing env is named in a note instead).
287
712
 
713
+ **The preview review is one-time per deck (2026-10-08).** The approval builds
714
+ the deck; every change after it — an edit, a comment on the delivered deck, a
715
+ reconcile fold, a repair, an added slide — goes straight into the `.pptx`, which
716
+ is where the user sees it. So `requireReviewApproved` asks whether the deck
717
+ EVER passed the review (`review.json`'s `approval`, carried through any later
718
+ comments round by `respondPreview`), never whether the slides still match it.
719
+ The hash still binds the ROUND: an answer on since-edited slides is refused as
720
+ `stale_subject`, and a gate asked again answers at once only when the latest
721
+ round approved these exact bytes (`previewApprovedAsIs`). This holds for the
722
+ create-slides skill and for packs like the proposal generator alike. Do not
723
+ bring back "re-review after any slide change" — it made the user approve the
724
+ same deck again for every fix.
725
+
726
+ A deck adopted from the user's own `.pptx` (the edit route,
727
+ `src/builders/edit-route.mjs` → `isEditRouteDeck`, which both gates import)
728
+ needs no review at all (2026-10-09): it was a deck before any agent touched it,
729
+ so all its edits are "afterwards". The workspace page shows it as *No review
730
+ needed* (`approved.notNeeded`), never as an approval nobody gave, and `ready`'s
731
+ `review` payload separates `current` (delivery may proceed) from `approved` (a
732
+ human approved).
733
+
734
+ **Every hand-back is a paste (2026-10-09; replaces the 2026-10-07 bounded
735
+ waits).** Nothing can push into an agent session whose turn has ended:
736
+ - the Codex desktop app runs a private app-server that `codex queue` doesn't
737
+ reach;
738
+ - `codex://threads/<id>?prompt=` only pre-fills, and is undocumented;
739
+ - Claude Code wakes an agent only for a command that agent backgrounded itself.
740
+
741
+ Waiting didn't close the gap. On 2026-10-08 a Codex agent did as its skill said
742
+ and ended its turn; the approval came 56 s later and went nowhere. So asking and
743
+ answering have one shape everywhere (doc 21 → *Tasks and agent continuation*):
744
+ 1. The agent's question command prints `ASK:` + `SAY:` and returns at once.
745
+ 2. The agent ends its turn with the SAY line.
746
+ 3. The user decides in the workspace; the click copies a message.
747
+ 4. The user pastes it; the message carries the command that receives the answer.
748
+
749
+ Things not to undo:
750
+
751
+ - **No gate waits, loops or re-runs.** `plan` / `review` / `tasks request` never
752
+ long-poll; `--wait` and `DECISION: pending → run it again` are gone. `tasks
753
+ wait` stays for scripts and no skill names it. A wait reintroduces the
754
+ "maybe an agent is listening" branch, and with it a second thing the page
755
+ must say.
756
+ - **The page claims nothing about agent processes.** No "delivered", "sending
757
+ to your agent", "your agent is waiting / away". After every answer it shows
758
+ the same dialog — "Paste this into your agent chat", then "Copied — press
759
+ ⌘V there and send." — and it says "Copied" only when a copy path actually
760
+ succeeded. "Your agent picked
761
+ it up" appears only once the receipt (`pickedUpAt`) lands, i.e. after the
762
+ paste. That dialog is the ONE place the page says to paste: the
763
+ buttons name only the decision (*Approve plan*, *Send 2 comments* — never
764
+ "& copy message"), and after *Done* nothing repeats it — no strip under the
765
+ bar, just a neutral "Answer saved" badge whose click reopens the dialog.
766
+ - **The human's instruction is "paste it".** The copied message names the deck,
767
+ the decision and the receive command (`slidemind plan|review "<path>"`, or
768
+ `slidemind tasks get "<path>" <id>` for a pack's task). The human never
769
+ types a command; the agent reads one.
770
+ - **One link per deck and view, and nothing opens a browser on its own.** The
771
+ link is `/deck/<id>/plan#bootstrap=…` or `/deck/<id>/preview#bootstrap=…`.
772
+ The Codex desktop app opens local URLs in its own browser pane by default;
773
+ it was verified 2026-10-09 (sign-in, both copy paths, `confirm()`,
774
+ notifications). A command that opened Chrome on top of that made two places.
775
+ - **The workspace is always running, and an agent never starts it from a
776
+ sandbox.** `setup` starts it and registers the login item (doc 21 → *Launch
777
+ and stop*). With network off, Codex's sandbox can read the config but can't
778
+ write it, can't keep a detached process and can't even connect to
779
+ `127.0.0.1`. A sandboxed question with no workspace stops on
780
+ `workspace_sandboxed`, and the skill re-runs it outside the sandbox. There
781
+ is no automatic standalone fallback page: a second page on another port is
782
+ the second link this protocol removed. `SLIDEMIND_WORKSPACE=0` (rollback)
783
+ and `SLIDEMIND_GATE_AUTOAPPROVE=1` (headless) are the only ways to it.
784
+ - **Decks live where the agent may write.** The skills create a deck under
785
+ `./decks/<slug>` of the agent's working folder. A path the user names (an
786
+ RFP, a reference deck) is input that is read, never adopted as the deck
787
+ folder. Working on a deck elsewhere is the user's explicit choice; there
788
+ Codex asks "Allow?", and that protection is never routed around through the
789
+ workspace (which runs outside the sandbox).
790
+ - **A stopped command stops its gate.** `runNode` forwards SIGTERM/SIGINT/SIGHUP
791
+ to the script it runs (the standalone gate still blocks): a harness signals
792
+ the pid it started, and an orphaned gate kept its port.
793
+ - **A framed view keeps its navigation; its MESSAGES are the page's.** The
794
+ plan gate, the review gate and a pack's plan page publish their status line,
795
+ every banner/notice and the hand-off dialog as one `chrome` object
796
+ (`slidemind-view-chrome`; doc 21 → *A framed view keeps its navigation*),
797
+ and the workspace page renders them above the frame. A view's own tabs,
798
+ title and switches stay inside the frame. Never paint a message inside the
799
+ frame it is about, and never move a view's navigation out of it.
800
+ - **A state is a badge; a notice is for something the user must do.** The
801
+ people reading these pages are not technical. Approved (and when), ready for
802
+ your review, answer saved, the agent revising —
803
+ each is a few words in the view's status badge, its why in the badge's
804
+ tooltip (`status.detail`).
805
+ A notice appears only when the user has to act or something failed. No
806
+ hashes, error codes, ISO dates or record vocabulary ("amendment draft",
807
+ "locally", "bound") on screen, and nothing said twice: the review gate owns
808
+ the stale-preview line, the workspace's connection banner owns "offline",
809
+ and a status with nothing to say is no badge at all (never "up to date").
810
+ - **No test may reach the real config dir.** A gate test that found the real
811
+ registry once spawned a real daemon. `npm test` preloads
812
+ `test/support/isolate-config.mjs` (a private `XDG_CONFIG_HOME` per file);
813
+ standalone-gate tests also pass `useWorkspace: false`. Nor the real
814
+ workspace PORT: the preload sets `SLIDEMIND_WORKSPACE_PORT` per file, so no
815
+ test server calls 57103 home (one took it while the real workspace restarted
816
+ into new code, and its links went dead). Run single files with
817
+ `XDG_CONFIG_HOME=$(mktemp -d) node --test --import ./test/support/isolate-config.mjs …`.
818
+
288
819
  `<deck>` resolves as: absolute path → `./<arg>` if it exists → `./decks/<arg>`. The decks directory is `./decks/` by default; override with `SLIDEMIND_DECKS=/abs/path`.
289
820
 
290
821
  **Multi-environment sessions.** The CLI stores one session **per backend** (`~/.config/slidemind/session.json`, v2 `{version, sessions[], default}`; the legacy single-record v1 shape is migrated on read). Connect once per environment (local / dev / prod) and every command **fans out** across all connected backends, tags each open window with its `env`, and **routes by the presentation's origin backend** automatically — no manual switching. Adding an environment is easiest with `slidemind setup <claim> --no-install --for none` — the claim carries its own backend, so nothing has to be typed or configured. There is no other way in: without a claim the CLI has no backend to talk to. Later commands may scope an existing session with `--backend <url|env>` / `SLIDEMIND_BACKEND`. A normal user connects exactly one backend.
@@ -343,6 +874,8 @@ The corporate design is **served by the add-in, not shipped by the CLI.** `slide
343
874
 
344
875
  Both skills are **brand-agnostic workflow guidance** and must not duplicate any brand rule — `scripts/check-skill-parity.mjs` (`npm run lint:parity`) scans **every** `skills/<name>/SKILL.md` and fails the build if a hardcoded color, design token, or layout name leaks into any skill doc.
345
876
 
877
+ Each `SKILL.md` also has a **byte budget** (`test/packaging.test.mjs` → *every shipped SKILL.md fits in one Codex read*): Codex reads a skill with one capped `cat` and drops the middle of anything longer, which is how the create flow of a 95 KB skill silently never reached the agent (2026-10-11). The authoring craft that is not CLI-specific belongs in the add-in's `references/*.md` (served as the bundle's `workflow/*.md`, read by the CLI agent and the in-app designer alike — when you move a rule there, drop its copy from the designer's `design/prompt.ts` too); a skill keeps the CLI mechanics, each written once, and points at the workflow doc that owns the rest.
878
+
346
879
  ## Sanity rules when editing slides
347
880
 
348
881
  - One default export per slide file, composing one bundle layout (Mode A) or a blank-content layout with freeform `children` (Mode B) — exactly as the bundle's `design.md` prescribes. No imports — layouts are globals in slide scope at build time.
@@ -356,9 +889,9 @@ Both skills are **brand-agnostic workflow guidance** and must not duplicate any
356
889
  ```bash
357
890
  slidemind ready <deck> # do this first: connection ladder + resolve the open design + materialize its bundle + bind the deck (host required)
358
891
  slidemind download-bundle <deck> # lower-level: materialize-only (no connection ladder, no bind) — `ready` now covers this (host-free once the deck is bound)
359
- slidemind plan <deck> # plan sign-off gate on <deck>/plan.md; blocks until approve/comments (host-free)
360
- slidemind review <deck> # spatial preview-review gate; blocks until approve/comments (host-free)
361
- slidemind build-pptx <deck> # native PPTX rendered server-side via the add-in (pane-free; a window only with --to)
892
+ slidemind plan <deck> # plan sign-off gate on <deck>/plan.md; asks in the workspace, returns at once; a pasted answer is received by running it again (host-free)
893
+ slidemind review <deck> # spatial preview-review gate; asks in the workspace, returns at once (host-free)
894
+ slidemind build-pptx <deck> # native PPTX rendered server-side via the add-in (pane-free; a window only with --to); publishes a SharePoint-connected deck too
362
895
  slidemind view-draft <deck> # lint + HTML build, then per-slide PNG draft preview, no push/no .pptx — the agent's look-before-review self-check (host-free once the bundle is cached); --fix autofixes lint
363
896
  slidemind push <deck> # render + push into a running PowerPoint (host required)
364
897
  slidemind read [presId|file.pptx] # pull current content of an open presentation (presId; host required) OR read a local .pptx (file path; host-free) (--mode summary|raw)
@@ -366,7 +899,50 @@ slidemind ls engines # list the live engine catalog (image + slide)
366
899
  slidemind search "<query>" # search an engine for imagery or slides (host required)
367
900
  slidemind insert-slide [opts] # reuse found slide(s) live into a window (host required)
368
901
  slidemind add-slide [opts] # fold slide(s) into a deck as native .pptx — engine (--search; host required) or local file (--from <file.pptx>; host-free)
902
+ slidemind adopt <file.pptx> # rebuild a deck workspace from a .pptx; --slides 2,7|2-4|all hydrates ONLY those slides to jsx, the rest stay lossless native — repeatable (same file = no-op, wider --slides hydrates the new ones, a different file reconciles)
903
+ slidemind revision <sub> <deck> # begin (owner token → export SLIDEMIND_OWNER) · status [--json] · accept --candidate <id> (the ONLY local way the base moves) · finish
904
+ slidemind rebase <deck> # --incoming <file.pptx|revisionId> [--dry-run]: onto a newer copy of the same file; a conflict activates nothing and writes a resolution skeleton for --resolve (no force flag)
369
905
  npm run lint:parity # assert EVERY skill (create-slides + search) carries no brand rules (CI guard)
370
906
  ```
371
907
 
372
908
  No linter beyond the parity guard, `node --test` for the suite, no package manager beyond npm itself. Slide-JSX lint (run by `view-draft` and `review` via `buildDeck`) uses the **add-in's** capability-table engine, materialized into the bundle cache as `lint-engine.mjs` (+ `primitives.json`) and dynamic-imported by `src/builders/lint.mjs` — this repo owns no lint rules; they update on the next bundle fetch. CI (`.github/workflows/test.yml`) runs the suite + parity guard on Linux, **Windows**, and macOS (Node 18 + 22) — the CLI promises native-Windows support, so keep it green there. The HTML preview is self-contained except for React + Babel-standalone pulled from a CDN on first load.
909
+
910
+
911
+ ## Production x42 authoring parity
912
+
913
+ The shipped CLI skill runs concepts → transcribe → compare/repair JSX → review → deliver.
914
+ Compact-v1 is the default reader schema; `SLIDEMIND_SPEC_SCHEMA=legacy` explicitly requests the
915
+ older schema. The manual `asset-plan.md` inventory is gone (2026-09-23): `transcribe` records and
916
+ fetches every asset the spec requests (`state/spec-assets.json`), so there is no second list to fill.
917
+ The production add-in mirrors the transcription modules listed in `scripts/sync-transcription-runtime.mjs` from `src/builders/` under
918
+ `src/ui/src/node/agent/runtime/transcription/`. Run `node scripts/sync-transcription-runtime.mjs`
919
+ to check parity, or `--write <addin-repo>` after changing the canonical CLI modules. The plan
920
+ contract travels the same way (storyboard/plan parsers, `plan-checks.mjs`, `plan-comments.mjs`,
921
+ `src/lib/image-intent.mjs` and its fixture): the add-in's plan review and runtime import the
922
+ mirror, so a plan reads, checks and words the same in both clients. Neither
923
+ runtime imports experiments. Existing and reverse-converted slides stay on the source-first
924
+ edit route; concept generation/transcription is not an editing prerequisite.
925
+
926
+
927
+ ## Measuring a transcription change (2026-09-23)
928
+
929
+ Single-run sim judge scores cannot resolve a transcription change: run-to-run noise is ~0.06–0.07
930
+ per deck and the concept image is regenerated every run. Measure on the frozen-slide replay instead:
931
+ `scripts/fidelity/replay.mjs --out <dir> <sim task dir>...` re-normalizes each stored reader spec
932
+ with the code under test, re-transcribes offline (icons resolved from the frozen run), captures, and
933
+ scores every element against the reader's spec (`scripts/fidelity/score.mjs`: overlaps the image
934
+ does not have, text escaping its shape, figures merging into their ground, connector path distance
935
+ matched by geometry, glued vs drawn connectors, `--gates` first-draft export-gate errors).
936
+ `scripts/fidelity/compare.mjs <baseline> <candidate>` pairs the slides. One change per comparison;
937
+ never edit `src/` while a replay runs (it spawns the live CLI). `SLIDEMIND_ABLATE=<step,...>` skips
938
+ named normalization steps for an ablation row — a measurement knob, unset in production.
939
+
940
+ The design palette is authoritative: colour is scored only as structure (a figure must stay apart
941
+ from its ground), never as distance to the concept's colour; icons are judged by placement and
942
+ variety, never by whether the glyph matches the concept's.
943
+
944
+ Sims run each codex session in its own HOME (`scripts/codex-sim.mjs`): agents read whatever
945
+ `~/.cache` and `~/.config` hold, and before x62 that was this machine's bundle cache and workspace
946
+ registry. `scripts/sim-validity.mjs` → `hostLeaks` marks a session that read host state as
947
+ contaminated. `slidemind workspace status` reports `headless: true` under
948
+ `SLIDEMIND_GATE_AUTOAPPROVE=1`, and the skill continues without a workspace there.