portable-agent-layer 0.32.0 → 0.34.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 (38) hide show
  1. package/README.md +1 -0
  2. package/assets/skills/presentation/SKILL.md +124 -5
  3. package/assets/skills/presentation/WORKSHOP.md +128 -0
  4. package/assets/skills/presentation/theme-base/base.css +113 -0
  5. package/assets/skills/presentation/theme-base/layouts.css +11 -2
  6. package/assets/skills/presentation/tools/build.ts +136 -6
  7. package/assets/skills/presentation/tools/doctor.ts +106 -317
  8. package/assets/skills/presentation/tools/lib/lint-helpers.ts +150 -0
  9. package/assets/skills/presentation/tools/lib/lint-rules.ts +744 -0
  10. package/assets/skills/presentation/tools/lib/lint-types.ts +40 -0
  11. package/assets/skills/presentation/tools/new-deck.ts +9 -4
  12. package/assets/skills/presentation/vendor/reveal/plugin/highlight/github-dark.css +118 -0
  13. package/assets/skills/projects/SKILL.md +111 -0
  14. package/assets/skills/telos/SKILL.md +4 -1
  15. package/assets/templates/AGENTS.md.template +28 -7
  16. package/assets/templates/PAL/ALGORITHM.md +2 -0
  17. package/assets/templates/PAL/README.md +1 -2
  18. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  19. package/assets/templates/pal-settings.json +2 -2
  20. package/package.json +2 -3
  21. package/src/cli/index.ts +7 -0
  22. package/src/hooks/UserPromptOrchestrator.ts +3 -1
  23. package/src/hooks/handlers/auto-graduate.ts +169 -0
  24. package/src/hooks/handlers/inject-retrieval.ts +50 -0
  25. package/src/hooks/handlers/project-touch.ts +39 -0
  26. package/src/hooks/lib/context.ts +9 -8
  27. package/src/hooks/lib/paths.ts +2 -0
  28. package/src/hooks/lib/projects.ts +270 -0
  29. package/src/hooks/lib/retrieval-index.ts +223 -0
  30. package/src/hooks/lib/retrieval.ts +170 -0
  31. package/src/hooks/lib/security.ts +2 -0
  32. package/src/hooks/lib/stop.ts +9 -1
  33. package/src/hooks/lib/text-similarity.ts +13 -9
  34. package/src/hooks/lib/wisdom.ts +155 -1
  35. package/src/tools/agent/project.ts +336 -0
  36. package/src/tools/self-model.ts +3 -3
  37. package/src/tools/token-cost.ts +4 -4
  38. package/assets/templates/PAL/CONTEXT_ROUTING.md +0 -30
package/README.md CHANGED
@@ -82,6 +82,7 @@ pal cli status # check your setup
82
82
  | `pal cli import` | Import user state from a zip |
83
83
  | `pal cli status` | Show current PAL configuration |
84
84
  | `pal cli doctor` | Check prerequisites and system health |
85
+ | `pal cli usage` | Summarize token usage and estimated cost |
85
86
 
86
87
  ### Target flags
87
88
 
@@ -59,9 +59,10 @@ Authoring this way means: a malformed edit only takes down its own slide, slides
59
59
  Per-slide conventions (inside each file):
60
60
  - Speaker notes: lines starting with `Note:`.
61
61
  - Layout directive: `<!-- .slide: data-layout="..." -->` at the top.
62
+ - Image references: `![alt](../assets/foo.png)` — the natural relative path from `slides/*.md` to the deck's `assets/` folder. The build rewrites this to `assets/foo.png` in the concatenated `<deck-name>.md` (which sits at the deck root, so the path stays valid for direct preview), and inlines the image bytes as a `data:` URI in the HTML so the output is truly self-contained (emailable, USB-stickable). Remote refs (`https://…`) and `data:` URIs pass through untouched.
62
63
  - See "Layouts" below for the available layout names.
63
64
 
64
- Backwards compatible: if `slides/` doesn't exist, the build falls back to a single `<deck-dir>/content.md` with `---` separators between slides.
65
+ Backwards compatible: if `slides/` doesn't exist, the build falls back to a single `<deck-dir>/content.md` with `---` separators between slides. Image refs there should use `assets/foo.png` (root-relative), since `content.md` already lives at the deck root.
65
66
 
66
67
  ### Step 3: Build
67
68
 
@@ -69,12 +70,14 @@ Backwards compatible: if `slides/` doesn't exist, the build falls back to a sing
69
70
  bun ~/.pal/skills/presentation/tools/build.ts <deck-dir> [--out <dir>] [--force]
70
71
  ```
71
72
 
72
- Output goes to `<out>/<deck-name>/`, where `<deck-name>` = the basename of `<deck-dir>`:
73
+ Output files (where `<deck-name>` = basename of `<deck-dir>`):
73
74
 
74
75
  - `<deck-name>.md` — the concatenated source (all `slides/*.md` joined with `---` separators), written to disk **first** so you can inspect, diff, or feed it into other tooling.
75
76
  - `<deck-name>.html` — the self-contained presentation (CSS, JS, fonts, logo all inlined). Email it, USB-stick it, host it anywhere.
76
77
 
77
- Defaults: `--out` defaults to the current working directory. The `<deck-name>/` subdir is always created even when `--out` is explicit so multiple decks can coexist in one folder.
78
+ Default location: `--out` defaults to the **deck-dir itself**, and the files land flat at `<deck-dir>/<deck-name>.{html,md}` next to your slides, regardless of where you ran the build from. The scaffolder's `.gitignore` already covers them.
79
+
80
+ Override: pass `--out <dir>` to redirect elsewhere. When `--out` is *not* the deck-dir, a `<deck-name>/` subdir is created under it so multiple decks can coexist in one collection folder.
78
81
 
79
82
  `--force` overwrites an existing build. Without it, the build refuses to clobber `<deck-name>.{md,html}`.
80
83
 
@@ -90,7 +93,8 @@ Catches authoring failures before you ever open the browser:
90
93
  - Title or subtitle exceeding the visual budget (h1 > 60 chars, h2 > 100).
91
94
  - Layout-content mismatch — `comparison` without a `<div class="compare">`, `metric-grid` without `<div class="metrics">`, `two-column` missing column wrappers, etc.
92
95
  - Overflow heuristics — `agenda` > 10 items, `content` > 7 bullets, `code` block > 25 lines, `table` > 10 rows, `metric-grid` ≠ 3 metrics.
93
- - Image referenced via `![](assets/...)` but the file is missing.
96
+ - **Visual-line budget**: any bullet-bearing slide (`content`, `agenda`, `comparison`, `two-column`) with > 10 flattened list lines (top-level + sub-bullets combined). 10 fits cleanly; 11+ overflows even when each line is short.
97
+ - Image referenced via `![](../assets/...)` (slide-relative) or `![](assets/...)` (root-relative, for legacy `content.md` decks) but the file is missing.
94
98
  - Layout requirements — `big-stat` without an h1, `quote` / `pull-quote` without a `> blockquote`.
95
99
 
96
100
  Exit codes: `0` = clean, `1` = errors found (or warnings under `--strict`), `2` = usage error. Run before each build in your iteration loop, or wire it into a pre-commit hook.
@@ -112,10 +116,105 @@ Open `<out>/<deck-name>/<deck-name>.html` in your browser. Iterate by editing a
112
116
  └── assets/ # images / videos referenced from slides/*.md
113
117
  ```
114
118
 
115
- Build output is **never** written inside `<deck-dir>` it lands in `<out>/<deck-name>/` (default `<cwd>/<deck-name>/`). The scaffolder's `.gitignore` covers the case of running build from inside the deck-dir.
119
+ Build output lands flat inside the deck-dir by default (`<deck-dir>/<deck-name>.{html,md}`), or under `<out>/<deck-name>/` when `--out` is explicit. Both shapes are covered by the scaffolder's `.gitignore`.
116
120
 
117
121
  (Legacy: a single `content.md` at the deck root still works — see Step 2.)
118
122
 
123
+ ## Content principles
124
+
125
+ The doctor enforces *structural* discipline (slide budgets, layout-content match, missing assets). These rules are about *content* — what goes on the slide and in the notes. They apply to every deck regardless of type. For type-specific rules, see the "see also" links at the bottom of this section.
126
+
127
+ ### Core rules
128
+
129
+ - **A slide carries what the *learner* takes away — never what happens in the room.** "We will discuss CI/CD agents" is not a slide; "CodeRabbit and CodeAnt cost ~$15/dev/mo at this scale" is.
130
+ - **Less is more on bullets.** The doctor caps `content` at 7 bullets; the content rule is stricter — 3–5 is usually right. One bullet is not a slide; either expand or fold into the previous one.
131
+ - **Two ideas → two slides.** If a slide carries two takeaways, split.
132
+ - **No sentences on slides or in notes.** Exceptions: quotes, code comments, single callout sentences. Everything else is bullets, including notes.
133
+ - **A title-only slide is valid only as a `section` divider.** Single-word slides and single-bullet slides are not valid — split or expand.
134
+ - **Vocabulary consistency.** Pick one word per concept ("agent" vs "assistant", "tool" vs "capability") and use it everywhere. Drift confuses the audience.
135
+ - **Layout choice = content choice, not decoration.** `big-stat` says "this number *is* the point." `comparison` says "you have to choose between these." `quote` says "someone earned the right to say this." Wrong layout muddies the message.
136
+ - **Define jargon before using it.** First use of a non-obvious term gets a parenthesized gloss or its own glossary slide. After that, no.
137
+
138
+ ### Bullet & sub-bullet structure
139
+
140
+ - **Top-level bullet = one fact or one claim.** 6–12 words. If you need more, split or use a sub-bullet.
141
+ - **Sub-bullets = elaboration of the parent.** A list, an example, a clarification. 2–8 words each. Don't start a new claim at a sub-bullet — that's a top-level bullet.
142
+ - **Never use em-dash continuations to extend a bullet.** `- Foo — bar — baz` is prose pretending to be a bullet. Convert to:
143
+ ```markdown
144
+ - Foo
145
+ - bar
146
+ - baz
147
+ ```
148
+ Em-dash is reserved for: title qualifiers ("Block 4 — Landscape"), and anticipated Q&A in notes (`"Question?" — short answer`).
149
+ - **Stable parallel structure inside a bullet group.** If bullet 1 starts with a verb, bullets 2–N start with verbs. If bullet 1 is `Name: description`, the rest match.
150
+ - **Concrete over abstract.** Show paths, file names, numbers, command snippets in backticks. "the project file" is weaker than `./CLAUDE.md`.
151
+
152
+ ### Notes structure
153
+
154
+ Notes are the speaker's working surface during delivery. They must be scannable, not readable. Strict format:
155
+
156
+ 1. **Source links first**, one per line, before any explanation. Multiple links are fine if the slide cites multiple sources. The speaker should never scroll to find a link they're about to open in the browser.
157
+ 2. **Named beats as top-level bullets.** A "beat" is a named chunk of context — `Eval setup`, `Headline numbers`, `Why X wins`, `Common output`, `Anticipated questions`. The beat name lets the speaker jump.
158
+ 3. **Sub-bullets under each beat.** Same 2–8 word budget. Lists, edge cases, examples. No prose.
159
+ 4. **Quotes nest under a `- Quote` beat** as a markdown blockquote (`>`). Quotes are the only place full sentences are allowed in notes.
160
+ 5. **Anticipated Q&A near the end.** Format: `- "Question?" — short answer`. The em-dash here separates the quote from the answer; this is the legal use.
161
+ 6. **Forward references are terse.** "more in Day 2 when you build your own", "see Block 2 for X". Not "we'll discuss this later" prose.
162
+
163
+ ### Voice
164
+
165
+ - **Declarative, not hedging.** "Anthropic's holdout" beats "Anthropic has chosen a different approach." If you mean it, say it; if you don't, cut it.
166
+ - **Opinionated where the room expects opinion.** Workshops are taught, not narrated. State which option you'd pick and why.
167
+ - **Concrete identifiers visible.** File paths, version numbers, URLs, exact tool names. The audience should be able to type what's on the slide and have it work.
168
+
169
+ ### Examples
170
+
171
+ **Bad slide — describes the room, not the takeaway:**
172
+
173
+ ```markdown
174
+ <!-- .slide: data-layout="content" -->
175
+ ## Code Review Agents
176
+
177
+ - We will look at three vendors
178
+ - Pricing comparison
179
+ - Live demo of CodeRabbit
180
+ ```
181
+
182
+ **Good slide — what the learner walks away with:**
183
+
184
+ ```markdown
185
+ <!-- .slide: data-layout="content" -->
186
+ ## Code review agents — when each wins
187
+
188
+ - CodeRabbit: best for large PRs, weakest at C# specifics
189
+ - CodeAnt: tightest C# rules, no PR summary
190
+ - Custom (Claude Code in CI): controllable, you pay per token
191
+ ```
192
+
193
+ **Bad notes — prose, no source link:**
194
+
195
+ ```markdown
196
+ Note: We should talk about EchoLeak here. It was a vulnerability in Microsoft 365 Copilot discovered in 2025 that allowed prompt injection through email content. The attack worked by sending a crafted email that the Copilot assistant would later read and execute instructions from. This is a good example of why we need to think about prompt injection in agentic systems.
197
+ ```
198
+
199
+ **Good notes — link first, bullets, anticipated questions:**
200
+
201
+ ```markdown
202
+ Note:
203
+ - [EchoLeak (CVE-2025-32711) — Microsoft writeup](https://msrc.microsoft.com/...)
204
+ - Zero-click prompt injection in M365 Copilot via crafted email
205
+ - Attack surface: any agent that reads attacker-controlled content
206
+ - Mitigations
207
+ - Tool allowlists, not denylists
208
+ - Human-in-loop on data exfil tools
209
+ - "Could this happen with Claude Code?" — yes, via any tool that reads external text (web fetch, MCP server, log file)
210
+ ```
211
+
212
+ ### See also (type-specific rules)
213
+
214
+ - **Workshops, training, hands-on sessions:** [`WORKSHOP.md`](WORKSHOP.md)
215
+
216
+ (Future: `PITCH.md`, `LECTURE.md`, `INTERNAL_REVIEW.md` — coming soon.)
217
+
119
218
  ## Content conventions
120
219
 
121
220
  ```markdown
@@ -188,6 +287,26 @@ Composable on any `<img>` or wrapper element:
188
287
  | `image-duotone` | Brand-tinted monochrome via filter chain |
189
288
  | `image-overlay` | Adds a brand-gradient scrim from transparent → primary at 75% bottom |
190
289
 
290
+ ## Text-alignment utilities
291
+
292
+ Composable on any block element. Use these instead of inline `style="text-align: …"` — Reveal's print stylesheet forces `text-align: left !important` on every `div`/`p`/`ol`/`ul`, which silently kills inline alignment styles in print (the on-screen view looks fine, the printed PDF lands left-aligned).
293
+
294
+ | Class | Effect |
295
+ |---|---|
296
+ | `text-center` | Center inline content (typical use: wrapping a centered `<img>` or caption) |
297
+ | `text-right` | Right-align inline content |
298
+ | `text-left` | Left-align inline content (rarely needed; default) |
299
+
300
+ Example — centering an image on a `content` slide:
301
+
302
+ ```markdown
303
+ <div class="text-center">
304
+
305
+ ![Waldo](../assets/waldo.png)
306
+
307
+ </div>
308
+ ```
309
+
191
310
  ## Design tokens
192
311
 
193
312
  The skill ships a token system in `theme-base/base.css`. Templates only declare two anchors (`--brand-primary`, `--brand-accent`); the rest derives via `color-mix(in oklch, …)`.
@@ -0,0 +1,128 @@
1
+ # Workshop-specific content rules
2
+
3
+ Read [`SKILL.md`](SKILL.md) first — the general content principles apply unchanged. This file only adds the rules that are specific to workshops, training sessions, and hands-on formats.
4
+
5
+ A workshop is not a talk. The deck is scaffolding for the room's work, not the work itself. If the slides could be read alone and convey the value, it isn't a workshop — it's a recorded lecture.
6
+
7
+ ## Rules
8
+
9
+ - **Block arc: opener → core → demo/exercise → synthesis.** Every block follows this shape.
10
+ - **Opener** — one slide that earns the block: a stat, a story, a failure case. "Why should you care for the next 45 minutes?"
11
+ - **Core** — the actual material. The teaching slides.
12
+ - **Demo or exercise** — the participants do something or watch something done. Live, not recorded.
13
+ - **Synthesis** — one slide that closes the loop: "what you now know" or "what you can do tomorrow."
14
+ - Skip a beat and the block falls flat. Especially the opener.
15
+ - **Topic shape inside a block — preferred, not required: what → proof → mechanics → advanced → exercise.** A useful default ordering for depth topics. Skip any beat that doesn't fit the topic. Reorder if a different sequence teaches better. Variability is expected; this is the shape to fall back to when nothing better suggests itself.
16
+ - **What** — what it is + landscape framing (who built it, who supports it, who doesn't), if it matters.
17
+ - **Proof** — only if you have a *genuinely* interesting stat, eval, or incident. See the big-stat rule below.
18
+ - **Mechanics** — how it actually works: scopes, locations, semantics, limits.
19
+ - **Advanced** — patterns, extensions, teaser hooks for later blocks/days.
20
+ - **Exercise** — bullets are *principles* (learner takeaway), notes carry facilitation.
21
+ - **Big-stat is for genuinely interesting numbers, not a checkbox.** Use the `big-stat` layout when you have a stat that *changes the room's mind* — a real eval result, a striking incident metric, a non-obvious cost number. Don't manufacture one. A topic without a strong stat skips the proof slide; a generic stat dilutes every later one. Format when used: h1 = the number with `<em class="unit">`, h2 = the comparison-with-context (named alternatives, named source).
22
+ - **~50% slides / ~50% hands-on.** If deck-time exceeds half the block's runtime, cut slides — not exercise time.
23
+ - **Recall check at every block boundary.** A question slide between blocks: "Before we move on — what would you do if X?" One question, no answer, deliberate silence. Forces consolidation.
24
+ - **Question slides as discussion prompts.** Bold question, no answer, no bullets. Use sparingly (2–3 per day max) — overuse breaks the room's trust that you have answers.
25
+ - **Energy curve matters.**
26
+ - Hardest cognitive lift in the morning, before lunch.
27
+ - Live coding right after lunch is malpractice (post-meal energy crash).
28
+ - Demos and discussion in the afternoon.
29
+ - End each day on synthesis or a payoff demo, never on dense content.
30
+ - **Buffer / reserve slides per block.** Every block has 1–2 slides marked optional (in the deck, not separate) used only if pace allows. Lets you stretch comfortably without scrambling. Marker convention: filename suffix `-reserve` (e.g., `045-reserve-extra-eval-example.md`) and a `<!-- reserve -->` comment at the top of the slide so the doctor can flag if any are still flagged at build time.
31
+ - **Exercise slides follow a strict template.**
32
+ - **Title prefix `Exercise — `** so the room sees the mode change.
33
+ - **Bullets = principles, not procedure.** What the learner *takes away* by doing the exercise. Procedure ("5 minutes solo, then we compare two") goes in notes.
34
+ - **Standard note beats, in order:** `Facilitation`, `Common output`, `Common mistakes`, `Anticipated questions`. Use these names verbatim — the speaker scans by them.
35
+ - **Keep principles transferable.** A bullet that only makes sense in the context of this specific repo isn't a principle; demote it to a note example.
36
+
37
+ ## Examples
38
+
39
+ **Bad opener — describes the agenda instead of earning the block:**
40
+
41
+ ```markdown
42
+ <!-- .slide: data-layout="content" -->
43
+ ## Security block
44
+
45
+ - Prompt injection
46
+ - Tool aliasing
47
+ - Credential exposure
48
+ - Hook-based guardrails
49
+ ```
50
+
51
+ **Good opener — a real incident earns the next 90 minutes:**
52
+
53
+ ```markdown
54
+ <!-- .slide: data-layout="big-stat" -->
55
+ # 9 seconds
56
+ ## Replit production database, deleted by an agent
57
+
58
+ Note:
59
+ - [Replit prod-DB deletion — postmortem](https://...)
60
+ - Agent had unscoped DB credentials in its environment
61
+ - "Drop the dev tables" → matched against prod by mistake
62
+ - This is the failure mode the next 90 minutes prevent
63
+ - "Could Claude Code do this?" — yes, if you give it `.env` with prod creds
64
+ ```
65
+
66
+ **Bad recall slide — gives the answer:**
67
+
68
+ ```markdown
69
+ <!-- .slide: data-layout="content" -->
70
+ ## Recap
71
+
72
+ - Tier 1 = enterprise sanctioned
73
+ - Tier 2 = paid + settings off
74
+ - Tier 3 = prohibited
75
+ ```
76
+
77
+ **Good recall slide — forces the room to retrieve:**
78
+
79
+ ```markdown
80
+ <!-- .slide: data-layout="quote" -->
81
+ > Your colleague asks if she can paste a 20-line C# snippet
82
+ > from our payment service into ChatGPT Plus. What do you say?
83
+
84
+ Note:
85
+ - Let the room answer first, ~30 seconds of silence is fine
86
+ - Correct: depends on classification — if Confidential, no; if Internal, sanitize and yes (settings off)
87
+ - Don't give the answer until at least one person tries
88
+ ```
89
+
90
+ **Bad exercise slide — procedure on the slide, no transferable principle:**
91
+
92
+ ```markdown
93
+ <!-- .slide: data-layout="content" -->
94
+ ## Write a CLAUDE.md
95
+
96
+ - Open your favorite repo
97
+ - Spend 5 minutes writing CLAUDE.md
98
+ - We'll pick 2 to share
99
+ - Discuss what worked
100
+ ```
101
+
102
+ **Good exercise slide — principles on the slide, procedure in notes:**
103
+
104
+ ```markdown
105
+ <!-- .slide: data-layout="content" -->
106
+ ## Exercise — write your CLAUDE.md
107
+
108
+ - Capture conventions, not procedures (procedures belong in skills)
109
+ - One fact per line, no prose paragraphs
110
+ - Glossary entries earn their place — only project-specific terms
111
+ - Order matters — most-violated rules first, model reads top-down
112
+ - Commit it; use `.local.md` only for personal overrides
113
+
114
+ Note:
115
+ - Facilitation
116
+ - 5 min solo on a repo they know
117
+ - 2 volunteers show on screen
118
+ - group critiques against the failure modes from prior slides
119
+ - Common output
120
+ - "Use Polly for retries, not custom code"
121
+ - "Tests live in `tests/Unit/`, not `src/__tests__/`"
122
+ - Common mistakes
123
+ - writing instructions that should be skills
124
+ - copying a generic style guide instead of capturing real violations
125
+ - Anticipated questions
126
+ - "Should this be in AGENTS.md or CLAUDE.md?" — symlink and stop choosing
127
+ - "What if there's already one?" — diff against it; usually adds 30%
128
+ ```
@@ -244,6 +244,14 @@
244
244
  .reveal tr:nth-child(even) td { background: var(--brand-surface); }
245
245
  .reveal tr:last-child td { border-bottom: 0; }
246
246
 
247
+ /* ── Text alignment utilities — composable on any block element.
248
+ * `!important` is needed because Reveal's print stylesheet forces
249
+ * `text-align: left !important` on every div/p/ol/ul, which would otherwise
250
+ * override author intent (inline styles or any non-`!important` rule). */
251
+ .reveal .text-center { text-align: center !important; }
252
+ .reveal .text-right { text-align: right !important; }
253
+ .reveal .text-left { text-align: left !important; }
254
+
247
255
  /* ── Image utilities (composable on any <img> or wrapper) */
248
256
  .reveal .image-rounded img,
249
257
  .reveal img.image-rounded { border-radius: var(--radius-md); }
@@ -323,3 +331,108 @@
323
331
  color: var(--brand-accent);
324
332
  height: 2px;
325
333
  }
334
+
335
+ /* ── Print
336
+ * Reveal's vendored print stylesheet (`@media print { html:not(.print-pdf) … }`)
337
+ * forces every heading to `color:#000!important` and `p/li/td` to `#000`. That
338
+ * inverts the white-on-brand text we use on `section` and `closing` dividers
339
+ * (whose gradient background is on the section element itself, so it still
340
+ * renders blue) — producing black-on-blue when the user hits Cmd+P without
341
+ * `?print-pdf`. Restore brand text colors here, and force color-adjust:exact
342
+ * so the gradient prints reliably. */
343
+ @media print {
344
+ .reveal section[data-layout="section"],
345
+ .reveal section[data-layout="closing"] {
346
+ -webkit-print-color-adjust: exact !important;
347
+ print-color-adjust: exact !important;
348
+ }
349
+ .reveal section[data-layout="section"] h1,
350
+ .reveal section[data-layout="section"] h4,
351
+ .reveal section[data-layout="closing"] h1 {
352
+ color: #fff !important;
353
+ }
354
+ .reveal section[data-layout="section"] h2,
355
+ .reveal section[data-layout="section"] h3 {
356
+ color: rgba(255, 255, 255, 0.7) !important;
357
+ }
358
+ .reveal section[data-layout="section"] p,
359
+ .reveal section[data-layout="section"] li,
360
+ .reveal section[data-layout="closing"] h2,
361
+ .reveal section[data-layout="closing"] p,
362
+ .reveal section[data-layout="closing"] li {
363
+ color: rgba(255, 255, 255, 0.85) !important;
364
+ }
365
+ .reveal section[data-layout="title"] h1 {
366
+ color: var(--brand-primary) !important;
367
+ }
368
+
369
+ /* ── Cmd+P scope (browser print on the regular URL, NOT ?print-pdf)
370
+ * Reveal forces every section to `display:block; position:static; padding:60px 20px;
371
+ * transform:none`, which strips the flex centering from title/section/closing/quote/
372
+ * big-stat/pull-quote and leaves their content top-aligned. Restore centering, then
373
+ * frame every printed page with a hairline so it reads like a printed sheet. */
374
+
375
+ /* Hairline frame — inset so it lives inside the printable area and survives
376
+ * the printer's unprintable-margin clipping. */
377
+ html:not(.print-pdf) .reveal .slides section {
378
+ box-shadow: inset 0 0 0 1px var(--neutral-300) !important;
379
+ }
380
+
381
+ /* Make every section fill the printable page. Two effects, no layout
382
+ * coercion:
383
+ * 1. The inset frame above wraps the full page edge instead of
384
+ * shrink-wrapping content.
385
+ * 2. The flex-centered layouts below have a tall container to center
386
+ * within (without a tall container, `justify-content: center` has
387
+ * nothing to center against). */
388
+ html:not(.print-pdf) .reveal .slides section {
389
+ min-height: 100vh !important;
390
+ box-sizing: border-box !important;
391
+ }
392
+
393
+ /* Apply `--table-scale` to td/th in print. Reveal's print stylesheet forces
394
+ * `font-size: 20pt !important` on all td, which would otherwise override
395
+ * the screen rule. Scale font AND padding together — scaling font alone
396
+ * leaves the row height dominated by static cell padding, dampening the
397
+ * shrink effect. */
398
+ html:not(.print-pdf) .reveal section[data-layout="table"] td,
399
+ html:not(.print-pdf) .reveal section[data-layout="table"] th {
400
+ font-size: calc(20pt * var(--table-scale, 1)) !important;
401
+ padding: calc(0.5rem * var(--table-scale, 1)) calc(1rem * var(--table-scale, 1)) !important;
402
+ }
403
+
404
+ /* Reveal's print stylesheet forces every section to `display: block`,
405
+ * stripping the on-screen flex centering from the layouts that use it.
406
+ * Restore `display: flex` for those layouts ONLY — `flex-direction`,
407
+ * `justify-content`, and `align-items` come from layouts.css unchanged.
408
+ * Layouts not listed here stay as `display: block` and sit at the top of
409
+ * the page; that preserves the natural flow of layouts that depend on it
410
+ * (image-text and two-column rely on inline-block siblings, content/agenda/
411
+ * comparison/metric-grid/code/table all flow as block stacks). */
412
+ html:not(.print-pdf) .reveal .slides section[data-layout="title"],
413
+ html:not(.print-pdf) .reveal .slides section[data-layout="section"],
414
+ html:not(.print-pdf) .reveal .slides section[data-layout="closing"],
415
+ html:not(.print-pdf) .reveal .slides section[data-layout="quote"],
416
+ html:not(.print-pdf) .reveal .slides section[data-layout="big-stat"],
417
+ html:not(.print-pdf) .reveal .slides section[data-layout="pull-quote"] {
418
+ display: flex !important;
419
+ }
420
+ }
421
+
422
+ /* ── Print-view polish (`?print-pdf` URL — Reveal stacks each slide as a `.pdf-page`)
423
+ * Reveal sizes `.pdf-page` to slide dims and stacks them flush-left with no
424
+ * separation. Center each one and draw a hairline frame so the print preview
425
+ * reads like a sheaf of printed pages, not a left-aligned wall of slides.
426
+ *
427
+ * Why box-shadow inset and not `border`: Reveal forces `@page { margin: 0 }`,
428
+ * so an outside border would land at the edge of paper and be clipped by the
429
+ * printer's unprintable margin. An inset shadow draws the line *inside* the
430
+ * page box and survives the print. */
431
+ html.print-pdf .reveal .slides .pdf-page {
432
+ box-shadow: inset 0 0 0 1px var(--neutral-300);
433
+ }
434
+ @media screen {
435
+ html.print-pdf .reveal .slides .pdf-page {
436
+ margin: 16px auto !important;
437
+ }
438
+ }
@@ -213,9 +213,13 @@
213
213
  letter-spacing: var(--tracking-wide);
214
214
  }
215
215
 
216
- /* ── 9. table ── styling already in base; just ensure breathing room */
216
+ /* ── 9. table ── styling already in base; just ensure breathing room.
217
+ * `--table-scale` is set per-slide by build.ts (`injectTableScale`) when row
218
+ * count exceeds 6 — multiplies the cell font-size to keep dense tables on a
219
+ * single page. Defaults to 1 (no change) when the var is unset. */
217
220
  .reveal section[data-layout="table"] table {
218
221
  margin-top: var(--space-3);
222
+ font-size: calc(var(--text-sm) * var(--table-scale, 1));
219
223
  }
220
224
 
221
225
  /* ── 10. comparison ── 2-3 option boxes with numbered badge + top stripe */
@@ -265,7 +269,12 @@
265
269
  max-height: 65vh;
266
270
  margin: var(--space-2) 0;
267
271
  }
268
- .reveal section[data-layout="code"] pre code { font-size: 0.9em; }
272
+ /* Code font size scales down for long blocks. The build script sets
273
+ --code-scale on each code-layout section based on line count: 1.0 for ≤15
274
+ lines, 0.6 for ≥25 lines, linear in between. Defaults to 1.0 if unset. */
275
+ .reveal section[data-layout="code"] pre code {
276
+ font-size: calc(0.9em * var(--code-scale, 1));
277
+ }
269
278
 
270
279
  /* ── 12. big-stat ── One giant number + a muted caption beneath. Use sparingly.
271
280
  * Author surface:
@@ -8,7 +8,8 @@
8
8
  // <out>/<deck-name>/<deck-name>.md concatenated slides (written first)
9
9
  // <out>/<deck-name>/<deck-name>.html self-contained presentation
10
10
  //
11
- // --out defaults to process.cwd(). The deck-name subdir is always created
11
+ // --out defaults to the deck-dir itself (output lands at <deck-dir>/<deck-name>/,
12
+ // which the scaffolder gitignores). The deck-name subdir is always created
12
13
  // inside --out, even when --out is explicitly provided. Existing files in
13
14
  // the subdir are preserved unless --force is passed.
14
15
 
@@ -19,6 +20,71 @@ import { dataUri, escapeForTextarea, readText } from "./lib/inline";
19
20
  import { THEME_BASE, VENDOR_REVEAL } from "./lib/paths";
20
21
  import { getTemplate } from "./lib/registry";
21
22
 
23
+ // Walk markdown line-by-line, skipping fenced code blocks, applying `transform`
24
+ // to each non-fenced line. Used by both the concat-step path rewrite and the
25
+ // HTML-step image inliner so neither touches example image syntax inside ``` blocks.
26
+ async function mapMarkdownOutsideFences(
27
+ md: string,
28
+ transform: (line: string) => string | Promise<string>
29
+ ): Promise<string> {
30
+ const out: string[] = [];
31
+ let inFence = false;
32
+ for (const line of md.split("\n")) {
33
+ if (/^```/.test(line)) {
34
+ inFence = !inFence;
35
+ out.push(line);
36
+ continue;
37
+ }
38
+ out.push(inFence ? line : await transform(line));
39
+ }
40
+ return out.join("\n");
41
+ }
42
+
43
+ // Rewrite `../assets/X` (the natural relative path from a `slides/*.md` file)
44
+ // to `assets/X` so the concatenated markdown — which lives at the deck root —
45
+ // resolves images correctly when previewed directly. Bare `X.png` references
46
+ // (no path) are left to the doctor to flag; we don't guess where they live.
47
+ function rewriteImageRefsForConcat(md: string): Promise<string> {
48
+ return mapMarkdownOutsideFences(md, (line) =>
49
+ line.replace(/(!\[[^\]]*\]\()([^)]+)(\))/g, (whole, open, ref, close) => {
50
+ const trimmed = ref.trim();
51
+ if (/^(https?:|data:)/i.test(trimmed)) return whole;
52
+ if (trimmed.startsWith("../assets/")) {
53
+ return `${open}${trimmed.slice(3)}${close}`;
54
+ }
55
+ return whole;
56
+ })
57
+ );
58
+ }
59
+
60
+ // Inline every local image reference in the concatenated markdown as a data: URI
61
+ // so the resulting HTML is truly self-contained (emailable, USB-stickable).
62
+ // Resolves refs against `deckDir` (the concat-md's location). Missing files are
63
+ // left untouched — the doctor flags them; build doesn't crash on author errors.
64
+ async function inlineImagesInMarkdown(md: string, deckDir: string): Promise<string> {
65
+ return mapMarkdownOutsideFences(md, async (line) => {
66
+ const matches = [...line.matchAll(/(!\[[^\]]*\]\()([^)]+)(\))/g)];
67
+ if (matches.length === 0) return line;
68
+ let result = line;
69
+ for (const m of matches) {
70
+ const [whole, open, ref, close] = m;
71
+ const trimmed = ref.trim();
72
+ if (/^(https?:|data:)/i.test(trimmed)) continue;
73
+ const abs = resolve(deckDir, trimmed);
74
+ try {
75
+ await access(abs, fsConst.F_OK);
76
+ } catch {
77
+ continue;
78
+ }
79
+ // dataUri returns `url("data:...")` for CSS use; strip the `url("…")` wrapper for <img>.
80
+ const wrapped = await dataUri(abs);
81
+ const inner = wrapped.replace(/^url\("/, "").replace(/"\)$/, "");
82
+ result = result.replace(whole, `${open}${inner}${close}`);
83
+ }
84
+ return result;
85
+ });
86
+ }
87
+
22
88
  const ASPECTS: Record<string, [number, number]> = {
23
89
  "16:9": [1920, 1080],
24
90
  "16:10": [1920, 1200],
@@ -65,6 +131,62 @@ function deckSlug(deckDir: string): string {
65
131
  return slug || "deck";
66
132
  }
67
133
 
134
+ // Build-time injection: code-layout slides with > 15 lines of code get a
135
+ // `style="--code-scale: X"` attribute baked into the slide directive. CSS in
136
+ // layouts.css multiplies the base code font by this variable. Linear from 1.0
137
+ // at 15 lines to 0.6 at 25 lines; clamped at 0.6 beyond. Build-time keeps the
138
+ // attribute on the rendered <section>, so navigation/Highlight re-runs cannot
139
+ // strip it.
140
+ function injectCodeScale(slideMarkdown: string): string {
141
+ const layoutRe = /<!--\s*\.slide:\s*data-layout="code"([^>]*)-->/i;
142
+ const layoutMatch = layoutRe.exec(slideMarkdown);
143
+ if (!layoutMatch) return slideMarkdown;
144
+
145
+ const codeRe = /```[^\n]*\n([\s\S]*?)\n```/;
146
+ const codeMatch = codeRe.exec(slideMarkdown);
147
+ if (!codeMatch) return slideMarkdown;
148
+
149
+ const lines = codeMatch[1].split("\n").length;
150
+ if (lines <= 15) return slideMarkdown;
151
+
152
+ const scale = Math.max(0.6, 1 - (lines - 15) * 0.04).toFixed(2);
153
+ // Don't double-inject if a previous build already set it.
154
+ const extras = layoutMatch[1].replace(/\s+style="--code-scale:\s*[^"]+"/i, "").trim();
155
+ const attrs = extras
156
+ ? `${extras} style="--code-scale: ${scale}"`
157
+ : `style="--code-scale: ${scale}"`;
158
+ const newDirective = `<!-- .slide: data-layout="code" ${attrs} -->`;
159
+ return slideMarkdown.replace(layoutMatch[0], newDirective);
160
+ }
161
+
162
+ // Build-time injection: table-layout slides with > 4 rows get a
163
+ // `style="--table-scale: X"` attribute baked into the slide directive. CSS
164
+ // multiplies cell font-size AND cell padding by this var so both shrink
165
+ // together (font-only shrinking is dampened by static padding). Linear from
166
+ // 1.0 at 4 rows to 0.6 at ≥10 rows. Mirrors `injectCodeScale`.
167
+ function injectTableScale(slideMarkdown: string): string {
168
+ const layoutRe = /<!--\s*\.slide:\s*data-layout="table"([^>]*)-->/i;
169
+ const layoutMatch = layoutRe.exec(slideMarkdown);
170
+ if (!layoutMatch) return slideMarkdown;
171
+
172
+ // Count markdown table rows (lines starting with `|`) excluding the
173
+ // separator (`| --- | --- |`) which doesn't render as a row.
174
+ const sepRe = /^\s*\|(\s*:?-+:?\s*\|)+\s*$/;
175
+ let rows = 0;
176
+ for (const line of slideMarkdown.split("\n")) {
177
+ if (/^\s*\|/.test(line) && !sepRe.test(line)) rows++;
178
+ }
179
+ if (rows <= 4) return slideMarkdown;
180
+
181
+ const scale = Math.max(0.6, 1 - (rows - 4) * 0.067).toFixed(2);
182
+ const extras = layoutMatch[1].replace(/\s+style="--table-scale:\s*[^"]+"/i, "").trim();
183
+ const attrs = extras
184
+ ? `${extras} style="--table-scale: ${scale}"`
185
+ : `style="--table-scale: ${scale}"`;
186
+ const newDirective = `<!-- .slide: data-layout="table" ${attrs} -->`;
187
+ return slideMarkdown.replace(layoutMatch[0], newDirective);
188
+ }
189
+
68
190
  async function buildConcat(deckDir: string): Promise<string> {
69
191
  const slidesDir = join(deckDir, "slides");
70
192
  if (await exists(slidesDir)) {
@@ -73,7 +195,8 @@ async function buildConcat(deckDir: string): Promise<string> {
73
195
  throw new Error(`slides/ is empty at ${slidesDir}`);
74
196
  }
75
197
  const parts = await Promise.all(files.map((f) => readText(join(slidesDir, f))));
76
- return `${parts.map((p) => p.trim()).join("\n\n---\n\n")}\n`;
198
+ const joined = `${parts.map((p) => injectTableScale(injectCodeScale(p.trim()))).join("\n\n---\n\n")}\n`;
199
+ return rewriteImageRefsForConcat(joined);
77
200
  }
78
201
  const legacy = join(deckDir, "content.md");
79
202
  if (await exists(legacy)) {
@@ -90,7 +213,7 @@ async function main() {
90
213
  }
91
214
  const deckDir = resolve(argv[0]);
92
215
 
93
- let outRoot = process.cwd();
216
+ let outRoot = deckDir;
94
217
  let force = false;
95
218
  for (let i = 1; i < argv.length; i++) {
96
219
  if (argv[i] === "--out") outRoot = resolve(argv[++i]);
@@ -110,7 +233,10 @@ async function main() {
110
233
  }
111
234
 
112
235
  const slug = deckSlug(deckDir);
113
- const outDir = join(outRoot, slug);
236
+ // When --out is the deck-dir itself (the default), write directly into it —
237
+ // no extra <slug>/ subdir. Otherwise create the subdir so multiple decks
238
+ // can coexist under one shared --out.
239
+ const outDir = resolve(outRoot) === deckDir ? deckDir : join(outRoot, slug);
114
240
  const concatPath = join(outDir, `${slug}.md`);
115
241
  const htmlPath = join(outDir, `${slug}.html`);
116
242
 
@@ -145,7 +271,7 @@ async function main() {
145
271
  const skeleton = await readText(join(THEME_BASE, "skeleton.html"));
146
272
  const revealCss = await readText(join(VENDOR_REVEAL, "reveal.css"));
147
273
  const highlightCss = await readText(
148
- join(VENDOR_REVEAL, "plugin", "highlight", "monokai.css")
274
+ join(VENDOR_REVEAL, "plugin", "highlight", "github-dark.css")
149
275
  );
150
276
  const revealJs = await readText(join(VENDOR_REVEAL, "reveal.js"));
151
277
  const markdownJs = await readText(
@@ -156,7 +282,11 @@ async function main() {
156
282
  );
157
283
  const notesJs = await readText(join(VENDOR_REVEAL, "plugin", "notes", "notes.js"));
158
284
 
159
- const contentMd = await readFile(concatPath, "utf8");
285
+ // Read the concat back from disk, then inline image refs as data: URIs so
286
+ // the HTML is self-contained. The on-disk concat keeps plain `assets/X` paths
287
+ // for direct markdown preview; only the HTML embeds full image bytes.
288
+ const contentMdRaw = await readFile(concatPath, "utf8");
289
+ const contentMd = await inlineImagesInMarkdown(contentMdRaw, deckDir);
160
290
 
161
291
  let deckOverridesCss = "";
162
292
  const overridesPath = join(deckDir, "overrides.css");