backdraft 0.4.0__tar.gz → 0.5.0__tar.gz

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 (150) hide show
  1. backdraft-0.5.0/.claude-plugin/marketplace.json +16 -0
  2. backdraft-0.5.0/.claude-plugin/plugin.json +14 -0
  3. {backdraft-0.4.0 → backdraft-0.5.0}/DESIGN.md +1 -0
  4. {backdraft-0.4.0 → backdraft-0.5.0}/PKG-INFO +52 -12
  5. {backdraft-0.4.0 → backdraft-0.5.0}/README.md +51 -11
  6. backdraft-0.5.0/ROADMAP.md +177 -0
  7. {backdraft-0.4.0 → backdraft-0.5.0}/SPEC.md +5 -3
  8. {backdraft-0.4.0 → backdraft-0.5.0}/demo/walkthrough.md +20 -21
  9. {backdraft-0.4.0 → backdraft-0.5.0}/pyproject.toml +1 -1
  10. {backdraft-0.4.0 → backdraft-0.5.0}/site/docs.html +53 -13
  11. {backdraft-0.4.0 → backdraft-0.5.0}/site/llms.txt +16 -2
  12. backdraft-0.5.0/site/privacy.html +126 -0
  13. {backdraft-0.4.0 → backdraft-0.5.0}/skills/backdraft/SKILL.md +19 -6
  14. {backdraft-0.4.0 → backdraft-0.5.0}/skills/backdraft-artifact/SKILL.md +1 -1
  15. {backdraft-0.4.0 → backdraft-0.5.0}/skills/backdraft-backfill/SKILL.md +7 -1
  16. backdraft-0.5.0/src/backdraft/__main__.py +6 -0
  17. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/cli.py +25 -3
  18. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/cli.py +111 -81
  19. backdraft-0.5.0/src/backdraft/extract/snapshots.py +170 -0
  20. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/vlm.py +20 -18
  21. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/gate/reader.py +23 -3
  22. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/gate/searcher.py +2 -2
  23. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/test_gate_cli.py +13 -3
  24. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/test_gate_reader.py +17 -0
  25. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_bind_cli.py +26 -0
  26. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_cli.py +113 -0
  27. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_extract.py +20 -24
  28. backdraft-0.5.0/tests/test_snapshots.py +509 -0
  29. {backdraft-0.4.0 → backdraft-0.5.0}/uv.lock +1 -1
  30. backdraft-0.4.0/ROADMAP.md +0 -59
  31. {backdraft-0.4.0 → backdraft-0.5.0}/.github/workflows/ci.yml +0 -0
  32. {backdraft-0.4.0 → backdraft-0.5.0}/.github/workflows/release.yml +0 -0
  33. {backdraft-0.4.0 → backdraft-0.5.0}/.github/workflows/site.yml +0 -0
  34. {backdraft-0.4.0 → backdraft-0.5.0}/.gitignore +0 -0
  35. {backdraft-0.4.0 → backdraft-0.5.0}/.python-version +0 -0
  36. {backdraft-0.4.0 → backdraft-0.5.0}/LICENSE +0 -0
  37. {backdraft-0.4.0 → backdraft-0.5.0}/assets/backdraft-mark.svg +0 -0
  38. {backdraft-0.4.0 → backdraft-0.5.0}/demo/generate_sources.py +0 -0
  39. {backdraft-0.4.0 → backdraft-0.5.0}/demo/memo.backdraft.html +0 -0
  40. {backdraft-0.4.0 → backdraft-0.5.0}/demo/memo.md +0 -0
  41. {backdraft-0.4.0 → backdraft-0.5.0}/demo/sources/t12-summary.pdf +0 -0
  42. {backdraft-0.4.0 → backdraft-0.5.0}/demo/sources/underwriting-model.xlsx +0 -0
  43. {backdraft-0.4.0 → backdraft-0.5.0}/site/demo.html +0 -0
  44. {backdraft-0.4.0 → backdraft-0.5.0}/site/index.html +0 -0
  45. {backdraft-0.4.0 → backdraft-0.5.0}/spec/artifact.md +0 -0
  46. {backdraft-0.4.0 → backdraft-0.5.0}/spec/chunking.md +0 -0
  47. {backdraft-0.4.0 → backdraft-0.5.0}/spec/tokens.md +0 -0
  48. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/__init__.py +0 -0
  49. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/__init__.py +0 -0
  50. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/binder.py +0 -0
  51. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/evidence.py +0 -0
  52. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/__init__.py +0 -0
  53. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/base.py +0 -0
  54. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/entail.py +0 -0
  55. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/overlap.py +0 -0
  56. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/recompute.py +0 -0
  57. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/bind/verify/value_trace.py +0 -0
  58. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/cli_context.py +0 -0
  59. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/credentials.py +0 -0
  60. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/__init__.py +0 -0
  61. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/base.py +0 -0
  62. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/csv.py +0 -0
  63. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/docx.py +0 -0
  64. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/image.py +0 -0
  65. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/pdf_text.py +0 -0
  66. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/pptx.py +0 -0
  67. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/sheet.py +0 -0
  68. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/text.py +0 -0
  69. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/vlm_settings.py +0 -0
  70. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/xls.py +0 -0
  71. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/extract/xlsx.py +0 -0
  72. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/gate/__init__.py +0 -0
  73. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/gate/cli.py +0 -0
  74. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/__init__.py +0 -0
  75. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/artifact.py +0 -0
  76. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/chunking.py +0 -0
  77. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/claims.py +0 -0
  78. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/errors.py +0 -0
  79. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/hashing.py +0 -0
  80. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/model.py +0 -0
  81. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/kernel/tokens.py +0 -0
  82. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/registry/__init__.py +0 -0
  83. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/registry/ledger.py +0 -0
  84. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/registry/schema.sql +0 -0
  85. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/registry/store.py +0 -0
  86. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/__init__.py +0 -0
  87. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/_text.py +0 -0
  88. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/cli.py +0 -0
  89. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/footnotes.py +0 -0
  90. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/__init__.py +0 -0
  91. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/assets.py +0 -0
  92. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/components.py +0 -0
  93. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/fmt.py +0 -0
  94. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
  95. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/page.py +0 -0
  96. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/html/text.py +0 -0
  97. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/markdown.py +0 -0
  98. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/placement.py +0 -0
  99. {backdraft-0.4.0 → backdraft-0.5.0}/src/backdraft/render/sidecar.py +0 -0
  100. {backdraft-0.4.0 → backdraft-0.5.0}/tests/conftest.py +0 -0
  101. {backdraft-0.4.0 → backdraft-0.5.0}/tests/conftest_registry.py +0 -0
  102. {backdraft-0.4.0 → backdraft-0.5.0}/tests/conftest_render.py +0 -0
  103. {backdraft-0.4.0 → backdraft-0.5.0}/tests/fakes.py +0 -0
  104. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/conftest.py +0 -0
  105. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/fake_registry.py +0 -0
  106. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/test_gate_ledger.py +0 -0
  107. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/test_gate_searcher.py +0 -0
  108. {backdraft-0.4.0 → backdraft-0.5.0}/tests/gate/test_reader_cells.py +0 -0
  109. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/bind/backfill.bound.md +0 -0
  110. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/bind/frontwalk.bound.md +0 -0
  111. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/long_tail.json +0 -0
  112. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/long_tail.txt +0 -0
  113. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/many_small.json +0 -0
  114. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/many_small.txt +0 -0
  115. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/mixed.json +0 -0
  116. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/mixed.txt +0 -0
  117. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/one_giant.json +0 -0
  118. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/one_giant.txt +0 -0
  119. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/short_page.json +0 -0
  120. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/short_page.txt +0 -0
  121. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/unsplittable.json +0 -0
  122. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/chunking/unsplittable.txt +0 -0
  123. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/render/backfill.footnotes.md +0 -0
  124. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/render/backfill.sidecar.json +0 -0
  125. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/render/demo.footnotes.md +0 -0
  126. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden/render/demo.sidecar.json +0 -0
  127. {backdraft-0.4.0 → backdraft-0.5.0}/tests/golden_util.py +0 -0
  128. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_bind_binder.py +0 -0
  129. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_bind_evidence.py +0 -0
  130. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_bind_verify.py +0 -0
  131. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_chunking.py +0 -0
  132. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_claims.py +0 -0
  133. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_credentials.py +0 -0
  134. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_fmt_parity.py +0 -0
  135. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_footnotes.py +0 -0
  136. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_gate_integration.py +0 -0
  137. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_hashing.py +0 -0
  138. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_html.py +0 -0
  139. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_invariants.py +0 -0
  140. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_model.py +0 -0
  141. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_placement.py +0 -0
  142. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_records_location.py +0 -0
  143. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_registry.py +0 -0
  144. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_reingest.py +0 -0
  145. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_render_cli.py +0 -0
  146. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_render_markdown.py +0 -0
  147. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_sidecar.py +0 -0
  148. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_snapshot_settings.py +0 -0
  149. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_tokens.py +0 -0
  150. {backdraft-0.4.0 → backdraft-0.5.0}/tests/test_value_trace.py +0 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "backdraft",
3
+ "owner": {
4
+ "name": "Spencer Braun",
5
+ "url": "https://github.com/spencerbraun"
6
+ },
7
+ "description": "Self-hosting marketplace for the backdraft plugin: agent skills for writing documents whose factual claims carry verifiable citations.",
8
+ "plugins": [
9
+ {
10
+ "name": "backdraft",
11
+ "source": "./",
12
+ "description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
13
+ "category": "productivity"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "backdraft",
3
+ "displayName": "Backdraft",
4
+ "version": "0.5.0",
5
+ "description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
6
+ "author": {
7
+ "name": "Spencer Braun",
8
+ "url": "https://github.com/spencerbraun"
9
+ },
10
+ "homepage": "https://backdraft.dev",
11
+ "repository": "https://github.com/spencerbraun/backdraft",
12
+ "license": "MIT",
13
+ "keywords": ["citations", "provenance", "sources", "evidence", "documents", "pdf", "xlsx", "writing"]
14
+ }
@@ -134,6 +134,7 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
134
134
  | 2026-07-29 | **Office formats extract natively, never through conversion.** DOCX and PPTX are read straight from their XML (python-docx / python-pptx, core deps). Converting to PDF was rejected on principle: conversion is rendering, so it needs a layout engine (LibreOffice, MS Office, or a cloud API — a 700MB dependency, a platform lock, or documents leaving the machine), and converter-dependent pagination would break content-addressed locator stability. DOCX pages are heading sections: the split level is the smallest outline level present among {Heading 1, Heading 2}; preamble is section 1; no headings → one section — locators stay `pN.cM`, no grammar change. PPTX pages are slides (slide N is slide N in every renderer). The text floor deliberately misses slide visuals; the ingest note says so and names the path that captures them — the user exporting the deck to PDF with the real renderer they already have, ingested through the vision extractor — so a calling agent can relay it. |
135
135
  | 2026-07-29 | **The vision extractor ships by default (supersedes "[vlm] as extra").** `openai` + `pdf2image` moved into core dependencies; `[vlm]` remains as an empty compat alias. Rationale: the VLM path is the recommended path for real PDFs, and an extra made the recommended path opt-in. The consent boundary does not move — installing deps was never consent; the backdraft-scoped `BACKDRAFT_VLM_API_KEY` is, and ambient provider keys stay unread. There is no slim install: pip extras can only add, never subtract; a separate core distribution is possible later if anyone asks. poppler stays a runtime capability check. |
136
136
  | 2026-07-29 | **Renderer decomposed; Python is the formatting authority.** `render/html.py` (1,300 lines) became the `render/html/` package (fmt, text, components, assets, page), byte-identical at the seam. The deliberate `fmt_cell` (Python) / `fmtCell` (JS) duplication is now pinned by one shared vector table driving both tests, with the JS run under node when available. The JS was found diverging on exact rounding ties (`toFixed` rounds half-up; Python rounds half-to-even on the double's value) and was rewritten to round exactly — Python defines correct, JS matches it. The artifact emits minified CSS/JS and unindented markup; the `$legend` and sidecar JSON stay readable in view-source (spec constraint). Snapshot budget knobs `BACKDRAFT_SNAPSHOT_QUALITY`/`BACKDRAFT_SNAPSHOT_MAX_HEIGHT` tune page-image weight at ingest — display only, tokens never derive from pixels. |
137
+ | 2026-07-30 | **Every PDF ingest captures page images, not just the VLM path's.** The text-layer route stored none, so the keyless path produced visually poorer artifacts for no principled reason — the pages render locally through poppler, no model calls, and the capability already existed as the `snapshot-pages` backfill. The capture is a step *after* extraction, owned by `cli.py` over `extract/snapshots.py`, deliberately **not** inside `pdf-text`: an extractor whose output varied with whether poppler happened to be installed would not be `deterministic = True`, and the same bytes must extract identically on every machine. Because snapshots only ever write `page_images` — a table no token, chunk, or receipt path reads — capture is safe to make best-effort: no poppler means ingest still exits 0, with a one-line note naming `snapshot-pages` as the backfill, rather than failing over a display concern. Tradeoff accepted: PDF ingest is now slower by a local render per page (one poppler call per page rather than one per document, to bound peak memory on long PDFs), and a library caller using `Registry.ingest` directly gets no snapshots — the policy lives at the CLI, which is the substrate that has a user to notify. |
137
138
 
138
139
  ## Open
139
140
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backdraft
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
5
5
  Project-URL: Homepage, https://backdraft.dev
6
6
  Project-URL: Documentation, https://backdraft.dev/docs.html
@@ -61,6 +61,7 @@ Markdown.
61
61
  $ backdraft init
62
62
  registry: /…/demo/.backdraft
63
63
  documents: 0
64
+ … plus where settings live and what to do next
64
65
 
65
66
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
66
67
  t12-summary t12-summary.pdf pdf 3 pages
@@ -130,7 +131,7 @@ $ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
130
131
  bound 15 claim(s), 16 citation(s) [frontwalk]
131
132
  resolved: 15
132
133
  unresolved: 1
133
- overlap: partial 4, pass 11
134
+ overlap: pass 11, skip 4
134
135
  value-trace: pass 15
135
136
  ! unresolved: bd:t12-summary:p4.c1:1a2b
136
137
  wrote .backdraft/records/memo.backdraft.json
@@ -209,8 +210,9 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
209
210
  key in the environment is not consent to send documents to its provider. The
210
211
  default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
211
212
  base_url + model, set explicitly. Without a scoped key, `auto` falls back to
212
- the keyless text layer and says so. PDF page rendering needs poppler,
213
- installed separately.
213
+ the keyless text layer and says so — still storing each page's image, which it
214
+ renders locally. PDF page rendering needs poppler, installed separately; without
215
+ it ingest succeeds without the images and names `snapshot-pages` as the backfill.
214
216
 
215
217
  Two extras:
216
218
 
@@ -232,13 +234,7 @@ uv run pytest
232
234
 
233
235
  ## Skills
234
236
 
235
- Three agent skills ship inside the package, and the CLI installs them:
236
-
237
- ```bash
238
- backdraft skill install # the writing skill, into ~/.claude/skills/
239
- backdraft skill install --all # plus backfill and artifact-reading
240
- backdraft skill install --project # into this repo's .claude/skills/
241
- ```
237
+ Three agent skills ship inside the package and in this repo:
242
238
 
243
239
  | Skill | For |
244
240
  |---|---|
@@ -247,7 +243,51 @@ backdraft skill install --project # into this repo's .claude/skills/
247
243
  | [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
248
244
 
249
245
  The CLI is the system; a skill is one page telling an agent to use it. Nothing in
250
- the skills is required to use backdraft by hand.
246
+ the skills is required to use backdraft by hand. How the skills reach your agent
247
+ depends on the harness:
248
+
249
+ **Claude Code.** The repo is its own plugin marketplace, so the plugin route
250
+ tracks releases:
251
+
252
+ ```text
253
+ /plugin marketplace add spencerbraun/backdraft
254
+ /plugin install backdraft@backdraft
255
+ ```
256
+
257
+ Or have the CLI copy the skills into your skills directory:
258
+
259
+ ```bash
260
+ backdraft skill install # the writing skill, into ~/.claude/skills/
261
+ backdraft skill install --all # plus backfill and artifact-reading
262
+ backdraft skill install --project # into this repo's .claude/skills/
263
+ ```
264
+
265
+ **Claude Cowork.** Once the plugin is listed in Anthropic's community
266
+ directory, it installs from Cowork's built-in skills directory. Until then,
267
+ zip a skill folder from `skills/` and upload it under Customize > Skills.
268
+ Inside a Cowork session the CLI itself runs per-command as
269
+ `uvx backdraft ...`, since installs do not persist between sessions.
270
+
271
+ **Codex, Cursor, Copilot.** Agents in this family read skills from
272
+ `~/.agents/skills`:
273
+
274
+ ```bash
275
+ backdraft skill install --agent codex # into ~/.agents/skills/
276
+ ```
277
+
278
+ or commit the skill folders to your repo under `.agents/skills/` so every
279
+ checkout carries them.
280
+
281
+ **Standing context, any harness.** Paste this into your repo's AGENTS.md:
282
+
283
+ ```markdown
284
+ ## backdraft (cited writing)
285
+ When a document must cite its sources, write it through the backdraft CLI:
286
+ it shows source text with a citation token over every span, and only shown
287
+ spans are citable. In a sandboxed session run every command as
288
+ `uvx backdraft ...` (no install, no PATH edits). Start from
289
+ `uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.
290
+ ```
251
291
 
252
292
  ## Documentation
253
293
 
@@ -36,6 +36,7 @@ Markdown.
36
36
  $ backdraft init
37
37
  registry: /…/demo/.backdraft
38
38
  documents: 0
39
+ … plus where settings live and what to do next
39
40
 
40
41
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
41
42
  t12-summary t12-summary.pdf pdf 3 pages
@@ -105,7 +106,7 @@ $ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
105
106
  bound 15 claim(s), 16 citation(s) [frontwalk]
106
107
  resolved: 15
107
108
  unresolved: 1
108
- overlap: partial 4, pass 11
109
+ overlap: pass 11, skip 4
109
110
  value-trace: pass 15
110
111
  ! unresolved: bd:t12-summary:p4.c1:1a2b
111
112
  wrote .backdraft/records/memo.backdraft.json
@@ -184,8 +185,9 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
184
185
  key in the environment is not consent to send documents to its provider. The
185
186
  default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
186
187
  base_url + model, set explicitly. Without a scoped key, `auto` falls back to
187
- the keyless text layer and says so. PDF page rendering needs poppler,
188
- installed separately.
188
+ the keyless text layer and says so — still storing each page's image, which it
189
+ renders locally. PDF page rendering needs poppler, installed separately; without
190
+ it ingest succeeds without the images and names `snapshot-pages` as the backfill.
189
191
 
190
192
  Two extras:
191
193
 
@@ -207,13 +209,7 @@ uv run pytest
207
209
 
208
210
  ## Skills
209
211
 
210
- Three agent skills ship inside the package, and the CLI installs them:
211
-
212
- ```bash
213
- backdraft skill install # the writing skill, into ~/.claude/skills/
214
- backdraft skill install --all # plus backfill and artifact-reading
215
- backdraft skill install --project # into this repo's .claude/skills/
216
- ```
212
+ Three agent skills ship inside the package and in this repo:
217
213
 
218
214
  | Skill | For |
219
215
  |---|---|
@@ -222,7 +218,51 @@ backdraft skill install --project # into this repo's .claude/skills/
222
218
  | [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
223
219
 
224
220
  The CLI is the system; a skill is one page telling an agent to use it. Nothing in
225
- the skills is required to use backdraft by hand.
221
+ the skills is required to use backdraft by hand. How the skills reach your agent
222
+ depends on the harness:
223
+
224
+ **Claude Code.** The repo is its own plugin marketplace, so the plugin route
225
+ tracks releases:
226
+
227
+ ```text
228
+ /plugin marketplace add spencerbraun/backdraft
229
+ /plugin install backdraft@backdraft
230
+ ```
231
+
232
+ Or have the CLI copy the skills into your skills directory:
233
+
234
+ ```bash
235
+ backdraft skill install # the writing skill, into ~/.claude/skills/
236
+ backdraft skill install --all # plus backfill and artifact-reading
237
+ backdraft skill install --project # into this repo's .claude/skills/
238
+ ```
239
+
240
+ **Claude Cowork.** Once the plugin is listed in Anthropic's community
241
+ directory, it installs from Cowork's built-in skills directory. Until then,
242
+ zip a skill folder from `skills/` and upload it under Customize > Skills.
243
+ Inside a Cowork session the CLI itself runs per-command as
244
+ `uvx backdraft ...`, since installs do not persist between sessions.
245
+
246
+ **Codex, Cursor, Copilot.** Agents in this family read skills from
247
+ `~/.agents/skills`:
248
+
249
+ ```bash
250
+ backdraft skill install --agent codex # into ~/.agents/skills/
251
+ ```
252
+
253
+ or commit the skill folders to your repo under `.agents/skills/` so every
254
+ checkout carries them.
255
+
256
+ **Standing context, any harness.** Paste this into your repo's AGENTS.md:
257
+
258
+ ```markdown
259
+ ## backdraft (cited writing)
260
+ When a document must cite its sources, write it through the backdraft CLI:
261
+ it shows source text with a citation token over every span, and only shown
262
+ spans are citable. In a sandboxed session run every command as
263
+ `uvx backdraft ...` (no install, no PATH edits). Start from
264
+ `uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.
265
+ ```
226
266
 
227
267
  ## Documentation
228
268
 
@@ -0,0 +1,177 @@
1
+ # Roadmap
2
+
3
+ Sequenced intent. DESIGN.md records what was *decided*; this file records what
4
+ is *queued*, so tabling something is an act of writing it down here rather than
5
+ losing it. Items graduate by getting built and deleted.
6
+
7
+ ## How the queue runs
8
+
9
+ The Now section is an ordered queue written for an implementing agent with no
10
+ session context: each item carries its intent, its shape, and its acceptance
11
+ test. The working agreement is one item at a time, top first, shipped complete
12
+ — code, tests, docs, and a DESIGN.md decision row where the item calls for one
13
+ — then deleted from this file in the same commit. Releases cut Friday, so an
14
+ item that lands mid-week waits on PyPI until the release; nothing ships
15
+ half-done to make a release. An item that turns out bigger than its sizing
16
+ gets split here, not stretched silently.
17
+
18
+ The cadence is daily and the runner is an agent: one item each morning Monday
19
+ through Thursday, landing as a direct commit to `main` — this repo does not
20
+ use pull requests, so a week of commits is the only review surface there is.
21
+
22
+ Friday builds nothing new. It reads that week's diff instead — duplication,
23
+ drift from the repo's own idioms, branches that landed untested, help text and
24
+ error messages that confuse, commands the docs describe wrongly — and lands
25
+ the fixes. Then the release, and only if something landed since the last tag:
26
+ the version moves in both `pyproject.toml` and `.claude-plugin/plugin.json`,
27
+ and publishing the GitHub release is what ships to PyPI. Then this section is
28
+ refilled with five items for the week ahead, drawn against the core — the four
29
+ verbs, the registry, the grammar, the artifact — and against where an agent
30
+ using backdraft actually stumbles. What the maintenance pass just found is the
31
+ best evidence available for what those five should be.
32
+
33
+ ## Now
34
+
35
+ ### 1. Source card: contents resize with the card
36
+
37
+ **Intent.** The artifact's source card is user-resizable (`resize:vertical`,
38
+ `max-height:82vh` — assets.py) but its inner blocks hold their own fixed caps
39
+ — the quote scroller is pinned at `15rem`, and the sheet grid and page image
40
+ size themselves — so dragging the card taller yields whitespace, not more
41
+ evidence. Resizing should mean seeing more.
42
+
43
+ **Shape.** Make the card a flex column whose scrollable evidence region
44
+ (quote / drift / sheet / page image) flexes to fill the card's height, with the
45
+ header, source selector, and tabs as fixed rows. Audit every fixed
46
+ `max-height` inside `.card` and convert to flex-driven sizing. The mobile
47
+ card keeps `resize:none` and its current behavior. Python is the formatting
48
+ authority and this is CSS/JS only, so no fmt-parity surface is touched; the
49
+ minified asset budget should move only trivially.
50
+
51
+ **Acceptance.** Dragging the card taller visibly grows the quote, sheet, and
52
+ page-image viewports; dragging it short reintroduces inner scrolling rather
53
+ than clipping the header or tabs. Verified against a real artifact (the demo)
54
+ across the three evidence kinds, plus the mobile breakpoint. Node-side DOM
55
+ test if cheap; otherwise the existing render tests plus a manual screenshot
56
+ pass recorded in the PR.
57
+
58
+ **Size.** One day.
59
+
60
+ ### 2. Theming: sticky user preferences, drop-in themes
61
+
62
+ **Intent.** The artifact's look is currently one hardcoded design. Let a user
63
+ set a theme once and have every artifact they render honor it, and let a theme
64
+ be a small file someone can drop in — fonts, colors, heading treatment at
65
+ minimum.
66
+
67
+ **Shape.** Two parts, in order:
68
+
69
+ - *Decision row first.* A theme is a named set of overrides for the CSS custom
70
+ properties the stylesheet already exposes (`--paper`, `--ink`, `--sel`, the
71
+ font stacks) plus a bounded set of typographic choices (heading family/case,
72
+ maybe rule weight). A theme may not change layout, structure, or any
73
+ verification affordance — display only, tokens and records untouched.
74
+ Settle the file format (a flat TOML of variable names, or a raw CSS-variables
75
+ block) and write the DESIGN.md row before code.
76
+ - *Then the loader.* Precedence: `render --theme <file-or-name>` >
77
+ project `.backdraft/theme.*` > user-wide config (XDG,
78
+ `~/.config/backdraft/theme.*`) > built-in default. The user-wide file is
79
+ what makes preferences stick across projects. Bundled themes (the default
80
+ plus one or two alternates) prove the format; the resolved theme is baked
81
+ into the artifact at render time so the file stays self-contained.
82
+
83
+ **Acceptance.** Rendering with no config is byte-stable against today's
84
+ output. A theme file in the XDG location changes fonts/colors/headers on every
85
+ subsequent render in any project without flags; `--theme` overrides it; a
86
+ malformed theme fails with a clear message, never a half-styled artifact.
87
+ Docs page gains a short theming section with a sample theme file.
88
+
89
+ **Size.** Two days — decision row and variable audit, then loader, bundled
90
+ themes, docs.
91
+
92
+ ### 3. URL sources: capture and link back
93
+
94
+ **Intent.** Diligence folders contain links, not just files. `backdraft ingest
95
+ <url>` should fetch a page, snapshot it into the registry like any other
96
+ source, and — the other half — artifacts should show a source's origin URL and
97
+ link back to it, so a claim cited to a web page carries both the frozen
98
+ receipt and the live pointer.
99
+
100
+ **Shape.** This is the graduation of the "HTML pages" question from Later, and
101
+ it needs its decision row: *the source's identity is the sha256 of the fetched
102
+ snapshot at ingest time*, same as a file — the URL is provenance metadata, not
103
+ identity. A page that changes later is a new generation; the existing drift
104
+ machinery already describes that. Capture: fetch (stdlib or the lightest
105
+ dependency that handles redirects and encodings), extract readable text into
106
+ the normal page/chunk structure, store the origin URL and fetch timestamp in
107
+ source meta. Render: sources carrying an origin URL show it in the resting
108
+ source list and the card, as a real link. Out of scope for this item:
109
+ JavaScript-rendered pages (note the limitation), authentication, and `.eml`
110
+ (stays in Later).
111
+
112
+ **Acceptance.** `ingest https://…` on a static page mints citable tokens;
113
+ `read`/`search`/`cell`-equivalents behave as for text sources; bind embeds the
114
+ receipt; the artifact's source entry links to the URL and the record JSON
115
+ carries url + fetched-at. Re-ingesting a changed page produces a new
116
+ generation and `drifted` behaves correctly. A network-free test suite (fixture
117
+ HTML served locally or loaded from disk); DESIGN.md row written; docs updated.
118
+
119
+ **Size.** Two to three days — decision row and capture first, render linkage
120
+ second.
121
+
122
+ ## Parked
123
+
124
+ Deliberately not queued, each with the reason, so picking one up starts from
125
+ the objection rather than rediscovering it.
126
+
127
+ - **Exhibits view** — the evidence-first inversion. Risk: it grows the
128
+ artifact and pollutes a design whose strength is restraint. Needs a
129
+ deliberate design pass and a real audience before any code; not a
130
+ weekend-feature shape.
131
+ - **Excel region maps and range reads** — the research notes exist, but no
132
+ confidence the deterministic detection cascade lands well on the first
133
+ try. Revisit when large-sheet navigation hurts in a real corpus, with
134
+ test workbooks in hand.
135
+ - **`bd:calc` derivations** — reserved in the grammar and parsed as
136
+ malformed-on-purpose, which is enough for now. Full support complicates
137
+ binding and verification for one field-trial case.
138
+ - **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
139
+ `frozen` in sheet meta and keeps them (cheap, and capture is the part you
140
+ cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
141
+ waits for a real sheet that renders wrong without it.
142
+
143
+ ## Later
144
+
145
+ - **Living documents** — re-ingest, re-bind, and a drift-first report: "these
146
+ claims cite figures that changed; cited-then vs. now." The primitives
147
+ (generations, `drifted`, the word-diff) all exist; the missing piece is a
148
+ diff-shaped report and a demo.
149
+ - **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
150
+ and `--to json` contracts are already shaped for it), and a client-side
151
+ drag-drop viewer page (no upload — the file never leaves the reader's
152
+ machine).
153
+ - **Email (.eml)** — arrives in diligence folders alongside everything else;
154
+ identity questions (headers vs. body, attachments as child sources) deserve
155
+ a decision row before code. The web-page half of this item graduated to the
156
+ Now queue as URL sources.
157
+ - **Anthropic-API provider for the VLM extractor** — Cowork's sandbox
158
+ egress allowlist includes api.anthropic.com but not the OpenAI-compatible
159
+ providers, so an Anthropic-native option would let sandboxed sessions
160
+ ingest at full fidelity. Needs a client abstraction the vlm module
161
+ currently doesn't have; the documented workaround (ingest locally, the
162
+ registry travels with the folder) covers it meanwhile.
163
+ - **Distribution** — published on PyPI; the repo ships
164
+ `.claude-plugin/plugin.json` and `marketplace.json`, so it installs as a
165
+ Claude Code plugin and self-hosts its marketplace, and the AGENTS.md
166
+ snippet is published in the README and site docs. The community-directory
167
+ submission went in 2026-07-29 and is awaiting review — approval is what
168
+ surfaces the skills in Cowork's directory.
169
+
170
+ ## Someday
171
+
172
+ - **Hosted team features** — upload, access control, shared registries.
173
+ Deliberately parked: the product's trust story is the self-contained file,
174
+ and a hosted viewer inverts it. Revisit only as a distinct teams product.
175
+ - **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
176
+ but has never been field-calibrated; needs a corpus and a rubric before it
177
+ is recommended anywhere.
@@ -352,8 +352,10 @@ class Registry:
352
352
  # page snapshots (v0.2: evidence for the artifact)
353
353
  def page_image(self, slug: str, number: int) -> PageImage | None: ...
354
354
  # The stored visual snapshot of a current-extraction page: what the VLM
355
- # extractor was shown (WebP q85), or a `snapshot-pages` backfill. None
356
- # for text-only extractions.
355
+ # extractor was shown (WebP q85), the local poppler render the CLI
356
+ # captures after a text-layer PDF ingest, or a `snapshot-pages`
357
+ # backfill. None for non-PDFs, and for PDFs ingested where poppler was
358
+ # unavailable.
357
359
  def save_page_image(self, extraction_id: int, number: int, *, data: bytes,
358
360
  format: str, width: int, height: int) -> None: ...
359
361
  def current_extraction_id(self, slug: str) -> int | None: ... # for backfill tools
@@ -370,4 +372,4 @@ class Registry:
370
372
 
371
373
  ## Addendum B — CLI assembly
372
374
 
373
- Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` commands. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
375
+ Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` / `snapshot-pages` commands. After each ingest, `cli.py` captures page snapshots for any PDF whose extraction carries none — the text-layer path, since `vlm` stores its own — via `extract/snapshots.py` (poppler through pdf2image, `snapshot-pages`' internals). It sits outside the extractor so `pdf-text` stays deterministic regardless of whether poppler is installed, and it is best-effort: a `SnapshotError` leaves ingest at exit 0 with a one-line note naming `snapshot-pages` as the backfill. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
@@ -43,17 +43,15 @@ underwriting-model underwriting-model.xlsx xlsx 2 pages
43
43
  note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
44
44
  ```
45
45
 
46
- The keyless text layer is fine for these clean generated sources. For the
47
- artifact to show the cited pages themselves, store their snapshots — local
48
- rendering, no model calls:
49
-
50
- ```bash
51
- backdraft snapshot-pages t12-summary
52
- ```
46
+ The keyless text layer is fine for these clean generated sources.
53
47
 
54
48
  Ingest picked an extractor per file (`pdf-text` for the PDF, `xlsx` for the
55
49
  workbook), snapshotted the text, and minted every anchor: chunks for the PDF
56
- pages, cells for the sheets. Nothing is citable that was not ingested.
50
+ pages, cells for the sheets. Nothing is citable that was not ingested. It also
51
+ rendered each PDF page to an image and stored it, so the artifact at the end
52
+ can show the cited pages themselves — local rendering through poppler, no model
53
+ calls. On a machine without poppler ingest prints a note instead and
54
+ `backdraft snapshot-pages t12-summary` backfills them whenever you like.
57
55
 
58
56
  The note is real: this walkthrough deliberately uses the keyless text-layer
59
57
  path so it reproduces anywhere, and our source PDF is born-digital, where that
@@ -362,12 +360,15 @@ claim occurs at the cited location, across unit, scale and format differences. I
362
360
  is what makes `[a purchase price of $24.85 million](…assumptions!B10:964a)` pass
363
361
  against a cell whose snippet is the eight characters `24850000`.
364
362
 
365
- **`overlap: partial 4, pass 11`.** Word-overlap between claim and snippet,
363
+ **`overlap: pass 11, skip 4`.** Word-overlap between claim and snippet,
366
364
  report-only by construction — it never returns `fail`, because a faithful summary
367
- can share almost no vocabulary with its source. The four `partial`s are the
368
- claims citing Excel cells, where the snippet is a bare number and the claim is a
369
- sentence. That is a signal, not a fault. The quoted claim in the memo is checked
370
- differently: a quotation is an exact-substring assertion, and it passes.
365
+ can share almost no vocabulary with its source. The four `skip`s are the overlap
366
+ check declining to measure wording against single-cell sources: those claims cite
367
+ Excel cells, and a bare number is not a sentence, so the record says it declined
368
+ instead of emitting a low score as though it meant something. Where it does
369
+ measure and finds little it says `partial` — a signal, not a fault. The quoted
370
+ claim in the memo is checked differently: a quotation is an exact-substring
371
+ assertion, and it passes.
371
372
 
372
373
  Both checks are off by default. Out of the box, bind is pure provenance.
373
374
 
@@ -379,10 +380,6 @@ directory; the memo and the artifact are the only files you see. (A readable
379
380
  markdown projection with a `## References` section is available with
380
381
  `bind --bound` when you want one.)
381
382
 
382
- The four `skip` verdicts are the overlap check declining to measure wording
383
- against single-cell sources — a number is not a sentence, and the record says
384
- so instead of emitting noise.
385
-
386
383
  Here is what one claim looks like in the sidecar:
387
384
 
388
385
  ```json
@@ -409,8 +406,8 @@ Here is what one claim looks like in the sidecar:
409
406
  },
410
407
  {
411
408
  "method": "overlap",
412
- "status": "partial",
413
- "detail": "0/6 claim tokens in snippet (ratio 0.00)"
409
+ "status": "skip",
410
+ "detail": "wording overlap does not apply to a single cell"
414
411
  }
415
412
  ]
416
413
  }
@@ -452,8 +449,10 @@ backdraft render memo.md --to html
452
449
  memo.backdraft.html
453
450
  ```
454
451
 
455
- [`memo.backdraft.html`](memo.backdraft.html) is one file, 64 KB, with no external
456
- request of any kind — no font, no stylesheet, no script that executes. Open it
452
+ [`memo.backdraft.html`](memo.backdraft.html) is one file, 522 KB — most of it the
453
+ embedded page images — with no external request of any kind: no font, no
454
+ stylesheet, no script that executes. `bind --lean` skips the images and takes it
455
+ to 126 KB, at the cost of the cited page being shown. Open it
457
456
  from `file://` with the network off. Hover or focus a claim to reveal its
458
457
  receipt: the verbatim snippet, the source slug and locator, the token, the full
459
458
  snippet hash, and a badge per verdict. Below the document body an **Unresolved**
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "backdraft"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Drop-in provenance for factual claims: one click from any claim to the evidence behind it."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -109,7 +109,7 @@ footer{text-align:center;font-family:var(--sans);font-size:.74rem;color:var(--mu
109
109
  <a href="#writing">Writing rules</a>
110
110
  <a href="#verification">Verification</a>
111
111
  <a href="#files">Files &amp; the artifact</a>
112
- <a href="#agents">Agents</a>
112
+ <a href="#agents">Agents &amp; harnesses</a>
113
113
  <a href="#security">Security &amp; privacy</a>
114
114
  <a href="#faq">FAQ</a>
115
115
  </aside>
@@ -131,9 +131,11 @@ repo</a>, and agents should start from <a href="llms.txt">llms.txt</a>.</p>
131
131
  <p>Python 3.13+. The vision-model extractor ships by default, the recommended
132
132
  path for real PDFs (glossy layouts, info boxes, scans) and the only path for
133
133
  scanned images; it activates only when <code>BACKDRAFT_VLM_API_KEY</code> is
134
- set, and PDFs need <code>poppler</code> on your PATH
135
- (<code>brew install poppler</code>). Without a key, PDFs fall back to the
136
- keyless embedded-text layer, and ingest says so.
134
+ set. Without a key, PDFs fall back to the keyless embedded-text layer, and
135
+ ingest says so. Either way PDFs want <code>poppler</code> on your PATH
136
+ (<code>brew install poppler</code>) — it renders the pages, which is what puts
137
+ the cited page into the artifact; without it ingest still works and says what
138
+ is missing.
137
139
  Spreadsheets (xlsx, xlsm), CSV, Word (docx), PowerPoint (pptx), text and
138
140
  Markdown are keyless and built in, and sheet evidence carries the workbook's
139
141
  own styling: bold, fills, number formats, column widths. Legacy xls
@@ -209,7 +211,7 @@ skill</a> imposes the gate, and you receive the artifact.</div>
209
211
  <tr><th>Command</th><th>Does</th></tr>
210
212
  <tr><td><code>init</code></td><td>Create <code>.backdraft/</code> (registry, credentials template) in the current directory.</td></tr>
211
213
  <tr><td><code>ingest &lt;files&gt;</code></td><td>Snapshot sources into the registry, minting every anchor. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX, PPTX, images (png, jpeg, tiff), text and Markdown. <code>--extractor auto|vlm|image|pdf-text|xlsx|xls|csv|docx|pptx|text</code>, <code>--slug</code>, <code>--config k=v</code>.</td></tr>
212
- <tr><td><code>snapshot-pages &lt;slug&gt;</code></td><td>Backfill page images for an already-ingested PDF, locally, no model calls (needs <code>poppler</code> for rendering).</td></tr>
214
+ <tr><td><code>snapshot-pages &lt;slug&gt;</code></td><td>Backfill page images for an already-ingested PDF, locally, no model calls (needs <code>poppler</code> for rendering). Ingest stores them already — this is for a registry built before it did, or on a machine that had no poppler then.</td></tr>
213
215
  <tr><td><code>ls</code></td><td>List ingested documents.</td></tr>
214
216
  <tr><td><code>read [slug] [selector]</code></td><td>The gate: document list, table of contents, or a token-marked page/range/sheet read. Mints what it shows.</td></tr>
215
217
  <tr><td><code>search &lt;query&gt;</code></td><td>Full-text search over every anchor; hits are citable. <code>--in slug</code>, <code>--limit</code>.</td></tr>
@@ -277,14 +279,48 @@ of a gigabyte corpus is a ~2&nbsp;MB file. Recipients need the one
277
279
  </section>
278
280
 
279
281
  <section id="agents">
280
- <h2>Agents</h2>
281
- <pre>backdraft skill install <span class="dim"># the writing skill, into ~/.claude/skills/</span>
282
- backdraft skill install --all <span class="dim"># plus backfill and artifact-reading</span>
283
- backdraft skill install --project<span class="dim"> # into this repo's .claude/skills/</span></pre>
282
+ <h2>Agents and harnesses</h2>
284
283
  <p>The skill's core instruction is a substitution: for source documents, use
285
284
  <code>backdraft read</code>/<code>search</code>, never raw file reads. The
286
285
  token-efficient reference for any agent is <a href="llms.txt">llms.txt</a>
287
- (~800 tokens).</p>
286
+ (~800 tokens). How the skill reaches your agent depends on the harness.</p>
287
+ <h3>Claude Code</h3>
288
+ <p>The repo is its own plugin marketplace, so the plugin route tracks
289
+ releases:</p>
290
+ <pre>/plugin marketplace add spencerbraun/backdraft
291
+ /plugin install backdraft@backdraft</pre>
292
+ <p>Or have the CLI copy the skills into your skills directory:</p>
293
+ <pre>backdraft skill install <span class="dim"># the writing skill, into ~/.claude/skills/</span>
294
+ backdraft skill install --all <span class="dim"># plus backfill and artifact-reading</span>
295
+ backdraft skill install --project<span class="dim"> # into this repo's .claude/skills/</span></pre>
296
+ <h3>Claude Cowork</h3>
297
+ <p>Once the plugin is listed in Anthropic's community directory, it installs
298
+ from Cowork's built-in skills directory; until then, zip a skill folder from
299
+ the repo's <code>skills/</code> and upload it under Customize &gt; Skills.
300
+ Inside a session, run the CLI per-command as <code>uvx backdraft ...</code>,
301
+ since installs do not persist between sessions.</p>
302
+ <p>Sandboxes and credentials: a sandbox usually cannot reach model providers,
303
+ so ingest inside one falls back to the keyless text layer and says so. For
304
+ full fidelity, ingest once on your own machine with the vision extractor.
305
+ The registry lives in <code>.backdraft/</code> inside the project folder, so
306
+ it travels with the folder into any session, and binding and rendering never
307
+ need a key. Put <code>BACKDRAFT_VLM_API_KEY</code> in
308
+ <code>.backdraft/env</code> yourself; never paste keys into a chat.</p>
309
+ <h3>Codex, Cursor, Copilot</h3>
310
+ <p>Agents in this family read skills from <code>~/.agents/skills</code>:</p>
311
+ <pre>backdraft skill install --agent codex <span class="dim"># into ~/.agents/skills/</span></pre>
312
+ <p>Or commit the skill folders to your repo under <code>.agents/skills/</code>
313
+ so every checkout carries them. For Codex cloud environments, add
314
+ <code>pip install backdraft</code> to the setup script, which runs while the
315
+ network is still on.</p>
316
+ <h3>Standing context</h3>
317
+ <p>For any harness that reads AGENTS.md, paste this into your repo's file:</p>
318
+ <pre>## backdraft (cited writing)
319
+ When a document must cite its sources, write it through the backdraft CLI:
320
+ it shows source text with a citation token over every span, and only shown
321
+ spans are citable. In a sandboxed session run every command as
322
+ `uvx backdraft ...` (no install, no PATH edits). Start from
323
+ `uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.</pre>
288
324
  </section>
289
325
 
290
326
  <section id="security">
@@ -312,9 +348,13 @@ registry contains the full text of everything ingested.</li>
312
348
  Text-layer receipts are fine for clean digital PDFs; glossy or scanned ones
313
349
  deserve the vision model.</p>
314
350
  <h3>My artifact has no page images.</h3>
315
- <p>Images are stored at ingest by the VLM path. For text-layer or older
316
- registries, run <code>backdraft snapshot-pages &lt;slug&gt;</code> (local,
317
- free), then re-bind.</p>
351
+ <p>Ingest stores them for every PDF, through both the vision and the text-layer
352
+ path — the latter renders the pages locally, which needs <code>poppler</code>
353
+ on the machine. If ingest printed a note that it could not capture them, install
354
+ poppler; then, for that registry or any built before ingest did this, run
355
+ <code>backdraft snapshot-pages &lt;slug&gt;</code> (local, free) and re-bind.
356
+ Also check you did not bind with <code>--lean</code>, which skips them
357
+ deliberately.</p>
318
358
  <h3>What does <code>not_shown</code> actually catch?</h3>
319
359
  <p>A real token, valid in the registry, that the writing session was never
320
360
  shown, i.e. the writer cited something it didn't read. Only a session ledger