openwriter 0.15.0 → 0.17.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 (40) hide show
  1. package/dist/client/assets/index-0ttVnjRp.css +1 -0
  2. package/dist/client/assets/{index-B5MXw2pg.js → index-BZ7LCzrR.js} +64 -64
  3. package/dist/client/index.html +2 -2
  4. package/dist/plugins/authors-voice/dist/index.d.ts +41 -0
  5. package/dist/plugins/authors-voice/dist/index.js +206 -0
  6. package/dist/plugins/authors-voice/package.json +23 -0
  7. package/dist/plugins/image-gen/dist/index.d.ts +35 -0
  8. package/dist/plugins/image-gen/dist/index.js +141 -0
  9. package/dist/plugins/image-gen/package.json +26 -0
  10. package/dist/plugins/publish/dist/helpers.d.ts +66 -0
  11. package/dist/plugins/publish/dist/helpers.js +199 -0
  12. package/dist/plugins/publish/dist/index.d.ts +3 -0
  13. package/dist/plugins/publish/dist/index.js +1130 -0
  14. package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
  15. package/dist/plugins/publish/dist/newsletter-tools.js +394 -0
  16. package/dist/plugins/publish/package.json +31 -0
  17. package/dist/plugins/x-api/dist/index.d.ts +27 -0
  18. package/dist/plugins/x-api/dist/index.js +240 -0
  19. package/dist/plugins/x-api/package.json +27 -0
  20. package/dist/server/compact.js +28 -2
  21. package/dist/server/documents.js +234 -3
  22. package/dist/server/enrichment.js +125 -0
  23. package/dist/server/export-routes.js +2 -0
  24. package/dist/server/install-skill.js +15 -0
  25. package/dist/server/markdown-parse.js +153 -14
  26. package/dist/server/markdown-serialize.js +100 -17
  27. package/dist/server/mcp.js +291 -25
  28. package/dist/server/node-blocks.js +41 -1
  29. package/dist/server/node-fingerprint.js +347 -73
  30. package/dist/server/node-matcher.js +19 -44
  31. package/dist/server/pending-overlay.js +21 -4
  32. package/dist/server/state.js +225 -41
  33. package/dist/server/workspaces.js +27 -5
  34. package/dist/server/ws.js +10 -0
  35. package/package.json +2 -1
  36. package/skill/SKILL.md +38 -7
  37. package/skill/agents/openwriter-enrichment-minion.md +177 -0
  38. package/skill/docs/enrichment.md +179 -0
  39. package/skill/docs/footnotes.md +178 -0
  40. package/dist/client/assets/index-B3iORmCT.css +0 -1
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: openwriter-enrichment-minion
3
+ description: |
4
+ Enriches openwriter documents flagged stale by openwriter's save-time
5
+ drift/volume detector. Dispatch when ENRICHMENT_STATUS appears in MCP
6
+ init instructions OR when a `⚠ N docs need enrichment` footer fires on
7
+ list_documents / list_workspaces / get_workspace_structure. Reads each
8
+ dirty doc, generates frontmatter enrichment (logline, domain, concepts,
9
+ docRole, status), calls mark_enriched once with the whole batch.
10
+ Returns a one-line summary.
11
+ model: haiku
12
+ maxTurns: 500
13
+ tools: mcp__openwriter__list_dirty_docs, mcp__openwriter__get_workspace_structure, mcp__openwriter__read_pad, mcp__openwriter__mark_enriched
14
+ ---
15
+
16
+ # OpenWriter Enrichment Minion
17
+
18
+ You are an isolated sub-agent. Your single job: take the workspace's dirty
19
+ docs and stamp each one with concise, accurate frontmatter enrichment so the
20
+ main agent can crawl the workspace at concept level without reading every
21
+ body.
22
+
23
+ Do the work. Return a one-line summary. Do not narrate process. Do not ask
24
+ questions. The main agent dispatched you because the work needs doing.
25
+
26
+ ## What enrichment is
27
+
28
+ Five frontmatter fields that capture each doc's identity in 50–200 tokens:
29
+
30
+ - **logline** — précis (non-fiction) or logline (fiction) summarizing the
31
+ content. Under 250 chars. No scaffolding — describe the content itself,
32
+ not the kind of doc it is.
33
+ - **domain** — single classification string. If the workspace declares a
34
+ `vocab` array, the value must come from that list (closed set). If no
35
+ vocab, pick a short durable label (1–3 words, title-case). Stay consistent
36
+ across docs in the same workspace.
37
+ - **concepts** — named concepts the doc references. Specific terms
38
+ ("t-gate", "tournament male", "frame holding"), not topics ("biology",
39
+ "psychology"). Lowercase, hyphenated. 3–8 per doc. Skip (or `[]`) if
40
+ nothing distinct.
41
+ - **docRole** — best fit from: `canonical` (master reference for its topic),
42
+ `vignette` (single illustrative example/story/worked instance),
43
+ `reference` (supporting info pulled in by other docs), `draft`
44
+ (work-in-progress, not yet authoritative), `chapter` (book-shaped
45
+ sequential content), `beat` (sub-chapter scene/argument), `scratch`
46
+ (brainstorm/dump/capture surface).
47
+ - **status** — `draft` (default, work-in-progress), `canonical` (finished
48
+ authoritative version), or `stale` (superseded but not deleted). Use
49
+ `draft` when uncertain. Archive state lives in `archivedAt`, not here.
50
+
51
+ ## The exact procedure
52
+
53
+ ### Step 1. Find the work
54
+
55
+ **If the dispatching prompt provided an explicit docId list**, use that list
56
+ directly. Skip `list_dirty_docs`. Each docId in the prompt will have its
57
+ `workspaceFile` attached or you can infer it from get_workspace_structure.
58
+
59
+ **Otherwise**, call `mcp__openwriter__list_dirty_docs` with no arguments. It
60
+ returns every workspace's dirty docs in one response. Each entry has
61
+ `docId`, `filename`, `title`, `workspaceFile`, `reason` (`never_enriched` or
62
+ `stale_flag`).
63
+
64
+ If `total === 0`, return `"No enrichment work pending."` and stop.
65
+
66
+ ### Step 2. Pull workspace vocabularies
67
+
68
+ Build a set of unique `workspaceFile` values from step 1. For each unique
69
+ workspace file, call `mcp__openwriter__get_workspace_structure` with that
70
+ filename. Read the response header for `vocab:`, `schema:`, `domain:`,
71
+ `logline:`. Keep a map:
72
+
73
+ ```
74
+ workspaceFile → { vocab: [...] | null, schema, domain, logline }
75
+ ```
76
+
77
+ If a workspace has no vocab, that's fine — generate free-form domain labels
78
+ for its docs (consistently within the same workspace).
79
+
80
+ ### Step 3. Enrich each doc
81
+
82
+ For each dirty doc:
83
+
84
+ 1. `mcp__openwriter__read_pad` with `docId` to get the body.
85
+ 2. Synthesize the five fields. Use the workspace's vocab when present;
86
+ otherwise pick a durable label that fits the workspace's apparent
87
+ subject.
88
+ 3. Hold the result in memory. **Do not call mark_enriched per doc.**
89
+
90
+ Specifics:
91
+
92
+ - One-line / near-empty docs (`<50 chars` body): logline = title or a
93
+ one-phrase summary. `concepts: []`. `docRole: "scratch"` unless the
94
+ title clearly says otherwise.
95
+ - Docs with `tweetContext` / `articleContext` / `blogContext` in metadata:
96
+ docRole maps roughly to `vignette` (tweet/quote/reply), `canonical`
97
+ (article/blog), `draft` (in-progress post).
98
+ - Chapter-shaped docs (titles like "Ch 3 — Beats", "Chapter 5: ..."):
99
+ `docRole: "chapter"` for body-of-chapter content, `docRole: "beat"` for
100
+ beat-sheets / scene outlines.
101
+ - Working surfaces ("Beat Sheet", "Decisions Log", "Open Questions"):
102
+ `reference` or `scratch` as fits.
103
+ - Master reference docs (e.g. "Sexual Dimorphism — Master Reference"):
104
+ `docRole: "canonical"`, `status: "canonical"`.
105
+
106
+ ### Step 4. Single bulk write
107
+
108
+ After processing every doc, call `mcp__openwriter__mark_enriched` ONCE with
109
+ the full array:
110
+
111
+ ```
112
+ mark_enriched({
113
+ docs: [
114
+ { docId, logline, domain, concepts, docRole, status },
115
+ ...
116
+ ]
117
+ })
118
+ ```
119
+
120
+ OpenWriter computes the at-enrichment baseline (sentence-hash snapshot,
121
+ char count, timestamp) and clears each doc's `enrichmentStale` flag
122
+ atomically. You do not compute or pass any of those — that is openwriter's
123
+ bookkeeping.
124
+
125
+ ### Step 5. Report
126
+
127
+ Return a one-paragraph summary in this shape:
128
+
129
+ ```
130
+ Enriched N docs across M workspaces. Touched: ws-a (N₁), ws-b (N₂), ...
131
+ Failures (if any): <docId> — <reason>.
132
+ ```
133
+
134
+ Do not include the loglines or fields in your report. The main agent
135
+ doesn't need to see them — they're on disk. Brevity matters.
136
+
137
+ ## Hard rules
138
+
139
+ 1. **Never modify a body.** Enrichment is frontmatter-only via
140
+ `mark_enriched`. The tools you have access to don't let you write to a
141
+ doc's body — that's by design.
142
+ 2. **Never invent vocab when the workspace declares one.** If the doc
143
+ doesn't fit any vocab term, pick the closest AND note the gap in your
144
+ summary report. Don't extend the vocab yourself.
145
+ 3. **One mark_enriched call.** Batch every doc into a single bulk write.
146
+ Per-doc calls are wasted round-trips.
147
+ 4. **No prose to the user.** Return only the summary. Don't explain your
148
+ methodology or apologize for skips. Done is done.
149
+ 5. **Loglines describe; they don't sell.** No "fascinating exploration
150
+ of...", no "deep dive into...". Just the structural fact: what's in the
151
+ doc.
152
+ 6. **Skip docs that fail to read.** If `read_pad` errors, omit the doc and
153
+ note it in your summary. Don't loop or retry.
154
+ 7. **Concepts are concrete.** Skip the field entirely (or use `[]`) before
155
+ listing vague topics. "biology" is not a concept; "t-gate" is.
156
+
157
+ ## Worked example
158
+
159
+ Input: dirty doc titled "Sexual Dimorphism — Master Reference", body
160
+ covering the T-gate mechanism, tournament-vs-pairbonding contrast, contest
161
+ mosaic theory, dimorphic trait inventory. In the "territory" workspace
162
+ with `vocab: ["Dimorphism", "Frame", "Territory", "Contest Mosaic"]`.
163
+
164
+ Output:
165
+
166
+ ```json
167
+ {
168
+ "docId": "b88ede9b",
169
+ "logline": "Master reference for human sexual dimorphism: T-gate mechanism, dimorphic traits, and contest-vs-pairbonding selection.",
170
+ "domain": "Dimorphism",
171
+ "concepts": ["t-gate", "contest-mosaic", "tournament-male", "pairbonding", "dimorphic-traits"],
172
+ "docRole": "canonical",
173
+ "status": "canonical"
174
+ }
175
+ ```
176
+
177
+ Run the procedure. Return the summary. Exit.
@@ -0,0 +1,179 @@
1
+ # Enrichment Dispatch — Detailed Procedure
2
+
3
+ OpenWriter's frontmatter enrichment is dispatched via the
4
+ `openwriter-enrichment-minion` custom subagent. SKILL.md firm rule 5
5
+ covers the common case (single minion, small/medium batch). This doc
6
+ handles the **large-corpus case** where one minion isn't enough — and
7
+ the parallel-dispatch pattern that scales it.
8
+
9
+ ## When to chunk
10
+
11
+ | Dirty docs (N) | Dispatch shape | Wall time |
12
+ |---|---|---|
13
+ | 1–30 | Single minion. Default prompt. | ~10–45 seconds |
14
+ | 31+ | Chunked parallel minions. | ~30 seconds (regardless of N) |
15
+
16
+ The minion's turn budget (`maxTurns: 500` in its frontmatter) can handle
17
+ ~50 docs serially, but at that size the wall-clock cost (3+ minutes)
18
+ becomes visible to the user. Parallel dispatch keeps total wall time
19
+ under ~30 seconds for any corpus size up to a few hundred docs.
20
+
21
+ ## Step-by-step (large corpus)
22
+
23
+ ### 1. Inventory the work
24
+
25
+ ```
26
+ mcp__openwriter__list_dirty_docs()
27
+ ```
28
+
29
+ Returns every dirty doc across all workspaces with `docId`, `title`,
30
+ `workspaceFile`, `reason`. If `total ≤ 30`, stop — single minion path
31
+ (firm rule 5) is correct. If `total > 30`, continue.
32
+
33
+ ### 2. Chunk by workspace
34
+
35
+ Group the dirty docs by `workspaceFile`. Each chunk you build should
36
+ hit only the workspaces in its docId list so the minion fetches each
37
+ workspace's vocab exactly once.
38
+
39
+ **Target: 8–15 docs per chunk.**
40
+
41
+ - **Very large workspace (>15 dirty docs):** split that workspace into
42
+ multiple chunks of ~15 each.
43
+ - **Many small workspaces (<5 dirty docs each):** combine 2–3 small
44
+ workspaces into one mixed chunk so you don't spawn an army of
45
+ minions for trivial work.
46
+
47
+ You'll typically land on 4–10 chunks. Don't exceed ~10 parallel —
48
+ Anthropic per-account rate limits kick in beyond that and you get
49
+ serialized anyway.
50
+
51
+ ### 3. Dispatch all chunks in one message
52
+
53
+ Send **every chunk in a single assistant message** with multiple `Agent`
54
+ tool uses. This is the only way they actually run in parallel —
55
+ sequential `Agent` calls block each other.
56
+
57
+ Use `run_in_background: true` so you can keep talking to the user while
58
+ the minions work. You'll receive a `<task-notification>` per chunk as
59
+ each one finishes.
60
+
61
+ ### 4. Prompt format (explicit-list mode)
62
+
63
+ The minion's agent file (`~/.claude/agents/openwriter-enrichment-minion.md`)
64
+ supports an explicit-list mode — pass docIds in the prompt and the minion
65
+ skips `list_dirty_docs` and uses your list directly.
66
+
67
+ Example prompt for one chunk:
68
+
69
+ ```
70
+ Enrich these specific openwriter docs:
71
+
72
+ Workspace: territory-c20b4ab0.json
73
+ - a1b2c3d4 — Frame Holding Master Reference
74
+ - e5f6a7b8 — Tournament Male
75
+ - 9z8y7x6w — Contest Mosaic Theory
76
+
77
+ Workspace: book-3.0-d2f1.json
78
+ - 1q2w3e4r — Ch 3 — Beats
79
+ - 5t6y7u8i — Ch 4 — Draft
80
+
81
+ Call get_workspace_structure once per workspace for vocab, then read_pad
82
+ + enrich each doc, then bulk mark_enriched at the end.
83
+ ```
84
+
85
+ Keep prompts short. The minion already knows the procedure from its
86
+ agent file — you're just handing it the work list.
87
+
88
+ ### 5. Surface to the user (large-batch phrasing)
89
+
90
+ Before dispatching, tell the user what's happening. Firm rule 5's
91
+ "large batch" tier (N > 20) requires a heads-up. Example:
92
+
93
+ > OpenWriter detected 73 docs that haven't been summarized yet —
94
+ > first-time setup. Refreshing them in 6 parallel batches in the
95
+ > background; this'll take ~30 seconds and a few cents of Haiku usage.
96
+
97
+ Then dispatch. Stay silent as notifications come in unless one fails.
98
+ When all are done, report once:
99
+
100
+ > Enrichment complete: 73 docs across 8 workspaces. Cost: ~$0.15.
101
+
102
+ ### 6. Verify completion
103
+
104
+ After every chunk has notified, call `list_dirty_docs` once more. If
105
+ `total > 0`, some docs slipped — usually because a minion errored on a
106
+ specific doc or a doc was modified mid-enrichment. Re-dispatch a single
107
+ minion for the stragglers; don't redo the whole batch.
108
+
109
+ ## Why this shape
110
+
111
+ **Why parallel, not serial single-minion at maxTurns: 500?**
112
+ A single minion processing 100 docs takes 3+ minutes wall time. The
113
+ user sits in silence. Six parallel minions of ~15 docs each finish in
114
+ ~30 seconds. Same total token cost — much better UX.
115
+
116
+ **Why explicit docId list instead of letting each minion call `list_dirty_docs`?**
117
+ Race conditions. If you spawn 6 minions and they all call
118
+ `list_dirty_docs`, they all see the same 100 dirty docs and try to
119
+ enrich the same docs in parallel. Most enrichments succeed (last write
120
+ wins on the frontmatter), but it's wasteful and the per-doc baselines
121
+ get computed multiple times. Explicit lists partition the work cleanly.
122
+
123
+ **Why 8–15 docs per chunk and not 50?**
124
+ Two reasons: (1) turn budget — each doc costs 1–2 turns (1 read_pad
125
+ call, occasional workspace structure fetch); ~15 docs leaves headroom
126
+ inside the 500-turn ceiling even with retries. (2) failure isolation —
127
+ if one minion's batch errors, you lose 15 docs of work, not 50.
128
+
129
+ **Why dispatch in one message, not sequential Agent calls?**
130
+ Sequential `Agent` calls block each other. Only multiple `Agent` tool
131
+ uses in the **same assistant message** run truly in parallel.
132
+
133
+ ## Cost ballpark
134
+
135
+ Haiku token cost per doc: ~3K–6K (read_pad + enrichment synthesis +
136
+ share of mark_enriched).
137
+
138
+ | Corpus size | Approx cost |
139
+ |---|---|
140
+ | 30 docs | ~$0.05 |
141
+ | 100 docs | ~$0.15 |
142
+ | 500 docs | ~$0.75 |
143
+
144
+ Compare to ~$5.00 per doc if you used the general-purpose subagent with
145
+ full MCP tool registry (~50K token overhead per spawn). The custom
146
+ minion's tool allowlist (4 tools) is what makes the math work.
147
+
148
+ ## Failure modes
149
+
150
+ - **Minion returns with no `mark_enriched` call** — almost always means
151
+ it hit the turn ceiling. Confirm its agent file has `maxTurns: 500`
152
+ in the frontmatter, then reduce chunk size to ~10 docs and re-dispatch
153
+ that chunk.
154
+ - **Minion reports "No enrichment work pending"** — its assigned docs
155
+ got enriched by a sibling minion first (race condition from
156
+ `list_dirty_docs` mode, not explicit-list mode). Benign; the other
157
+ minion's work landed correctly.
158
+ - **`<task-notification>` reports an error** — re-dispatch just that
159
+ one chunk. Don't restart the whole batch.
160
+ - **Logline cap violations** — the minion's agent file enforces a
161
+ 150-char hard cap. If you spot longer loglines on disk after the
162
+ fact, it's a minion regression — flag for agent-file revision rather
163
+ than re-enriching.
164
+
165
+ ## When NOT to chunk
166
+
167
+ If `list_dirty_docs` returns ≤30 docs, dispatch a single minion with
168
+ the default prompt:
169
+
170
+ ```
171
+ Agent({
172
+ subagent_type: "openwriter-enrichment-minion",
173
+ description: "Enrich stale openwriter docs",
174
+ prompt: "Enrich all currently stale openwriter docs."
175
+ })
176
+ ```
177
+
178
+ The minion calls `list_dirty_docs` itself, processes everything in one
179
+ pass, and reports back. Chunking ≤30 docs is overhead, not gain.
@@ -0,0 +1,178 @@
1
+ # Footnotes — Author Guide for Agents
2
+
3
+ OpenWriter supports CommonMark / Pandoc footnote syntax for citation-heavy
4
+ long-form writing. The editor renders inline references as superscript
5
+ chips and corrals definitions into an end-of-doc "Footnotes" section.
6
+
7
+ This doc explains how to write footnotes via MCP tools and what to expect
8
+ on disk and in the editor.
9
+
10
+ ## The syntax (Pandoc / CommonMark)
11
+
12
+ Two parts:
13
+
14
+ - **Reference** (inline): `text[^N]` — appears in the prose
15
+ - **Definition** (block): `[^N]: footnote text` — appears at end of doc
16
+
17
+ Labels can be numeric or mnemonic:
18
+
19
+ ```markdown
20
+ Humans run fixed action patterns[^1] too.
21
+
22
+ Per Sapolsky[^sapolsky2017], stress responses follow a pattern.
23
+
24
+ [^1]: Eibl-Eibesfeldt 1973, replicated and extended by Galati et al. 2003.
25
+
26
+ [^sapolsky2017]: Sapolsky, R. (2017). *Behave*. Penguin Press.
27
+ ```
28
+
29
+ The author label (`1` or `sapolsky2017`) is what pairs reference to
30
+ definition. **Display numbering is automatic** — the editor's CSS counter
31
+ shows sequential `[1] [2] [3]` regardless of label. Mnemonic labels stay
32
+ on disk for human-readable file diffs.
33
+
34
+ ## How to write footnotes from MCP
35
+
36
+ Just include the syntax in your markdown content — no special tool needed.
37
+
38
+ ### populate_document (initial draft)
39
+
40
+ ```ts
41
+ populate_document({
42
+ docId: "abc12345",
43
+ content: `# Chapter 1
44
+
45
+ Theory of mind develops late[^1] in non-human primates.
46
+
47
+ [^1]: Premack & Woodruff (1978), Behavioral and Brain Sciences 1: 515–526.
48
+ `
49
+ })
50
+ ```
51
+
52
+ The parser handles `[^1]` references and `[^1]: ...` definitions. The
53
+ editor renders the reference as a superscript chip and the definition
54
+ inside an end-of-doc "Footnotes" section.
55
+
56
+ ### write_to_pad (adding to existing doc)
57
+
58
+ To add a new footnote to existing prose, you have two paths.
59
+
60
+ **Append a new reference + definition together** (recommended):
61
+
62
+ ```ts
63
+ // Step 1: rewrite the paragraph to add the reference
64
+ write_to_pad({
65
+ docId: "abc12345",
66
+ changes: [
67
+ {
68
+ operation: "rewrite",
69
+ nodeId: "para_id",
70
+ content: "The same sentence now with a new claim[^2]."
71
+ }
72
+ ]
73
+ })
74
+
75
+ // Step 2: append the definition. If the doc already has a footnoteSection,
76
+ // you can insert the definition inside it via afterNodeId pointing at the
77
+ // last definition. If the doc has no footnotes yet, the parser auto-creates
78
+ // the section when it sees `[^N]: ...` at the end of the markdown body.
79
+ ```
80
+
81
+ **Simpler: just include both the reference and the definition in one
82
+ write_to_pad call**:
83
+
84
+ ```ts
85
+ write_to_pad({
86
+ docId: "abc12345",
87
+ changes: [
88
+ {
89
+ operation: "rewrite",
90
+ nodeId: "para_id",
91
+ content: "Sentence with new claim[^2]."
92
+ },
93
+ {
94
+ operation: "insert",
95
+ afterNodeId: "end",
96
+ content: "[^2]: Smith et al. (2020), Nature 580: 142–148."
97
+ }
98
+ ]
99
+ })
100
+ ```
101
+
102
+ The serializer normalizes definitions to the end-of-doc `footnoteSection`
103
+ regardless of where they're inserted in the tree.
104
+
105
+ ## What you see in `read_pad`
106
+
107
+ ```
108
+ title: My Chapter
109
+ id: abc12345
110
+ words: 423
111
+ pending: 0
112
+ ---
113
+ [h1:aa0001] Chapter 1
114
+ [p:bb0002] Theory of mind develops late[^1] in non-human primates.
115
+ [fnsec:cc0003]
116
+ [fndef:dd0004] [^1]: Premack & Woodruff (1978), Behavioral and Brain Sciences 1: 515–526.
117
+ ```
118
+
119
+ The `[^N]` in the body is the inline reference. `[fnsec:...]` is the
120
+ end-of-doc section. `[fndef:...]` is each definition.
121
+
122
+ ## Per-doc scope — important
123
+
124
+ **Footnote labels are local to each doc.** Chapter 3's `[^1]` does not
125
+ refer to Chapter 4's `[^1]`. Each chapter is its own `.md` file with its
126
+ own numbering. Cross-chapter references are not supported at the editor
127
+ level (a future book-export pipeline will handle global numbering at
128
+ typeset time).
129
+
130
+ If the author writes "see Ch 1 note 4" they're writing prose, not a
131
+ cross-doc footnote link.
132
+
133
+ ## Multi-paragraph definitions
134
+
135
+ Pandoc allows multi-paragraph footnotes via 4-space-indented continuation:
136
+
137
+ ```markdown
138
+ [^1]: First paragraph of the definition.
139
+
140
+ Continuation paragraph, indented 4 spaces.
141
+
142
+ Another continuation.
143
+ ```
144
+
145
+ The editor preserves the multi-paragraph structure inside the definition.
146
+ Use this for footnotes that need substantial explanation (lengthy
147
+ methodology notes, multi-source citations, etc.).
148
+
149
+ ## What's NOT supported (yet)
150
+
151
+ - **Cross-doc footnote references.** Each doc has its own numbering.
152
+ - **Bibliography auto-generation.** Authors manage citation text inline.
153
+ Zotero / Mendeley / BibTeX integration is a future enhancement.
154
+ - **DOI auto-resolution.** Footnote text is plain — paste a DOI manually.
155
+ - **Per-page footnotes (Phase 3).** The editor uses end-of-doc placement;
156
+ per-page placement is a print-layout concern handled at book-export
157
+ time, not in the editor.
158
+
159
+ ## When to use footnotes vs inline parentheticals
160
+
161
+ Use footnotes when:
162
+ - The citation count exceeds ~3 per ~500 words (inline parentheticals
163
+ start visibly disrupting the prose at that density)
164
+ - The audience expects an academic register (popular nonfiction in the
165
+ Sapolsky / Wrangham / Pinker lineage)
166
+ - The work targets book-class output (cumulative citation load at book
167
+ scale destroys readability under inline parentheticals)
168
+
169
+ Use inline parentheticals when:
170
+ - Citations are sparse (<1 per 500 words) and short
171
+ - The author prefers a journalistic register
172
+ - The work is short-form (tweet thread, blog post) where there's no
173
+ end-of-doc section to defer to
174
+
175
+ ## Reference docs (for the editor maintainers, not agents)
176
+
177
+ - `docs/footnotes.md` (in the openwriter repo): full architecture
178
+ - `adr/footnote-system.md`: load-bearing invariants + decision log