Skip to content

Task tag marks

This guide is the authoritative display contract for task tag marks, in the style of docs/date-marks.md. The JavaScript mirror lives in bob-ledger-tools (src/138-task-tag-marks.js and src/138-checklist-task-marks.js pure core plus src/264-plugin-task-tag-marks.js Live Preview, rendered views, and toggle); its tests run the conformance vectors below verbatim. There is no api namespace (the top-level api stays v3; add one only when a consumer exists).

What you see

#task is the Tasks global filter (globalFilter: "#task", removeGlobalFilter: false), so about 3,400 task lines start with an accent-colored #task tag pill. That pill repeats on almost every line and adds the most visual noise. Every exact #task tag on a task line renders as one small teal identity hash glyph instead:

before: - [ ] #task Rename the queue input  โ–ฎโ–ฎโ–ฎ  + Sep 29  โง— today
after:  - [ ] โŒ— Rename the queue input  โ–ฎโ–ฎโ–ฎ  + Sep 29  โง— today      (โŒ— โ‰ˆ the teal identity hash glyph)
        - [ ] Pack charger                                         (plain checklist: no glyph, unchanged)
dash.md Tasks results:  - [ ] Rename the queue input  โ–ฎโ–ฎโ–ฎ  + Sep 29   (tasks_results = hide: tag dropped)

Hovering the glyph shows #task ยท tracked task plus the demote hint. Moving the cursor into the glyph, or clicking it, reveals #task for editing.

Principles

Display-only: the text #task stays the only stored form, and nothing writes the mark. Tasks, Dataview, bob-cli, and capture semantics are unchanged. Small but distinct: it repeats on every task line, so it uses no pill or resting background, and one constant identity ink that never encodes state. It is truthful, never guessing: a mark appears only on exact, whole #task tags on task lines; anything else stays Obsidian's normal tag pill. It is reversible: the cursor or a click reveals the raw tag, source mode shows raw text, a session toggle restores the pills instantly, and without the plugin the vault looks exactly as it does today. There is one glyph definition, a CSS mask shared by every host.

The glyph: a soft slanted hash

A Lucide-style hash keeps the tag affordance: you can see the tag is still there and still editable. It is drawn thin, slanted, and round-capped, so it reads as an icon rather than text. Geometry (viewBox 0 0 16 16, stroked, used as a CSS mask, tunable ยฑ0.3 units): stroke-width 1.8, stroke-linecap round, path M6.5 3 5.5 13M10.5 3 9.5 13M3.5 6.25h9M3.5 9.75h9. The glyph is centered at (8, 8), with roughly square cells, and is about the height of a text #. CSS custom properties, defined once on body:

--bob-task-tag-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M6.5 3 5.5 13M10.5 3 9.5 13M3.5 6.25h9M3.5 9.75h9'/%3E%3C/svg%3E");
--bob-task-tag-hue: color-mix(in srgb, var(--color-cyan, #00bfbc) 72%, var(--text-normal));

Ink

The ink is one constant identity hue: a deep teal that means "tracked task" and nothing else. It never follows status, lane, priority, or freshness; that job stays with the checkbox and line tint. It never uses red, orange, yellow, green, or blue, and it stays legible on the red Blocked line tint.

Hue Already means
red Blocked (line tint, blocked chips)
orange attention: due freshness, ROTTEN, repair flags, the Pending chip
yellow In Progress (line tint, the half-ring progress mark)
green Next (line tint), fresh โœ“ today
blue the Ready lane
violet accent tags (#prj, #gtd, โ€ฆ) and links
cyan inline code chips (--inline-code-hue: var(--color-cyan))
grey priority and date marks, cancelled

Vault counts (2026-10-08): 3,406 #task lines, 2,934 closed, about 470 open โ€” mostly Blocked ([?], 383 lines) on the red tint, plus Next ([*], 19), In Progress ([/], 7), and plain [ ] (63). 656 #task lines carry another tag pill, usually #prj in the violet accent, beside the glyph.

The chosen teal blends theme cyan toward the text color so it adapts to both themes: about #0a9391 in light (about 3.8:1 on white, above the 3:1 non-text contrast floor) and a soft aqua of about #79dedc in dark. Teal complements the red Blocked tint, so it is crispest where most open #task glyphs sit, and it stands apart from the violet #prj pill. It shares a hue family only with inline code, a bordered monospace chip the glyph never reads as. Defined on body:

--bob-task-tag-hue: color-mix(in srgb, var(--color-cyan, #00bfbc) 72%, var(--text-normal));

The theme hook --bob-task-tag-color is unset by default and still wins when set.

Anatomy and tones

There are two hosts with one shared rule set, both gated by body.bob-task-tag-marks:

  1. Live Preview widget: an empty span.bob-task-tag-mark with role="img", aria-label, and data-tooltip-position="top".
  2. Rendered views: Obsidian's own a.tag element with the class bob-task-tag-mark added, plus the same aria-label and data-tooltip-position. The element is never replaced: its text #task stays in the DOM for copy and screen readers, and its native click (tag search) keeps working.

The shared rules use family metrics: font-family: var(--font-interface), font-size: 0.8em, display: inline-block, position: relative, a content-box glyph area of 1.08em square, padding 0.06em 0.14em, margin 0 0.08em, border: 0, border-radius: 999px, background: none, text-decoration: none, overflow: hidden, white-space: nowrap, color: transparent (this hides the a.tag text), and vertical-align: -0.1em (tunable so the hash sits on the text baseline like a #). Every theme tag-pill property (--tag-* padding, background, border, color, weight, size) is overridden with enough specificity (body.bob-task-tag-marks a.tag.bob-task-tag-mark). The glyph is drawn by ::before { content: ""; position: absolute; inset: 0.06em 0.14em; background-color: var(--bob-task-tag-ink); } with prefixed and unprefixed mask-image: var(--bob-task-tag-glyph), mask-size: contain, mask-repeat: no-repeat, mask-position: center, plus print-color-adjust: exact (and the -webkit- form) so PDF exports keep the glyph.

Situation Ink (--bob-task-tag-ink) Other
rest var(--bob-task-tag-color, var(--bob-task-tag-hue)) opacity 0.9
hover var(--bob-task-tag-color, var(--bob-task-tag-hue)) opacity 1; capsule color-mix(in srgb, var(--bob-task-tag-ink) 14%, transparent) on both hosts (not currentColor: it is transparent here)
resting: closed task (x, X, -) color-mix(in srgb, var(--bob-task-tag-color, var(--bob-task-tag-hue)) 40%, var(--text-faint)) opacity 0.75, derived from the nearest li.task-list-item[data-task] / .HyperMD-task-line[data-task] like the priority mark; wins on both hosts

Cursor: default on the Live Preview widget (as in the family) and pointer on the a.tag host (it is a link). Transitions (opacity, background-color, 120ms) are off under prefers-reduced-motion. The theme hook --bob-task-tag-color is unset by default. The ink is constant: it names what the line is, never its state.

Tooltip

The text is exactly #task ยท tracked task followed by Ctrl+Shift+] to demote to a bullet (lines joined with \n). It never contains ::, because Dataview re-scans innerHTML. Exported as the TASK_TAG_MARK_TOOLTIP constant. Ctrl+Shift+] is task-status-cycler's toggle-obsidian-task hotkey, the existing gesture that removes the checkbox and #task.

Eligibility

  • Tag token: the exact, case-sensitive text #task. The character before it is the start of the text or whitespace. The character after it is the end of the text or not a tag character (/[\p{L}\p{N}_\/-]/u). So #tasks, #task/sub, #Task, foo#task, and (#task) never match, while #task! does.
  • Live Preview: only on task lines, using freshnessTaskStatus(line) !== null (quote-aware, so callouts and numbered lists count). Every eligible token on the line gets its own mark. A token is skipped when freshnessMarkPosInCode(tree, absFrom + 1) is true (inline code or a code block). Plain bullets (- #task โ€ฆ), paragraphs, and headings are left alone: on a non-task line, the raw pill is the honest signal that Tasks ignores the line.
  • Rendered views: an a.tag whose textContent is exactly #task (and whose href, if present, is #task). Its nearest li ancestor within the post-processor root must carry task-list-item, so a nested plain child bullet under a task gets no mark, while a task nested under a plain bullet does. It must not sit under code/pre or inside a Tasks result row (.plugin-tasks-list-item, .tasks-list-text, or .task-description). With no li ancestor (for example, a detached element), there is no mark.

Surfaces and interaction

  • Live Preview: a Prec.highest ViewPlugin emits one Decoration.replace per eligible token, covering exactly the 5 characters of #task. There is no whitespace folding, so the spaces around the tag stay text. The mark is revealed (no decoration) while any selection range touches the tag inclusively (sel.from <= absTo && sel.to >= absFrom). As a result, the cursor never rests against a mark: typing #task stays raw until you type the following space. The ledger snippet #task [created::โ€ฆ] (cursor between the two spaces) shows the glyph immediately. Mousedown places the cursor at the tag start and focuses the editor; it never writes. Widget equality uses a constant key, so marks never flicker. Rebuild on doc, viewport, or selection changes, Live Preview or file switches, and the new taskTagMarksRefresh effect. A cheap line.text.indexOf("#task") prefilter runs before the task-line check. There are no marks in source mode.
  • Rendered views: a markdown post-processor (sort order 50) annotates eligible a.tag elements in place: it adds the class plus aria-label and data-tooltip-position, and never creates or removes nodes. It is idempotent, and attributes survive Dataview's innerHTML round trip. It covers reading view, embeds, hover previews, canvas cards, and Dataview task lists.
  • Coexistence: ranges are disjoint from the freshness, priority, and date marks. The date mark's whitespace fold may start exactly at the tag's end (adjacent is fine, never overlapping). Vector TT16 asserts this.

Tasks query results

Tasks query results use CSS only, with no JS, and are keyed on the selector body.bob-task-tag-marks .plugin-tasks-list-item .task-description a.tag:is([data-tag-name="#task"], [href="#task"]). Tasks 8.4.0 sets data-tag-name on description tags in addInternalClasses. hide tags and re-renders keep working because no JS touches Tasks' DOM. The JS post-processor never annotates these rows (TR6). The body class makes the toggle instant.

Every row is a #task task by the global filter, so the tag carries no information there and is hidden with display: none. The rule sits after, and with higher specificity than, the shared host rules. The description's leading space remains, giving a uniform gap on every row (accepted).

Reference reading tasks

A task line whose exact #task tag is immediately followed by one whitespace run and #ref (any case, so #REF and #Ref pair) is a reference reading task: the pair renders as one open-book glyph in the slot, in the same teal identity ink the hash uses. Shape says the kind; ink still says "tracked task".

before: - [ ] #task #ref Harness Engineering
after:  - [ ] ๐Ÿ“– Harness Engineering               (๐Ÿ“– โ‰ˆ the teal open-book glyph)

One identity slot: #ref anywhere else (prose, headings, code, non-task lines, #ref #task order, #task #references) never renders the book. #task #references keeps the hash on #task; #ref #task keeps the hash on #task; a lone #ref gets no mark.

The glyph is a 16ร—16 stroked open-book mask in the hash's stroke language (stroke-width 1.8, round caps and joins), defined once as --bob-ref-task-glyph beside --bob-task-tag-glyph. Closed reading tasks rest at the same 40 % mix toward --text-faint as closed hashes. ๐Ÿ”– already means "jump to this highlight" on annotation follow-ups, which share the same ## Tasks lists, so the book keeps one symbol for reading tasks. One symbol, one meaning.

Behavior matches the hash: the cursor or a click reveals both raw tags (one touch anywhere on the pair exposes both, since the decoration covers the whole span); the session toggle, keyboard access, and PDF export behave as for #task. Hovering the book shows #task #ref ยท reference reading task; its accessible label is "Reference reading task".

Reading view turns the #task anchor into the book and hides the adjacent #ref anchor in place (it stays in the DOM for copy and Dataview's innerHTML round trip). In Tasks query results, where #task already hides, the #ref pill renders as the book (CSS-only, like the #task rule).

Pickers never show the pair: display text drops the #ref that follows #task (as it drops #task) and reads ๐Ÿ“– Harness Engineering โ€” in the task pickers, the Task Card header, and inbox-route subtitles. The stored line keeps both tags, so Tasks, Dataview, bob query, and grep keep working.

GTD checklist tasks

The canonical run #task #gtd #pre is one teal sunrise in the task identity slot. #task #gtd #post is one teal flag. The partners may appear in either order after #task; the single glyph replaces the whole run, so its shape names the checklist lane while the shared teal ink still means "tracked task."

before: - [?] #task #gtd #pre Brush teeth
after:  - [?] โ˜€ Brush teeth                 (โ˜€ sunrise: PRE opens the review)
        - [?] โš‘ Morning review: chores done  (โš‘ flag: POST closes the review)

The two 16ร—16 stroked masks use the hash/book geometry: no fill, stroke-width: 1.8, round caps and joins. They are defined beside --bob-ref-task-glyph as --bob-pre-task-glyph and --bob-post-task-glyph. The sunrise path is M1.4 12.2h13.2M3.8 12.2a4.2 4.2 0 0 1 8.4 0M8 3.9v2.4M2.13 6.33l1.7 1.7M13.87 6.33l-1.7 1.7; the flag path is M3.6 14.2V2.3M3.6 2.9h8.6l-2.1 3.2 2.1 3.2H3.6.

Both variants use the same theme-aware teal identity ink, hover capsule, and sizing as the hash and book. The glyph does not encode status; closed tasks (x, X, -) rest at the shared 40% mix toward --text-faint with 0.75 opacity.

Kind Variant class Accessible label Tooltip
PRE bob-pre-task-mark PRE opening checklist task #task #gtd #pre ยท PRE opening checklist / Opens the ]s walk ยท Ctrl+Enter when done
POST bob-post-task-mark POST closing checklist task #task #gtd #post ยท POST closing checklist / Closes the ]s walk (]S) ยท Ctrl+Enter when done

Eligibility is deliberately narrow. On a real task line, the exact #task token must be followed in the identity slot by one whitespace run, the exact whole-token tags #gtd and #pre or #post in either order, and one whitespace run between them. Matching is case-insensitive for partner tags. A line containing both #pre and #post anywhere keeps its pills as a conflict signal. The existing #task #ref pair takes precedence. Tags elsewhere in the title, a missing partner, #prep, #gtd/pre, a non-task line, or code keep the native pills.

Live Preview replaces the full run with one stable widget; touching any part of it reveals all three raw tags for editing. Reading view annotates the #task anchor and hides both partner anchors in place, preserving copy, re-scans, and the toggle. Tasks query rows use CSS only: the sunrise or flag appears when the description has the matching #gtd pair, and the redundant #gtd pill is hidden. Rows with both #pre and #post remain native.

CSS cannot enforce that the partner tags occupy the identity slot, because sibling combinators ignore text nodes. The Tasks-row rule may therefore draw on a misplaced but otherwise matching pair; the vault's rows are canonical. No JavaScript touches Tasks' DOM.

The Today ๐Ÿ… follows the complete checklist identity run, so it stays between the glyph and the title. The existing task-tag-marks toggle restores the three source pills everywhere. Pickers, notices, the review footer, CLI/TUI, bob query, and Bob Mac Capture are unchanged.

Toggle and lifecycle

The command "Toggle task tag marks" (id toggle-task-tag-marks) is session-only and on by default. It flips this.taskTagMarksEnabled and body.bob-task-tag-marks, dispatches the refresh effect to every markdown editor, and shows the Notice Task tag marks on / Task tag marks off. Toggling off also strips the class, aria-label, and data-tooltip-position from every annotated a.tag.bob-task-tag-mark in the document, so pills look and behave natively at once. Toggling on re-annotates every eligible a.tag in document.body using the same eligibility function, so rendered views update without a re-render. Because Tasks results are CSS-only, the body class handles them instantly. onunload removes the body class and strips the annotations. When off, JS creates no marks.

Rejected alternatives

  • Hide #task entirely everywhere: this erases the tracked-versus-plain distinction for about 17,800 plain checkboxes and leaves invisible text under the cursor.
  • Change the global filter or storage: this rewrites about 3,400 lines and breaks the bob-cli parsers, capture, Tasks, and the plugins that match #task.
  • Tasks removeGlobalFilter: true: this is a vault-wide Tasks setting with edit-modal side effects, and it is not toggleable or owned here.
  • A CSS-only .cm-active line reveal: every j/k step would reveal the tag on the new line, shifting every title by about 1.5em.
  • A dot or bullet: it reads as a nested bullet. A check icon is redundant with the checkbox and clashes with done. A filled tag silhouette turns into a blob at 0.8em. Lucide list-todo is too busy.
  • Ink: faint grey (the 1.35.0 ink) is lost beside the grey priority and date marks and near-invisible on closed lines; status hues and blue would claim a state or the Ready lane; orange means attention and repair; pink and orchid turn muddy on the Blocked tint and read as a near-miss beside violet pills; the theme tag accent makes the mark read as one more tag pill beside #prj.
  • Replacing a.tag with a new span in rendered views: this loses native tag search, copy text, and robustness to re-renders.
  • A glyph in Tasks results: it would be a column of identical glyphs that carries zero information.
  • An api namespace with no consumer: rejected. Pickers, the Task Card, and notices already strip #task.

Non-goals: storage, Rust CLI and TUI, bob query, Bob Mac Capture, the Obsidian search and backlinks panes, the tag pane, source mode, Mod+click tag search from Live Preview, and repair flags (tags have no malformed canonical form).

Conformance vectors

Live Preview core: taskTagMarkRanges(lineText) returns [{ from, to, kind }] in UTF-16 line offsets, where kind is task | ref | pre | post. A ref range spans #task #ref; pre and post ranges span the whole three-tag checklist identity run.

# Input Expected
TT1 - [ ] #task Buy milk [6,11)
TT2 - [x] #task Report [completion:: 2026-10-05] [6,11) (resting tone is CSS)
TT3 - [?] #task #gtd #context/home Check weather [6,11) only; other tags untouched
TT4 > - [ ] #task Quoted [8,13) (quote-aware)
TT5 1. [ ] #task Numbered [7,12)
TT6 - [x] (1645-1710) #task #sase Research [18,23) (mid-line tag)
TT7 - [ ] Pack charger none (plain checklist)
TT8 - #task Launch epic! none (not a task line)
TT9 - [ ] #tasks a / - [ ] #task/sub a / - [ ] #Task a none
TT10 - [ ] Note `the #task tag` here none in Live Preview: the core returns [16,21), and the inline-code check drops it
TT11 - [ ] foo#task none
TT12 - [ ] #task! Ship it [6,11)
TT13 - [ ] #task [created::2026-10-07], cursor 12 mark [6,11) shown (cursor is not touching)
TT14 - [ ] #task Buy milk, cursor 5 / 6 / 11 / selection 0โ€“20 shown / revealed / revealed / revealed
TT15 - [ ] #task A #task B [6,11) and [14,19)
TT16 - [ ] #task A [priority:: high] [created:: 2026-10-07] [6,11), disjoint from the priority and date mark ranges
TT17 Paragraph #task text none
TT18 - [ ] #task [6,11) (the tag ends the line)
TT19 - [ ] (#task) parens none (conservative boundary)
TT20 - [ ] #task #ref Harness Engineering [{[6,16), ref}] (one range covering both tokens)
TT21 - [x] #task #ref Done book [completion:: 2026-10-05] [{[6,16), ref}] (resting tone is CSS)
TT22 - [ ] #task #REF Loud [{[6,16), ref}] (case-insensitive partner)
TT23 - [ ] #task #ref Extra spaces [{[6,18), ref}] (extra whitespace still pairs)
TT24 - [ ] #task #references Not a ref [{[6,11), task}] (hash only)
TT25 - [ ] #ref #task Reversed [{[11,16), task}] (hash only on #task)
TT26 - [ ] #ref Alone none (#ref alone)
TT27 - #task #ref Not a task none (not a task line)
TT28 - [ ] Note `the #task #ref pair` here none in Live Preview: the core returns [{[16,26), ref}], and the inline-code check drops it

PRE/POST checklist marks:

# Input Expected
CK1 - [ ] #task #gtd #pre Brush teeth [{[6,21), pre}]
CK2 - [?] #task #gtd #post Morning review [{[6,22), post}]
CK3 - [x] #task #gtd #pre Check weather [completion:: 2026-10-21] [{[6,21), pre}] (resting tone is CSS)
CK4 - [ ] #task #pre #gtd Swapped [{[6,21), pre}]
CK5 - [ ] #task #GTD #Pre Loud [{[6,21), pre}]
CK6 - [ ] #task #gtd #post Extra spaces [{[6,25), post}]
CK7 - [ ] #task #gtd #pre #post Conflict / - [ ] #task #gtd #pre Brush #post [{[6,11), task}]; pills stay
CK8 - [ ] #task #gtd Topic only / - [ ] #task #pre Missing gtd [{[6,11), task}]
CK9 - [ ] #task #gtd #prep Typo / - [ ] #task #gtd/pre Nested [{[6,11), task}]
CK10 - [ ] #task Brush teeth #gtd #pre [{[6,11), task}] (not in the slot)
CK11 - [ ] #task #ref #gtd #pre Odd [{[6,16), ref}] (the ref pair wins)
CK12 - #task #gtd #pre Not a task / - [ ] Note `#task #gtd #pre` none / none in Live Preview (code)
CK13 - [ ] #task #gtd #pre Brush teeth ^teeth, Today-linked ๐Ÿ… widget at offset 22
CK14 checklistKindForText on #gtd #pre / #GTD #Post / #gtd #pre #post / #gtd/pre / #pre, #gtd pre / post / pre / null / pre

Rendered views (fake DOM in tests):

# DOM Expected
TR1 li.task-list-item > a.tag[href="#task"] with text #task annotated (class, tooltip aria-label, data-tooltip-position="top")
TR2 li.task-list-item > p > a.tag (loose list) annotated
TR3 task li > ul > plain li > a.tag #task not annotated
TR4 plain li > ul > li.task-list-item > a.tag #task annotated
TR5 a.tag text #Task / #tasks not annotated
TR6 a.tag inside .plugin-tasks-list-item .task-description not annotated (Tasks rows are CSS-only)
TR7 detached root, no li ancestor not annotated
TR8 a second pass over TR1 no change (idempotent)
TR9 marks off / toggle off / toggle on nothing annotated / annotations stripped document-wide / eligible tags re-annotated
TR10 task li with #task + adjacent #ref anchors #task annotated with the book (reference label and tooltip); #ref hidden in place
TR11 #task + #references anchors, or #ref before #task hash only on #task
TR12 #ref anchor inside .plugin-tasks-list-item .task-description not annotated (Tasks rows are CSS-only: the #ref pill renders the book)
TR13 pair present / toggle off / toggle on book plus hidden #ref / book stripped and #ref revealed document-wide / pair re-annotated
CR1 task li with adjacent #task #gtd #pre anchors #task has sunrise class, label, and tooltip; #gtd and #pre stay hidden in place
CR2 #task #gtd #pre #post, or #task + #gtd + a word + #pre hash only; every pill stays visible
CR3 checklist pair present / toggle off / toggle on variants, labels, and hidden classes stripped document-wide / then re-annotated
CR4 Today reading view on a CR1 row ๐Ÿ… inserted after the hidden #pre anchor and before the title
CR5 Tasks row with #gtd + #post / with #pre + #post flag drawn and #gtd hidden / every pill stays native

Live verification (Bryan, in Obsidian)

  • The teal ink reads clearly yet stays secondary to the title in light and dark themes.
  • The ink stays legible on Blocked (red tint), Next (green tint), and In Progress (yellow tint) lines, and sits well beside a #prj pill.
  • The hover capsule appears in both Live Preview and reading view.
  • Plain checkboxes show no glyph.
  • The cursor or a click reveals #task, and typing #task stays raw until the space.
  • The ledger ta task snippet shows the glyph at once.
  • Vim j/k through a task list does not jitter unless the column is inside the tag.
  • Reading view, embeds, and hover previews show the glyph, and clicking it opens the #task tag search.
  • dash.md Tasks results show no tag.
  • The toggle restores pills everywhere and back again.
  • Closed tasks show a faint ghost of the hue (reading view and Live Preview).
  • The glyph sits cleanly with the priority, date, and fresh marks on one line.
  • Mobile (iOS) renders it.
  • PDF export shows the glyph.
  • On red Blocked [?] rows, sunrise and flag are distinct in light and dark themes and on iOS.
  • ]s keeps the glyph on the landing row, and ๐Ÿ… follows it on Today-linked PRE rows.
  • Touching the checklist glyph reveals all three tags; typing stays raw.
  • Tasks query rows show the matching CSS glyph, while a hand-typed #pre #post conflict keeps all its pills.
Tested rolling snapshot bob-cli 41afa14 bob-mac-capture 556bb67 bob-plugins af1e691 Built 2026-10-24T22:34:13Z (zensical) build.json