bob ref create — vault notes, Markdown, PDF, and URL targets¶
bob ref create <TARGET> turns a vault note, Markdown, a local PDF, a PDF URL,
an arXiv paper URL, or a web article URL into a Highlights-ready reference.
By default, create files the PDF in lib/, writes the ref/ note and reading
task, commits those paths together, and pushes. If intake is kept or cannot
run safely, create leaves the PDF in xlib/ with a queue reason for
bob ref scan -i or the host's local timer to drain later. Vault notes become
projected reading snapshots; other Markdown uses pandoc, a PDF is stamped as
is, and web article URLs run through the web-article engine.
bob ref create [OPTIONS] <TARGET>...
Examples¶
# Markdown through pandoc (default ref type chat)
bob ref create report.md -P sase
# Local PDF, stamped as-is (default ref type papers)
bob ref create paper.pdf -t papers -P sase
# PDF URL with an explicit stem
bob ref create https://example.com/paper.pdf -N my_paper -P sase
# arXiv paper, preview only
bob ref create https://arxiv.org/abs/1706.03762 -d -P bob
# Web article through the web-article engine (default ref type blogs)
bob ref create https://example.com/article/hello -P sase
# Overrides
bob ref create paper.pdf -T "The Real Title" -N my_paper -t docs -P sase
bob ref create report.md --audio episode.mp3 --include-id -P sase
# Keep the PDF queued for a later bob ref scan
bob ref create report.md -k -P sase
# Web article with saved page and metadata overrides
bob ref create https://example.com/essay -H saved.html -P sase
bob ref create https://example.com/essay -A "Jane Doe" -p 2026-01-02 -d -P sase
# Merge a reading packet into one reference and PDF (2–20 web articles)
bob ref create https://example.com/2024/01/tokio-internals-part-1 \
https://example.com/2024/02/tokio-internals-part-2 -L -P sase
bob ref create URL1 URL2 URL3 -N spotify_bg_agents -P sase
Create-time intake¶
Create-time intake is enabled by default. Use -k, --keep-intake to leave a
new PDF in xlib/ for a later bob ref scan -i or local timer. The same
behavior can be selected with
highlights.ingest_on_create: false; BOB_HIGHLIGHTS_INGEST_ON_CREATE accepts
0, false, no, or off to disable intake and 1, true, yes, or on
to enable it. A nonblank environment value overrides the config value.
When enabled, create takes bob_sync.lock and then scan.lock, pulls the
vault, rechecks the library destinations, moves the PDF and its companions,
writes the reference note and reading task, commits only those paths, and
pushes. It skips the pull for a PDF created directly in lib/. If the vault
is unreachable, a lock is busy, or a collision or planning refusal occurs,
create exits successfully and reports queued: ... (<reason>) · next: bob ref
scan; the PDF stays in xlib/. --dry-run prints the intended intake line
without taking locks or syncing.
Vault note targets¶
A target may be an exact vault-relative route, a unique case-insensitive note
basename, a simple [[route|alias]] wikilink, or an existing in-vault Markdown
path. Existing filesystem paths keep precedence. Dot-directories, _conflicts/,
xlib/, lib/, lit_review/, and shared excluded note directories are not
snapshot notes. An excluded Markdown path keeps the regular Markdown route.
For a vault note, the default parent is the note itself if it is an eligible
root area or non-terminal project. Otherwise create follows its parent: links
for up to eight hops and chooses the nearest eligible area or project. If that
walk finds none, pass -P/--parent. Other target types still require -P.
Explicit parents use the existing bob capture-targets eligibility and
resolution rules (see highlights-ref-sync.md).
The default filename is <note-stem>_<YYMMDD>.pdf; BOB_NOW controls the date
in tests. -N/--name overrides the dated stem, while -o/--output chooses the
complete PDF path. The identity title comes from -T, frontmatter title, the
first H1 outside fences and comments, then a humanized filename stem. The
selected H1 is removed from the projected body only when it supplied the title.
Without -T, the snapshot date is appended to the identity title. -T is the
exact identity title.
By default, closed tasks are summarized as omitted and managed schedule/work
logs are condensed. -C/--closed restores closed tasks in an appendix and full
logs. Blocked [?] tasks with open dependencies, a #prj_is_blocked project,
or no future date stay in a Waiting on lane; tasks blocked only by a
scheduled date after the snapshot day become compact Scheduled rows grouped
This week, This month, and Later. -M/--plain uses the existing plain
Markdown renderer, while keeping the vault-note parent, naming, title, and
source_note provenance. -C and -M only accept one vault-note target
and conflict with each other.
Repeat -S/--section HEADING to keep only named projected sections; names match
case-insensitively, and the dry-run section summary reflects the selection.
Dry run lists earlier snapshots of the same note newest-first as
earlier: <stem> (<status>, N highlights). Both dry run and a real create print
hint: earlier snapshot <stem> is still unread (0 highlights) for each earlier
snapshot that is still ready with 0 highlights. Create never changes, closes,
or abandons those notes.
Status glyphs are open ○, next ◉, in progress ◐, blocked ⊘, done ✓, and
canceled ⊗. Chips show priority, sched, repeats, ref, and moved N×
metadata, along with Keep and quote links where available.
Note snapshot and research-report Markdown PDFs use a clean text layer: ragged
right, no hyphenation, Ligatures=NoCommon for serif, sans, and mono fonts,
and no Type 3 body fonts. Snapshots render through pandoc and XeLaTeX because
the PDFKit measurements found that Chromium/Skia with variable fonts damages
some extracted words.
A second snapshot of the same note on the same day refuses with already
queued (use -f to replace it) or already captured. Pass -N for a
deliberate second snapshot that day. Older plain-Markdown renders of a note
stay where they are when note snapshots are created.
Embedded PNG and JPEG images are copied into the private render scratch as-is when their long edge is at most 1600 pixels; larger images are downscaled with a high-quality filter. GIF embeds use the first frame, and WebP embeds are converted to PNG. SVG and unreadable images become an “Image not printable” placeholder with a warning. Dry runs report the image count, downscaled paths, and unprintable paths.
Dry runs show the selected title, parent, destination, projection summary, and
warnings without running pandoc, xelatex, or listen; they end with writes:
none. Set BOB_HIGHLIGHTS_KEEP_WORKDIR=1 to retain projected Markdown, TeX,
logs, fonts, and staged assets for debugging. -L/--listen passes the rendered
projected.md as {target} and the snapshot PDF as {pdf}. In -M mode,
{target} is the original Markdown.
bob ref create harbor_cranes
bob ref create '[[harbor_cranes]]' -d
bob ref create zk/some_idea -N idea_261016
bob ref create harbor_cranes -C
bob ref create harbor_cranes -S Requirements -S Outline
bob ref create harbor_cranes -M
bob ref create harbor_cranes -L
Other single targets¶
Markdown outside the vault, local PDFs, and URLs continue to require
-P/--parent: the area or project note that owns the reading task. A missing
-P fails before rendering, fetching, or writing with a hint naming
bob capture-targets. Markdown files render ragged-right, with hyphenation
off and Ligatures=NoCommon on the DejaVu serif, sans, and mono families, so
Highlights quotes stay free of hyphen splits and common ligatures.
See web article capture for reader-mode capture, headed
retries, the --html escape hatch, and environment variables. bob ref clip
is a hidden compatibility alias for create with the same targets and options.
Several URLs¶
Two to twenty web-article URLs passed to one create invocation become one
reference and one PDF. The URLs stay in argument order. The collection has one
reading task when create files it, one annotation space, and one optional listen
episode. Use it for material you'll read as one unit; capture an ongoing series
part by part under the same -P. A collection is a snapshot: create does not
add parts to an existing collection.
bob ref create URL1 URL2 -P sase
bob ref create URL1 URL2 -N my_reading_packet -P sase
bob ref create URL1 URL2 -o ~/bob/xlib/blogs/my_reading_packet.pdf -P sase
bob ref create URL1 URL2 -T "Tokio Internals" -P sase
Options can appear before or after the URLs. One target still uses the existing
single-target route. Several targets must all be live HTTP(S) web articles:
local files, arXiv papers, PDFs, and -H/--html replay are not supported in a
collection. A collection accepts at most 20 URLs. Duplicate URLs and any
pre-existing PDF-backed or queued reference that overlaps one of the parts make
the whole command fail. Legacy notes without a Highlights PDF produce a warning
and do not block capture. Captures are staged privately; a failed part leaves
the vault unchanged and reports its part N/M position.
Naming¶
The collection name is resolved before any network request, in this order:
-N/--name.- The filename stem from
-o/--output(the option remains an exact PDF path). - Shared slug words, then a qualifying shared folder, as described below.
snake_caseof-T/--title.- Otherwise the command exits 2 with the URLs and a hint to pass
-Nor-T.
With several URLs and no -N, bob names the reference after the words every
URL's slug starts with, or failing that the folder they all share, ignoring
dates, ids, part numbers, and filler. It needs two meaningful words, or one
when the pages differ only by a part or page number. Otherwise pass -N (or
-T). The derived name is validated and follows the usual _2, _3 collision
walk. The selected source is shown in normal and dry-run output.
The rule is intentionally strict. URLs must have the same site (host compared
case-insensitively, with www. ignored). Each URL must have a usable slug: the
last path segment after skipping short, numeric, date, and known noise
segments. Slugs are split into lowercase words; bob takes the longest shared
word prefix and trims filler, series terms (part, chapter, episode,
page, and related forms), numbers, and hex ids from its ends. It accepts at
least two meaningful words when one is specific rather than generic. One
remaining word of at least four letters is accepted only when the URLs differ
by a part/page number. If the slug rule fails, a shared multi-word folder can
qualify by the same two-word rule; a one-word folder never names a collection.
This prevents common paths such as core or configuration from becoming
misleading names.
Examples: tokio-internals-part-1 and tokio-internals-part-2 derive
tokio_internals; /raft-part-1 and /raft-part-2 derive raft; and
/core/configuration plus /core/usage require an explicit name because
core is too generic. A Spotify set whose slugs share no meaningful start also
requires -N or -T. If every URL has a part/page number and their numeric
order goes backwards, bob warns but keeps the supplied argument order.
Collection document and metadata¶
The PDF opens with a collection title, byline/metadata, and linked contents.
Every article starts on a fresh page with its own masthead and part label. The
PDF has one outline bookmark per article, continuous page numbering, and
part-prefixed fragment ids and assets so repeated names do not collide. Relative
links resolve against each article's URL. The source_url marker value is an
ordered list; after scanning, the single reference note retains every URL, and
bob ref find can locate the collection from any part.
-T overrides the collection title. Otherwise bob uses the longest shared
article-title prefix at a word boundary, or the humanized collection name when
there is no useful shared title. An author shared by every part is kept; when
they differ, bob formats the distinct authors together. The published date is
the earliest date available, and -A / -p override the collection values.
Each part retains its own source and article metadata in the PDF.
--dry-run still loads each web page in the browser but skips composition and
installation. -L/--listen narrates the staged collection PDF as one episode.
See the listen contract below.
Prerequisites¶
Dependencies depend on the target; basic vault capture does not require the full reference-import toolchain.
| Target | Required tools |
|---|---|
| Local PDF | No external renderer; Bob stamps the PDF directly |
| PDF URL or arXiv | curl for downloads |
| Web article | curl for initial URL classification (skipped with --html), uv for the pinned Python adapter, and a browser: Google Chrome at /opt/google/chrome/chrome, /usr/bin/google-chrome, /usr/bin/google-chrome-stable, or the macOS Google Chrome app, then Playwright's bundled Chromium. Set BOB_CHROME for any other executable, including a system Chromium package. Headed retries use Xvfb on Linux and an off-screen window on macOS |
| Markdown | pandoc, xelatex, the LaTeX packages below, and DejaVu Serif, DejaVu Sans, and DejaVu Sans Mono (mainfont, sansfont, and monofont) |
| Markdown with Mermaid | uv and a discovered browser are optional; without them Mermaid blocks print as source with a visible note |
Any target with --listen |
A configured highlights.listen_command (sase listen render ...) that writes the requested MP3. Installing Bob does not install a narration frontend: the SASE plugin (sase plugin install listen) provides sase listen |
Run bob ref doctor to inspect the current host. Its pandoc, xelatex, and
latex_packages rows diagnose Markdown rendering. A missing renderer or
package is a warning rather than a fatal doctor error because other target
types can still work. The package check runs kpsewhich from beside
xelatex when that copy exists, otherwise kpsewhich on PATH. The status
row names the missing files (latex_packages: warn (missing …)). The
warnings section prints one combined tlmgr install … line for every missing
package; run that command with the TeX Live installation that provides your
xelatex. A later render failure can also hint at a single missing package.
Bob's headers require fvextra, lineno, upquote, needspace, and pgf
(tikz.sty). The note snapshot print profile additionally requires
titlesec, enumitem, fancyhdr, lastpage, refcount, and xurl.
Doctor also checks the packages used by pandoc's LaTeX template, including
table and image support. Doctor does not check whether the DejaVu fonts are
installed. If XeLaTeX cannot find DejaVu Serif, DejaVu Sans, or DejaVu Sans
Mono, install them for the host's font system before retrying. Note snapshots
bundle their own fonts (Source Serif 4, Inter, JetBrains Mono, Noto
Emoji) and write them to the scratch fonts/ directory, so no system-font
install is needed for that route.
Mermaid diagrams¶
bob ref create renders explicit Mermaid code blocks as fitted vector figures
on both Markdown routes: ordinary Markdown imports and vault-note snapshots,
including -M, --plain. A block must be tagged as Mermaid: a backtick fence
labelled mermaid, a tilde fence labelled mermaid, or a backtick fence with
{.mermaid}; fences nested in lists and blockquotes
are supported. Bob does not guess from untagged flowchart or graph source,
so use a text fence when you want Mermaid code to remain visible as source.
PDF, PDF URL, arXiv, and web-article imports are unchanged.
Rendering uses the pinned, vendored Mermaid 12.2.0 bundle and DejaVu Sans
font through one offline browser launch per document. A block-level parse or
render failure keeps the source below an italic note and does not fail the
import. If uv, the browser, or the adapter is unavailable, every Mermaid
block remains as source with a note and Bob prints one warning. For example:
diagrams: rendered 3 of 4 Mermaid diagrams
warning: Mermaid diagram 4 of 4 not rendered: Parse error on line 2: …
Figures scale down to fit the text width and at most 80% of the text height. That leaves room on the same page for the heading, return links, and opening caption line, so a tall diagram no longer leaves a blank page or runs into the margin. Figures are still never rearranged or split, and short figures print at natural size. When a figure would print below 50% of its natural size, Bob warns that its labels may be small; that estimate uses the same 80% height budget as the printed scale. The diagrams remain vector text; no raster images are used.
The renderer does not backfill existing library PDFs. Recreate an individual
reference manually when you want its existing PDF replaced. Set
BOB_HIGHLIGHTS_DIAGRAMS=off to skip diagram rendering while debugging; the
dry-run still counts the candidate fences and says rendering is disabled.
To bump Mermaid, replace scripts/web_clip/vendor/mermaid.min.js, update its
version and SHA-256 in web_clip_adapter.py and vendor/README.md, then run
just check-web-clip-adapter and the gated live Mermaid render test. Keep the
bundle self-contained and verify the vector-font and no-raster checks before
updating the pin. Other diagram languages, SVG image embeds, a listen-skip
marker, persistent caching, landscape pages, and Mermaid accessibility-title
metadata are outside this version's scope.
macOS BasicTeX: installing a missing package in user mode¶
BasicTeX ships a small package set, so a render can fail with
! LaTeX Error: File '<name>.sty' not found even when pandoc and XeLaTeX
are present (verified 2026-10-10: needspace.sty missing from BasicTeX /
TeX Live 2026). Use kpsewhich, xelatex, and tlmgr from the same
installation; on that host all three live under /Library/TeX/texbin,
while the system tree (/usr/local/texlive/2026basic) is root-owned.
For a missing package such as needspace, initialize the
default user tree once and install without administrator access, as the
same user that runs the file hook:
tex_home=$(/Library/TeX/texbin/kpsewhich -var-value=TEXMFHOME)
if ! test -f "$tex_home/tlpkg/texlive.tlpdb"; then
/Library/TeX/texbin/tlmgr init-usertree
fi
/Library/TeX/texbin/tlmgr --usermode install needspace
/Library/TeX/texbin/kpsewhich needspace.sty
The default user tree (~/Library/texmf) is already on kpsewhich's
search path. User-tree packages are maintained separately from the system
installation, so specify --usermode on future package operations too.
Verify with the latex_packages doctor row (ok with all packages checked)
and then a real render: a doctor exit of zero alone does not establish render
readiness, because missing render dependencies are warnings by design,
and Markdown --dry-run returns before pandoc runs.
The [WARNING] Deprecated: --highlight-style line is unrelated noise
from pandoc, not the fatal error; the ! LaTeX Error line names the
actual missing file. See the official
TeX Live user-mode documentation;
the CTAN needspace entry confirms its
TeX Live package name.
--dry-run plans the import and skips installing the final PDF and audio.
What it still does depends on the target:
| Target | During --dry-run |
|---|---|
| Local PDF | Reads the local file and prints the plan. No renderer runs |
| Markdown | Returns before pandoc and XeLaTeX run. When Mermaid fences are present, prints their approximate count; dry-run never starts the adapter or browser |
| PDF URL | Downloads the PDF with curl, then prints the plan |
| arXiv | Downloads the API metadata and the PDF, then prints the plan |
| Web article | Launches the browser and loads the page. Image localization and the PDF write are skipped |
Web article with --html |
Skips the curl classification fetch, launches the browser in html-file mode, and loads the page. Image localization and the PDF write are skipped |
URL targets still use the network during --dry-run. Use
bob capture --dry-run '<URL>' to preview automatic URL routing from local
state only.
Targets¶
| Kind | Route | Default -t |
Title (first hit wins) | Stem (first hit wins) |
|---|---|---|---|---|
Markdown (.md) |
Rendered with pandoc/XeLaTeX | chat |
-T, frontmatter title, first H1, file stem |
-N, file stem |
Local PDF (.pdf or %PDF- magic) |
Stamped as-is | papers |
-T, plausible Info /Title, humanized stem |
-N, file stem if it passes --name validation, else snake_case |
PDF URL (PDF content-type or sniffed %PDF-) |
Downloaded with curl, stamped as-is |
papers |
-T, plausible Info /Title, humanized stem |
-N, URL slug, short-title stem, <host>_<YYYYMMDD> |
arXiv (arxiv.org abs/html/pdf) |
PDF fetched from arxiv.org/pdf/<id>, metadata from the API |
papers |
-T, arXiv API title, plausible Info /Title, arXiv <id> (no version) |
-N, short-title stem, arxiv_<id> (no version) |
| Web article (HTML 2xx or 403/429/503) | Captured with the web-article engine | blogs |
web-article rules (-T overrides the title) |
web-article rules (-N) |
| Several web articles (2–20 URLs) | Captured in order and composed into one PDF | blogs |
Shared title prefix or humanized name (-T overrides) |
Collection naming waterfall above |
Short-title stem: when the text before the first : is 1–4 words, that
prefix is the short name (EA-Graph: … becomes ea_graph); otherwise the
first 6 words stand in, run through snake_case (capped at 80 chars).
A trailing .md, .markdown, or .txt is dropped first (case-insensitive),
and the same rule applies to the title-stem fallback.
Attention Is All You Need and Attention Is All You Need.md both become
attention_is_all_you_need. URL-derived stems discard trailing opaque hex IDs
and UUIDs; if the final meaningful URL path segment is itself an opaque ID,
there is no URL stem and the next configured fallback is used.
Plausible Info title: trimmed and whitespace-collapsed, at least 3 chars
with a letter, not equal to the source file stem or URL stem
(case-insensitive), not a bare filename (.pdf, .doc, .docx, .tex,
.dvi, .ps, .pages), not an exporter default (Microsoft Word - …,
Microsoft PowerPoint - …), and not untitled/title. The same
normalization applies to /Author.
Local links and return pills¶
Every same-document link ([words](#some-heading)) gets a small raised
letter tag beside its words, and its target grows a row of return pills just
underneath. Each pill reads ↩ p. N plus the matching tag: tapping it jumps
back to the passage the reader left, about a line and a half above the
original sentence, keeping the current zoom and horizontal position. The
heading text, TOC, and PDF bookmarks never change, and before tapping
anything the row also shows which pages cite that section.
Eligibility:
| Link location or target | Result |
|---|---|
| Paragraphs, plain blocks, line blocks, definition lists, block quotes, Div content, list items (not nav lists), table body cells, figure content, footnotes | Tagged + paired (when the target is return-capable) |
| Headings, figure captions, table captions, table head rows (including intermediate body heads), table foot rows, hand-written TOC lists | Untagged: rewritten to the canonical id, still a working forward link |
| Target is a CodeBlock, Table, Figure, Image, Code, Link, Span in a skip context, or a duplicated id | Untagged: working forward link |
| Listen cards (already raw LaTeX) and metadata (title, abstract) | Untouched |
Unresolvable target (#, missing, or ambiguous alias) |
Dead: plain text (a Span of the link content) plus a report record; applies in every context |
Non-# links |
Untouched |
Return-capable targets are Headings, Divs, and Spans outside headings,
captions, and table head/foot rows. Each tagged link gets its own sequential
letter in reading order (a…z without l, o, q, then aa, ab, …), and
each target carries exactly one pill row in source reading order.
Targets resolve in this order: the exact pandoc id, then the percent-decoded
id, then a GitHub-style alias computed by pandoc's own gfm reader (only
unique aliases count). Real ids always beat aliases; nothing is fuzzed by
case or heading text. Headings that are linked often are safest with an
explicit {#id}, which also fixes ambiguous-alias warnings.
On success bob prints a links: line after pages: (links: 27 paired
(11 targets), plus · N via GitHub-style slugs, · N untagged, · N dead
when nonzero; links: none when there are no # links; links: off
(bob-return-links: false) for the opt-out below). Each dead link and each
duplicated anchor also prints one warning: line on stderr: a missing target
renders as plain text, an ambiguous GitHub-style slug names the fix
(give the heading an explicit {#id}), and a duplicated anchor keeps working
forward links but gets no return pills.
Frontmatter bob-return-links: false turns the whole feature off for that
document. Internal links, URLs, file links, citations, tags, and pills share
one link ink (#2F5E96); the TOC stays black. Existing PDFs gain the feature
only when re-rendered. When a render pairs links, bob stamps
return_links: true so bob ref sync can keep exports clean. Dry runs
preview the planned marker without return_links; the flag is added only
after an actual render pairs a link.
PDF route¶
Before any write the PDF must load with lopdf, have at least one page,
not be encrypted, and be smaller than 95 MiB (files of 50 MiB or more print
a warning). Encrypted PDFs fail with save an unencrypted copy and pass
that file.
When page 1 already carries a standalone /Text note, a parseable
Highlights marker means already captured (see below); any other note is
kept and bob's marker is prepended ahead of it, so it stays the first
standalone note.
Stamping loads the PDF, embeds the marker, sets Info Title/Author, saves
to a private scratch stamped.pdf, verifies the marker round-trips and
the page count is unchanged, and only then atomically installs the target.
A local source file is never modified.
URL routes stamp source_url (cleaned URL, or https://arxiv.org/abs/<id>
with version), author, published (arXiv first-version date), captured
(local YYYY-MM-DD), and always id (the stem). Local PDFs stamp id
only with -i (with -N, -i embeds the name).
A local PDF inside lib/ or xlib/ with a marker is already captured:
already captured; bob ref sync <PDF> re-syncs it plus the
--listen attach hint. Without a marker it must be moved out first. A PDF
outside the vault, or a downloaded PDF (PDF URL or arXiv), that already
carries a Highlights marker is refused before any write:
PDF already carries a Highlights marker: <path> with a
bob ref sync / create <library PDF> --listen hint.
arXiv¶
URL recognition mirrors sase-listen (abs, html, pdf, new- and
old-style ids, version kept, query/fragment ignored, .pdf only on
/pdf/). The fetch always goes to https://arxiv.org/pdf/<id> and never
falls back to the abstract page. arxiv:ID and bare ids are out of scope.
Metadata comes from https://export.arxiv.org/api/query?id_list=<id> (20 s
cap): the first <entry> title (whitespace collapsed), every
<author><name>, and the <published> date part. An api/errors entry, a
network failure, or a parse failure prints a warning: and falls back to
PDF Info metadata. Authors display as A, A and B, A, B, and C, or
A et al..
Dedupe¶
Every URL dedupes before any fetch on its syntactic key. arXiv spellings
(abs/html/pdf, with or without version) share
https://arxiv.org/abs/<id>; everything else uses the web-article key. Ref notes
record source_url and legacy url:, and queued intake markers record
both, so existing paper refs take part instead of duplicating. A
PDF-backed ref-note hit or a queued-intake hit refuses (use --force
only to overwrite the same intake target). The refusal hint points at
--listen attach mode. A URL recorded only by legacy notes without a
Highlights PDF warns (already in the library as <path>, a note without
a Highlights PDF; capturing a fresh copy) and captures a fresh copy
instead of refusing; with --listen it is a normal capture plus listen,
never attach mode. A dry run adds
legacy: <path> (superseded by this capture) to its report.
Import deduplication reads every URL from scalar or list-valued
source_url/url fields in ref notes and queued intake markers, matching
the URLs indexed by bob ref find.
For several URLs, each part is checked against recorded sources before capture.
Any part already in a PDF-backed ref or queued intake refuses the complete
collection, even if the remaining URLs are new. --force applies only when
the queued collection has exactly the same URL set, in any order. A legacy
PDF-less note is reported as a warning and the collection may proceed. The
collection marker stores the cleaned URLs in argument order as a YAML list;
create writes that list to one ref note (or a later bob ref scan -i does so
when intake is queued). A later create of an individual
part is refused as already captured, and bob ref find <part URL> locates the
collection.
Default targets dedupe by walking <stem>, <stem>_2, <stem>_3, …
(up to <stem>_999): a candidate is occupied when its matching PDF, sidecar,
or note exists in any reference-type directory under xlib/, lib/, or
ref/, or when any note under ref/ already records that id. A free
candidate is chosen, a candidate occupied by a different reference is
skipped, and a candidate occupied by the same reference refuses
(already captured as <path> for library or ref-note occupants,
already queued in <path>; pass --force to overwrite it for intake
occupants). Both refusals keep
hint: to add audio to that capture, run bob ref create <library PDF> --listen.
For Markdown and local PDFs the identity is the create-time title (the -T
override, else frontmatter/H1/stem or Info title/filename): a candidate is
the same reference when its intake or library PDF carries that title as its
page-1 marker title or raw Info Title (trimmed, whitespace-collapsed,
case-insensitive). A different source with the same filename but a different
title is suffixed; two different sources with the same stem and title still
refuse (pass -N). For URL routes the identity is the source URL dedupe
key (arXiv checks both the paper and URL keys): in practice only the
--force same-intake overwrite refuses, everything else suffixes. An
explicit -o/--output path keeps the exact path. Whenever the route records
an id, it uses that output PDF's basename as the id; if another note already
uses that id, Bob refuses and asks for a different output filename because the
exact path cannot be collision-suffixed. On default targets, the collision walk
also reserves ids case-insensitively across all of ref/.
When the walk skips at least one candidate, dry-run and success output print
renamed: <base>.pdf is taken by <first occupant>; using <final>.pdf just
before pdf:. --force only overwrites the same reference's intake PDF,
never a different reference. If all 999 candidates are taken, the command
fails with no free default filename for <stem>; pass -N/--name or -o/--output.
Ingest boundary¶
src/native/highlights_ref/ingest.rs::ingest_url is the typed,
non-printing URL ingest shared by the capture background worker and
bob gkeep pull. It composes the same building blocks as create
(resolve_url_syntactic, fetch_and_route, sources dedupe,
pdf_target planning/stamp/install, ClipAdapterClient). It takes a
vault root, a cleaned URL, and the resolved parent route, and returns
Created, AlreadyInLibrary, or AlreadyQueued, or an IngestError
with a snake-case kind and a retryable() flag. Fixed defaults:
route-default ref type (blogs for articles, papers for PDFs and
arXiv), status ready, no audio, no force, no title or name override.
It holds
the machine-wide
${XDG_STATE_HOME:-~/.local/state}/bob-cli/ref/ingest.lock (fs2
exclusive, blocking) for the whole call — the lock serializes the
capture worker and Keep pull — and reports
waiting for another clip… once through its progress callback while it
waits. bob ref create keeps its own printing routes and never calls
ingest_url. Ingest uses the same default-target suffixing (<stem>_2,
<stem>_3, …) with its URL identity and force = false. Ingest writes
nothing to stdout and prints nothing to stderr itself.
Error kinds: network, timeout, http_status (retryable only for 408,
429, and 5xx), browser, and dependency are retryable; blocked,
thin, render, unsupported_content, collision, invalid_url, and
internal are permanent. IngestError::fallback_note(url) renders the
shared ⚠️ Clip failed (<kind>): <message> · retry: bob ref create <url>
bullet used by every fallback path.
Listen¶
-L, --listen (conflicts with -a and -n) narrates the target with the
configured highlights.listen_command and binds the episode as the PDF's
companion audio. For a multi-URL collection, bob passes the staged composed
PDF as {target} and {pdf}, with the resolved collection title as {title};
the episode binds to the one installed collection PDF. If the same URL set is
already captured or queued, in any order, --listen attaches an episode to that
capture. Partial overlap still refuses.
bob ref create https://arxiv.org/abs/1706.03762 -L
The URL % shorthand in bob capture and bob gkeep pull (with URL @
kept as a compatible alias) clips like
bob ref create <URL> -P <parent> -L (same placeholders, same install
guarantees, same create-time intake). It never adds audio to existing
captures; background ingest runs the command quietly (never stdout) with
progress on stderr.
All or nothing, on every route:
- The listen command is resolved and validated first: an unconfigured or invalid command fails before any fetch or render.
- The target is resolved, deduped, and planned (see above).
- The episode destination
<target>.mp3is planned: an existing mirrored library audio refuses. - The PDF is produced in private scratch (rendered, downloaded, or adapter-captured).
- A dry run prints the plan with
listen: would-run …and stops. - The
sase listen rendercommand runs with stdin, stdout, and stderr inherited, so the listen plugin's live checklist and its "Published to apollo" summary appear unchanged. bob printslisten: run sase listen render ...first (shell quoting is display-only). - Collisions and the audio plan are re-checked (the vault may have
changed during a long listen), the audio is installed, then the PDF.
If the PDF install fails, only an audio file this run created is
deleted. If this episode or an earlier one is still in the sase-listen
publish outbox, bob ends with a
warning: podcast feed: …line and a hint.
Placeholder values: {target} is the cleaned URL for URL routes (the listen renderer
does its own arXiv rewrite), the absolute source path for a local PDF, and
the staged <scratch>/<stem>.pdf render for Markdown (pandoc sets its Info
Title, which the listen renderer reads). {pdf} is the unstamped scratch PDF (or
the existing capture in attach mode), {audio} is <scratch>/<stem>.mp3,
and {title} is the resolved title. bob passes every value as a single argument to sase itself;
never quote the placeholders. A direct sase-listen render ... setting migrates automatically to sase listen render ...; wrappers, shell programs, chains, pipelines, redirections, and expansions are rejected. Markdown skips audio discovery under
--listen, and the listen card's Play URI targets <stem>.mp3.
Success looks like this (arXiv example):
listen: run sase listen render https://arxiv.org/abs/1706.03762 -e full -o /tmp/bob-create-…/attention_is_all_you_need.mp3
… the listen plugin's live checklist and summary, unchanged …
ok created Highlights-ready PDF
source: https://arxiv.org/abs/1706.03762 (arXiv 1706.03762)
pdf: /home/bryan/bob/xlib/papers/attention_is_all_you_need.pdf
audio: /home/bryan/bob/xlib/papers/attention_is_all_you_need.mp3 (from --listen)
title: Attention Is All You Need
author: Ashish Vaswani et al.
published: 2017-06-12
captured: 2026-10-06
status: ready
parent: sase
id: attention_is_all_you_need
pages: 15 · size: 2.2 MB
ingested: lib/papers/attention_is_all_you_need.pdf → ref/papers/attention_is_all_you_need.md · task → sase.md ^ref-attention-is-all-you-need
sync: committed <sha7> · pushed
A failed listen command writes nothing (nothing was written to the vault;
rerun the same command once the listen error above is fixed); rerunning is
cheap because the listen renderer resumes from its caches. An interrupted listen
exits 130. Exit 0 with no MP3 at {audio} is an error. A failure after the
episode exists keeps the scratch directory, prints kept: <audio path>,
and hints at rebinding it (bob ref create <TARGET> --audio <path>
in normal mode, copy it to <xlib dest> for scan to pair in attach mode).
Attach¶
When --listen is given and the target is already captured, the new
episode attaches to the existing capture instead of refusing:
- A ref-note dedupe hit: the PDF is the note's
source_pdf(it must resolve insidelib/and exist, elseref note has no resolvable source_pdf). - An intake dedupe hit (not a
--forcerecapture): the queued intake PDF. - A local PDF target inside
lib/orxlib/that carries a marker.
The command refuses when the capture already has audio (any allowed
companion beside lib/<rel>.pdf or xlib/<rel>.<ext>, or an audio
field on the ref note: already has companion audio: <path>). Otherwise
it runs the listen command with {target} as the cleaned URL (or the PDF
path), {pdf} as the existing PDF, {title} as the note or marker
title, and {audio} as <scratch>/<stem>.mp3, then installs the episode
at xlib/<rel>.mp3. Intake then files that episode:
- Library capture (
lib/<rel>.pdf): move the mp3 intolib/, re-sync the existing note so it gainsaudio:frontmatter and the embed, and commit the mp3 plus the note (bob ref create: attach audio …). - Queued capture (
xlib/<rel>.pdf): file the PDF with the mp3 as a companion, the same as a normal create.
-k (or a disabled ingest switch) keeps today's attach output, including
next: bob ref scan; drain the queue with bob ref scan -i or its local
timer. A failed intake prints that same next-step line so the queued files
can still be paired.
ok attached listen episode to existing capture
source: https://arxiv.org/abs/2608.04278 (arXiv 2608.04278)
pdf: /home/bryan/bob/lib/papers/ea_graph.pdf (unchanged)
ref: /home/bryan/bob/ref/papers/ea_graph.md
audio: /home/bryan/bob/xlib/papers/ea_graph.mp3 (from --listen)
title: EA-Graph: Artifact-Anchored Verification Memory for Coding Agents under Upstream Drift
ingested: lib/papers/ea_graph.pdf → ref/papers/ea_graph.md
sync: committed abc1234 · pushed
Configuration¶
highlights:
# `bob ref create --listen` narrates the target
# with `sase listen render ...`, which must write MP3 audio to {audio}; bob binds it as
# the PDF's companion audio. bob passes {target} {pdf} {audio} {title} as single
# arguments to `sase` itself — do not quote them. The plugin's feed.auto_publish publishes the
# episode. Installing Bob does not install a narration frontend.
listen_command: sase listen render {target} -e full -o {audio}
A direct sase-listen render {target} -e full -o {audio} setting migrates automatically
to sase listen render ... in memory (the file is never rewritten). Wrappers,
shell programs, chains, pipelines, redirections, and expansions are rejected with
the canonical remediation. The managed setup above is the SASE plugin
(sase plugin install listen).
BOB_HIGHLIGHTS_LISTEN_COMMAND overrides the configured command. The setting must be
a direct sase listen render ... invocation (a direct sase-listen render ...
migrates); it must contain {audio} plus {target} or {pdf}; unknown placeholders,
quoted placeholders, wrappers, chains, pipelines, redirections, and shell expansions
are errors. bob ref doctor reports the listen_command row (none, ok, or warn)
and the listen_feed queue row, and checks the fixed sase launcher. An unconfigured
--listen fails with the config snippet above.
Troubleshooting:
- A missing launcher (
sase: command not found) or a failing plugin:saseitself is missing from PATH, or the listen plugin (or its credentials) failed. Check withcommand -v sase,sase listen --help, andbob ref doctor --no-hooks. Doctor'slisten_commandrow checks the fixedsaselauncher and template validity (anokrow is not proof the plugin works). There is no fallback tosase-listen: a directsase-listen render ...setting migrates tosase listen render ..., and wrappers/shell programs are rejected with the canonical remediation. The failed command left the vault untouched and can be rerun after SASE and the setting are corrected. Temporary override:
BOB_HIGHLIGHTS_LISTEN_COMMAND='sase listen render {target} -e full -o {audio}' \
bob ref create https://interconnected.org/home/2026/01/15/reminders -i -L -P sase
Permanent repair: set highlights.listen_command in the managed Bob
config to the canonical plugin template above and apply it.
- A listen-renderer bot-wall 403 on an article that bob captured headed: bob
captured the PDF fine, but the renderer fetches the URL itself and fails.
Replay the page another way, or narrate the staged PDF.
- A credential prompt from pass (via sase-listen's api_key_command):
it inherits bob's stdin, so answer it in the terminal and the run
continues.
- A queued publish (remote publish failure): the episode still binds and the
vault is fine, but it will not reach AntennaPod until the feed host is fixed.
Check sase listen doctor on the feed host, then run
sase listen publish --pending; pull to refresh the feed in AntennaPod.
For example, a feed host whose token_command needs an unlocked gpg-agent
fails every non-interactive publish.
- A rerun after failure reuses the renderer's source, writer, and chunk
caches, then re-masters and re-publishes; a kept scratch episode rebinds
with --audio.
Scratch staging¶
Renders, downloads, stamped copies, and filter files live in a 0700
scratch directory (TMPDIR when short, else /tmp) until the final atomic
installs, because create, scan, and the per-host intake drain treat every
file in xlib/ as input.
It is removed on drop, kept with BOB_HIGHLIGHTS_KEEP_WORKDIR=1 (or the
legacy BOB_WEB_CLIP_KEEP_WORKDIR=1). The note snapshot renderer keeps the
same ownership: with the keep flag set, the scratch directory retains
projected.md, note-profile.tex, note.lua, fonts/, glyphs.lua, the
pandoc-generated note.tex, note.log, and note.pdf for debugging.
When Mermaid blocks are present, diagrams/ holds the collect manifest,
render results, and per-diagram PDFs until XeLaTeX finishes; the same keep flag
retains those files for inspection.
Environment variables¶
| Variable | Meaning |
|---|---|
BOB_HIGHLIGHTS_CURL |
Replaces curl (test seam, like BOB_PANDOC_COMMAND) |
BOB_HIGHLIGHTS_RESOLVE |
Replaces DNS for the fetch address check: comma-separated host=ip pairs with * as a wildcard (test seam) |
BOB_HIGHLIGHTS_KEEP_WORKDIR |
1 keeps the scratch directory for debugging |
BOB_HIGHLIGHTS_DIAGRAMS |
off, 0, false, or no skips Mermaid rendering; skipped blocks print as source without a fallback note |
BOB_PANDOC_COMMAND |
Overrides pandoc for the Markdown route |
BOB_XELATEX_COMMAND |
Overrides xelatex for the note snapshot print profile (test seam, like BOB_PANDOC_COMMAND) |
BOB_HIGHLIGHTS_AUDIO_LIBRARY |
sase-listen library for Markdown audio discovery |
BOB_HIGHLIGHTS_LISTEN_COMMAND |
Overrides highlights.listen_command for --listen |
Fetching runs curl -sS --proto =http,https --connect-timeout 15
--max-time 300 --max-filesize 95M, follows up to 10 redirects manually
through URL validation (a private-host redirect is refused), and reports
curl exit codes (6/7/28/35/60/63) with hints. Before every hop the host
is resolved and refused when any address is non-global (<host> resolves
to a private address (<addr>)); curl is then pinned to the checked
address with --resolve host:port:addr. The article adapter's own browser
navigation is not pinned: only bob's curl fetches go through the
resolved-address check. A missing curl hints at
install curl or set BOB_HIGHLIGHTS_CURL. On a TTY one
fetching <host>… line goes to stderr. bob ref doctor reports the
curl row.
Verified on athena and the Mac¶
The extended note snapshot corpus gate ran on athena on 2026-10-17 from this
branch (working-tree parent 860f0be plus working-tree resolver, visibility,
corpus-gate, and docs changes), with pandoc 3.1.11.1 and XeTeX
3.141592653-2.6-0.999996 (TeX Live 2025/dev/Debian):
- 103 area/project notes rendered successfully; 5,081 Markdown notes dry-ran successfully.
- 0 render failures, 0 dry-run failures, 0 missing-glyph warnings, 0 overfull warnings, 0 Type 3 fonts, and 0 residue findings (status-chip rows, raw block-ID labels, blank lines before task text, false local-heading warnings, false unresolved-file warnings). The vault git status was unchanged.
- Dry runs reported 5,543 unresolved-reference warnings and 4,348 parent
fallbacks through
body(plus 1 same-day name fallback). The gate treats an unresolved target as a failure when a matching vault file exists, including unique basename matches for directory-qualified targets; the remaining warnings are genuine missing notes, attachments, headings, or block fragments, plus cyclic or oversized embed skips. The summary contains no vault note contents.
The Mac verification ran on 2026-10-17 at df4e120, before these fixes. The
Mac used pandoc 3.11 and BasicTeX TeX Live 2026, with user-mode titlesec,
enumitem, and lastpage. The gate rendered 101 notes and dry-ran 5,062; it
reported 0 failures, 0 missing glyphs, 0 Type 3 fonts, and 0 overfull
warnings, and left the vault unchanged. The PDFKit probe paired 101/101 PDFs;
all 240 unknown tokens were explained (207 “In this note” chrome, 29
return-link tag joins, and 4 wikilink-suffix joins). The research-report probe
found 0 hyphen splits and 0 ligatures. These fixes add no TeX packages or
fonts.
The 2026-10-17 Highlights round trip returned a highlight spanning a task row
and its child bullet with every word byte-identical to the source; the child
row's list-bullet dash was included in the quote. The ref note carries
source_note: "[[sase_blog]]". A live #task-comment follow-up and image
highlight were not exercised; CLI annotation-task tests cover that path.
Live verification ran 2026-10-06 into a scratch vault (-b /tmp/lv-vault):
- Every non-listen target kind created, and
scanwrote 5 ref notes (markdown, local PDF, arXiv 1706.03762, PDF-URL dummy.pdf, Wikipedia article); a pdf-spelling dedupe rerun refused with the--listenhint. - A real unpublished full-edition render
(
BOB_HIGHLIGHTS_LISTEN_COMMAND='sase listen render {target} -e full -o {audio} --no-publish') streams the listen plugin's Rich live checklist unchanged, and a failed listen writes nothing to the vault. Cost estimate for the arXiv paper was ≈$0.15 (10 chunks). - The full-edition TTS backend stalled on 2026-10-06: the arXiv render sat
at Synthesize 6/10 for 45+ minutes with zero new cached chunks, and a
fresh 239-word local-PDF render synthesized 0/6 in 9 minutes. The
--listen→scan→ player path was therefore proven with a stub listen command writing ID3-marker bytes to{audio}:create -Lexited 0 withaudio: … (from --listen),scanmoved the pair tolib/papers/, and the ref note carriesaudio: "[[lib/papers/<stem>.mp3]]"plus a![[…mp3]]player under the^reftask. Attach mode (create <in-vault-PDF> -L) printedok attached listen episode to existing capture, left the library PDF bytes unchanged, and a rescan paired the episode; re-attaching where audio already exists refuses withalready has companion audio. cargo test highlights: 152 passed, 1 failed (listen_filter_renders_card_and_encoded_play_linkexpects\&inside\href, but athena's pandoc 3.1.11.1 emits a bare&; recorded as a follow-up on bead bob-cli-4s.6).