omni-dev β€” Atlassian, PRs, commits, the browser bridge, and more in seven tabs

A tutorial covering seven omni-dev workflows. Switch tabs to navigate.

JIRA β€” read, write, and edit issues as local markdown

omni-dev exposes JIRA issues through the JFM (JIRA-Flavoured Markdown) format β€” YAML frontmatter plus a markdown body that round-trips losslessly to and from Atlassian Document Format (ADF). You edit issues like normal markdown files; omni-dev handles the ADF translation on push.

The basic loop

1Authenticate once per Atlassian instance.
export ATLASSIAN_INSTANCE_URL="https://myorg.atlassian.net"
export ATLASSIAN_EMAIL="me@example.com"
export ATLASSIAN_API_TOKEN="atlassian_api_token_here"

omni-dev atlassian auth status
2Read an issue into a local .md file.
omni-dev atlassian jira read PROJ-1234 > PROJ-1234.md
3Edit the file in your editor of choice.
4Write it back. omni-dev validates the ADF locally before the network call, so schema violations abort with a clear diagnosis instead of a vague HTTP 500.
omni-dev atlassian jira write PROJ-1234.md

For a one-shot fetch/edit/push cycle in $EDITOR, use the edit subcommand:

omni-dev atlassian jira edit PROJ-1234

A realistically messy JFM issue body

The example below exercises most of the unusual ADF nodes JFM supports β€” panels, expanders, nested expanders, layout sections with columns, directive tables with merged cells and nested expands inside table cells, decision and task lists, status pills, mentions, dates, emoji, inline cards, annotations, subscript/superscript, colored spans, and the adf-unsupported escape hatch.

markdown β€” PROJ-1234.md
---
type: jira
instance: https://myorg.atlassian.net
key: PROJ-1234
summary: "Add diff/patch coverage analysis for PR comments"
status: In Progress
issue_type: Story
assignee: Alice Smith
priority: High
labels:
  - coverage
  - ci
  - tooling
---

# Overview

The PR coverage comment reports whole-project coverage on every PR,
which hides whether the *changed* lines are tested. This issue tracks
adding patch-coverage attribution β€” see :mention[Alice]{id=712020:abc} for the design.

Status today: :status[Implementation]{color=blue}, target ship
:date[2026-06-30]{timestamp=1782777600000}.

:::panel{type=info}
**Context.** This is the third attempt at patch attribution. Read the
:card[https://myorg.atlassian.net/browse/PROJ-1100]{localId=card-1} retrospective before
proposing new architecture changes.
:::

:::panel{type=warning}
**Heads up.** The 12k-line lcov fixture under
`tests/fixtures/coverage/big_project.lcov` is licensed CC0 but is
2 MB β€” do **not** add more like it without a justification.
:::

## Layout: requirements + non-goals side by side

::::layout
:::column{width=66.66}
### Requirements

- First :status[Partial]{color=yellow} patch-coverage number within
  :span[300 ms]{color=#cf222e} of report parse.
- Uncovered-new-line list exact against the diff, to within
  ≤ 1 line on the reference report set.
- Memory ceiling: 512 MB resident on the i7-1185G7 reference
  laptop.
:::
:::column{width=33.33}
### Non-goals

- Branch coverage (line coverage only; deferred to PROJ-1500).
- Mutation testing.
- Report formats other than lcov / llvm-cov JSON / Cobertura at v1.
:::
::::

## Decisions

:::decisions
- <> Use line coverage only; ignore branch data (PR #952).
- <> Default `DiffScope` to `DiffOnly` so run-to-run variance on
  untouched files does not surface as phantom deltas (PR #973).
- <> Auto-detect the report format from file contents.
:::

## Open tasks

- [x] Pin the lcov grammar to the documented subset.
- [x] Verify the metric shape against :card[https://github.com/rust-works/omni-dev/issues/949]{localId=task-card}.
- [ ] Land the `CoberturaReport` parser β€” see expander below.
- [ ] Snapshot the markdown-renderer output.

## Implementation detail (collapsed)

:::expand{title="Patch-attribution pseudocode (click to expand)" localId=exp-pseudo}
The analyzer intersects the diff's added lines with the report's
covered lines and emits a `PatchCoverage { covered, total }` summary.

```rust
state:
  added:      BTreeSet<(PathBuf, u32)>   // new lines from the diff
  covered:    BTreeSet<(PathBuf, u32)>   // hit lines from the report
  uncovered:  Vec<(PathBuf, u32)>        // added ∧ ¬covered
  patch_pct:  f64
```

:::nested-expand{title="Why a BTreeSet?"}
Deterministic ordering so the uncovered-line list and the rendered
snapshots stay stable across runs β€” set iteration order can otherwise
churn the comment on every CI run.
:::
:::

## Risks (directive table β€” exercises colspan, borders, and nested-expand inside cells)

::::table{layout=default}
:::tr
:::th
Risk
:::
:::th
Likelihood
:::
:::th
Mitigation
:::
:::
:::tr
:::td{border-color=#091e42 border-size=2}
**Phantom per-file deltas** from run-to-run coverage variance on files
the PR never touched.
:::
:::td
:status[High]{color=red}
:::
:::td
:::nested-expand{title="DiffScope=DiffOnly strategy"}
Default the project-delta and indirect-change sections to files the
diff touches. Surface real cross-file effects only via a
magnitude-gated note (≥ 10 net covered lines). `--all-files`
restores the noisier report.
:::
:::
:::
:::tr
:::td
Baseline lcov missing at the merge-base (cold start / expired artifact).
:::
:::td
:status[Medium]{color=yellow}
:::
:::td
Recompute coverage at the merge-base in a worktree, rewriting absolute
`SF` paths to the workspace prefix so both reports strip one prefix.
:::
:::
:::tr
:::td{colspan=3}
**Cross-cutting:** huge reports blow the parse budget β€” see
[the design](https://example.com/design){annotation-id=anno-7 annotation-type=inlineComment}.
:::
:::
::::

## Inline rich content

H2O is water; E = mc2. The candidate
:span[attribution algorithm]{bg=#fff8c5} is named after :emoji{shortName=:dolphin: text="🐬"} β€”
no idea why.

This paragraph contains :placeholder[fill in parse time once
measured] and a :card[https://github.com/rust-works/omni-dev/issues/952]{localId=card-952}.

[This sentence carries an inline comment thread.]{annotation-id=anno-3 annotation-type=inlineComment}

## Unsupported-node escape hatch

ADF nodes JFM doesn't yet model round-trip through a fenced block:

```adf-unsupported
{"type":"customAtlassianNode","attrs":{"key":"value"}}
```

omni-dev preserves these byte-for-byte across read/write.

What the same content looks like rendered (approximate)

PROJ-1234 β€” preview

Add diff/patch coverage analysis for PR comments

Implementation   target ship 2026-06-30

ℹ️ Context
This is the third attempt at patch attribution. Read the PROJ-1100 retrospective before proposing new architecture changes.
⚠️ Heads up
The 12k-line lcov fixture under tests/fixtures/coverage/big_project.lcov is licensed CC0 but is 2 MB β€” do not add more like it without a justification.
Requirements
  • First Partial patch-coverage number within 300 ms of report parse.
  • Uncovered-new-line list exact against the diff (≀ 1 line).
  • Memory ceiling: 512 MB resident.
Non-goals
  • Branch coverage (deferred to PROJ-1500).
  • Mutation testing.
  • Formats beyond lcov / llvm-cov / Cobertura at v1.
Decisions
  • Use line coverage only; ignore branch data (PR #952).
  • Default DiffScope to DiffOnly so variance on untouched files isn't a phantom delta (PR #973).
  • Auto-detect the report format from file contents.
Open tasks
  • Pin the lcov grammar to the documented subset.
  • Verify the metric shape against #949.
  • Land the CoberturaReport parser.
  • Snapshot the markdown-renderer output.
Patch-attribution pseudocode (click to expand)

The analyzer intersects the diff's added lines with the report's covered lines and emits a PatchCoverage { covered, total } summary.

state:
  added:      BTreeSet<(PathBuf, u32)>
  covered:    BTreeSet<(PathBuf, u32)>
  uncovered:  Vec<(PathBuf, u32)>
  patch_pct:  f64
Why a BTreeSet? Deterministic ordering so the uncovered-line list and the rendered snapshots stay stable across runs β€” set iteration order would otherwise churn the comment on every CI run.
RiskLikelihoodMitigation
Phantom per-file deltas from run-to-run coverage variance on untouched files. High
DiffScope=DiffOnly strategy Default the project-delta and indirect-change sections to files the diff touches. Surface real cross-file effects only via a magnitude-gated note (β‰₯ 10 net covered lines). --all-files restores the noisier report.
Baseline lcov missing at the merge-base (cold start / expired artifact). Medium Recompute coverage at the merge-base in a worktree, rewriting absolute SF paths to the workspace prefix.
Cross-cutting: huge reports blow the parse budget β€” see the design.

H2O is water; E = mc2. The candidate attribution algorithm is named after 🐬 β€” no idea why.

This paragraph contains [fill in parse time once measured] and a #952.

This sentence carries an inline comment thread.

❌ Schema-trap
:::expand nested directly inside :::panel parses fine in JFM but the Atlassian API will reject the ADF. Invert the nesting (panel inside expand) or use :::nested-expand inside table cells. omni-dev runs the validator before pushing, so you'll see this locally β€” but knowing the rule up-front saves a round-trip.

Other JIRA subcommands worth knowing

SubcommandWhat it does
searchRun a JQL query, get hits as YAML.
createCreate an issue from a JFM file with no key in frontmatter.
transitionList or execute workflow transitions (e.g. In Progress β†’ Done).
commentAdd, edit, list comments on an issue.
devShow linked PRs, branches, and repositories for an issue.
sprintList/create/update sprints, add issues to a sprint.
linkManage issue links (blocks, relates-to, duplicates, …).

Confluence β€” pages as round-trippable markdown

Confluence pages use the same JFM scheme as JIRA issues: YAML frontmatter discriminated by type: confluence, plus a markdown body that converts to ADF. The same set of unusual ADF nodes is available, but Confluence pages tend to lean harder on layout sections, media with captions, inline annotations (Confluence's inline-comment threads), and block/embed cards.

The basic loop

omni-dev atlassian confluence read 12345 > page.md      # pull
$EDITOR page.md                                          # edit
omni-dev atlassian confluence write page.md              # push

page_id in the frontmatter does the routing. Omit it and use create instead to create a new page:

omni-dev atlassian confluence create page.md

A complex Confluence page exercising unusual nodes

This page exercises multi-column layouts, captioned media, inline media, decision lists, task lists, nested expanders, directive tables with bordered cells and per-cell localIds, inline-comment annotations, embed cards, block cards, emoji, status pills, dates, mentions, indented blocks, text-colour and background-colour marks, subscript/superscript, and the adf-unsupported fenced block.

markdown β€” page.md
---
type: confluence
instance: https://myorg.atlassian.net
page_id: "98765432"
title: "Coverage Diff β€” Architecture Brief"
space_key: ENG
status: current
version: 14
parent_id: "12345"
---

# Coverage Diff β€” Architecture Brief
{align=center}

::card[https://github.com/rust-works/omni-dev/issues/949]
::embed[https://www.youtube.com/watch?v=dQw4w9WgXcQ]{width=480}

:::panel{type=info}
**Owner:** :mention[Alice Smith]{id=712020:abc accessLevel=CONTAINER} ·
**Reviewers:** :mention[Bob Lee]{id=712020:def}, :mention[Carla Ng]{id=712020:ghi} ·
**Last reviewed:** :date[2026-05-20]{timestamp=1779574400000}
:::

## Executive summary
{indent=1}

We propose a three-stage coverage-diff pipeline: parse →
diff → attribute. Stage 1 is shipped; stage 2 lands in
:status[Q2]{color=purple}; stage 3 begins :status[Q3]{color=neutral}.

[The patch-coverage figure is the load-bearing number.]{annotation-id=ann-budget annotation-type=inlineComment}

## Three-column layout: stages of the pipeline

::::layout
:::column{width=33.33}
### 1. Parse
:emoji{shortName=:inbox_tray: text="πŸ“₯"}

- lcov / llvm-cov JSON / Cobertura
- line hits per file
- format auto-detect (see #949)
:::
:::column{width=33.33}
### 2. Diff
:emoji{shortName=:twisted_rightwards_arrows: text="πŸ”€"}

- `git diff` base → head
- added / changed lines only
- rename-aware path mapping
:::
:::column{width=33.34}
### 3. Attribute
:emoji{shortName=:bar_chart: text="πŸ“Š"}

- patch coverage %
- per-file project deltas
- indirect coverage flips
:::
::::

## Captioned diagram

![Pipeline overview](){type=file id=acab1234-5678-90ab-cdef-1122334455 collection=contentId-98765432 width=720 height=320}
:::caption{localId=cap-diagram-1}
Stage boundaries map 1:1 to the parse / diff / attribute modules. The
dashed arrows show the baseline path; solid arrows show the head path.
:::

The inline-media variant
:media-inline[]{type=file id=11112222-3333-4444-5555-666677778888 collection=contentId-98765432}
appears mid-sentence β€” handy for screenshots referenced inline.

## Decision log

:::decisions
- <> **Line coverage only; branch data ignored** (PR #952). Drives all
  downstream decisions on this page.
- <> **DiffScope = DiffOnly by default.** Re-evaluate when cross-file
  effects need surfacing past the magnitude gate.
- <> **No mutation testing in v1.** Deferred to PROJ-1500.
:::

## Open work

- [x] Land the lcov parser (#949).
- [x] Land the llvm-cov JSON parser (#952).
- [ ] Land the Cobertura parser (this page, #964).
- [ ] Wire the markdown renderer into the PR comment (#948).
- [ ] Make the coverage tests deterministic (#966).

## Two-column: configuration knobs vs. defaults

::::layout
:::column{width=50}
### Knobs
| Knob | Type |
| --- | --- |
| `base_ref` | String |
| `head_ref` | String |
| `diff_scope` | enum |
| `format` | enum |
| `gate_pct` | f64 |
:::
:::column{width=50}
### Defaults
| Knob | Default |
| --- | --- |
| `base_ref` | merge-base |
| `head_ref` | HEAD |
| `diff_scope` | `DiffOnly` |
| `format` | `markdown` |
| `gate_pct` | (none) |
:::
::::

## Runtime budget table (directive table β€” captures hard breaks, colspan, nested-expand)

::::table{layout=wide isNumberColumnEnabled=true}
:::tr
:::th
Stage
:::
:::th
Target p50
:::
:::th
Target p95
:::
:::th
Measured (this repo)
:::
:::
:::tr
:::td
Parse report (12k lines)
:::
:::td
< 50 ms
:::
:::td
< 120 ms
:::
:::td
:status[31 ms]{color=green}\
:status[98 ms]{color=green}
:::
:::
:::tr
:::td
Compute diff
:::
:::td
< 20 ms
:::
:::td
< 80 ms
:::
:::td
:status[12 ms]{color=green}\
:status[54 ms]{color=green}
:::
:::
:::tr
:::td{border-color=#cf222e border-size=2}
**Full PR comment** :emoji{shortName=:warning: text="⚠️"}
:::
:::td
< 300 ms
:::
:::td
< 600 ms
:::
:::td
:status[412 ms]{color=red}\
:status[890 ms]{color=red}
:::
:::
:::tr
:::td{colspan=4}
:::nested-expand{title="Why is the full comment over budget?"}
The merge-base baseline recompute dominates when the artifact is
missing. Options:

1. Cache the baseline lcov as a CI artifact keyed on the merge-base.
2. Skip indirect-change detection when no baseline is available.
3. Run parse and diff concurrently.

Owner: :mention[Alice Smith]{id=712020:abc}. Due
:date[2026-06-01]{timestamp=1780617600000}.
:::
:::
:::
::::

## Expander with nested expander

:::expand{title="API surface (click to expand)" localId=exp-api}
The report trait is the only thing downstream consumers need to
import.

```rust
pub trait CoverageReport {
    fn line_hits(
        &self,
        file: &Path,
    ) -> Option<&LineHits>;
    fn files(&self) -> Box<dyn Iterator<Item = &Path> + '_>;
}
```

:::nested-expand{title="CoverageReport variants"}
- `Lcov` β€” parsed from `.lcov` / `.info` (`SF`/`DA` records).
- `LlvmCovJson` β€” llvm-cov `export` JSON.
- `Cobertura` β€” Cobertura XML (line coverage; branch data ignored).
:::
:::

## Highlights and inline marks

A :span[bright callout]{color=#cf222e bg=#fff8c5} marks the
load-bearing claim. We mix in :span[teal text]{color=#1a7f37} and
:span[a yellow highlight]{bg=#fff8c5} to signal severity.

Footnote-style :span[fn-1]{sup} sits superscript; chemical
H:span[2]{sub}O sits subscript. The
[entire sentence is annotated for inline comment thread]{annotation-id=ann-sentence-1 annotation-type=inlineComment}.

## Embedded BigQuery extension (unsupported — round-trips verbatim)

```adf-unsupported
{"type":"bodiedExtension","attrs":{"extensionKey":"bq-query","parameters":{"sql":"SELECT 1"}},"content":[{"type":"paragraph","content":[{"type":"text","text":"placeholder"}]}]}
```

## Manual hard breaks

Line one\
line two (same paragraph as line one β€” that's a hardBreak)\
line three.

---

© ENG space. Edits via `omni-dev atlassian confluence write page.md`.

Approximate rendered preview

page 98765432 β€” preview

Coverage Diff β€” Architecture Brief

#949 β€” Coverage-diff issue  YouTube embed

Owner: @Alice Smith Β· Reviewers: @Bob Lee, @Carla Ng Β· Last reviewed: 2026-05-20

We propose a three-stage coverage-diff pipeline: parse β†’ diff β†’ attribute. Stage 1 is shipped; stage 2 lands in Q2; stage 3 begins Q3.

The patch-coverage figure is the load-bearing number.

1. Parse πŸ“₯
  • lcov / llvm-cov / Cobertura
  • line hits per file
  • format auto-detect (#949)
2. Diff πŸ”€
  • git diff base β†’ head
  • added / changed lines
  • rename-aware
3. Attribute πŸ“Š
  • patch coverage %
  • per-file deltas
  • indirect flips
[ pipeline-overview.png ]
Stage boundaries map 1:1 to the parse / diff / attribute modules. Dashed = baseline, solid = head.
Decision log
  • Line coverage only; branch data ignored (PR #952).
  • DiffScope = DiffOnly by default.
  • No mutation testing in v1.
Runtime budget table
#StageTarget p50Target p95Measured (this repo)
1Parse report (12k lines)< 50 ms< 120 ms31 ms
98 ms
2Compute diff< 20 ms< 80 ms12 ms
54 ms
3Full PR comment ⚠️< 300 ms< 600 ms412 ms
890 ms
Why is the full comment over budget?

The merge-base baseline recompute dominates when the artifact is missing. Options:

  1. Cache the baseline lcov as a CI artifact keyed on the merge-base.
  2. Skip indirect-change detection when no baseline is available.
  3. Run parse and diff concurrently.

Owner: @Alice Smith. Due 2026-06-01.

A bright callout marks the load-bearing claim. We mix in teal text and a yellow highlight to signal severity.

Footnote-style fn-1 sits superscript; chemical H2O sits subscript.

πŸ“ Validator-first workflow
omni-dev runs validate_document against the assembled ADF before sending. The most common rejection on Confluence pages is :::expand nested directly inside a table cell β€” use :::nested-expand. The schema source of truth is src/atlassian/adf_schema/mod.rs, transcribed from the upstream @atlaskit/adf-schema package per ADR-0023.

Other confluence subcommands worth knowing

SubcommandWhat it does
searchCQL search across spaces.
childrenList child pages of a page or top-level pages in a space.
historyList version-history metadata for a page.
compareStructural diff between two versions of a page.
downloadRecursively download a page tree as .md files.
labelAdd/remove/list labels on a page.
attachmentList/upload/delete attachments on a page.
moveReparent a page within the same space.

Create PRs β€” AI-generated descriptions, one command

omni-dev git branch create pr opens a pull request on the current branch with an AI-generated title and body derived from the commits and the diff against the base branch.

Typical usage

1Land your commits on a feature branch.
git checkout -b feature/coverage-diff
# ... commits ...
git push -u origin feature/coverage-diff
2Create the PR. omni-dev reads the commits, builds a prompt, and posts via the gh CLI.
omni-dev git branch create pr
3Review. The CLI prints the generated title and body before posting; reject if unhappy.

Useful flags

FlagEffect
--base <branch>Override the auto-detected base branch.
--draftCreate the PR as draft.
--ai-backend claude-cliUse a sandboxed nested claude -p session instead of the default HTTP backend. See ADR-0028.
--claude-cli-max-budget-usd 0.50Cap nested-session spend per invocation.

What feeds the prompt

  • The list of commits between HEAD and the base branch (subject + body).
  • A summarized diff (file paths, lines added/removed, and small-file content).
  • Optional template at .omni-dev/pr-template.md β€” used as a stylistic exemplar.
ℹ️ Auto-detection
Base branch defaults to whatever the local repo's default branch points at (origin/HEAD). Set --base when opening PRs against a non-default branch (release branches, long-running feature branches).

End-to-end example

$ git log --oneline main..HEAD
c5449c9 feat(coverage): land patch-coverage attribution
841187f feat(coverage): lcov + llvm-cov JSON parsers
728ce74 test(coverage): Cobertura fixture

$ omni-dev git branch create pr --draft

  β†’ analysing commits (3)…
  β†’ summarising diff (12 files, +984 βˆ’214)…
  β†’ calling claude (β‰ˆ $0.02)…

PROPOSED TITLE
  feat(coverage): diff/patch coverage analysis β€” parsers + attribution

PROPOSED BODY (excerpt)
  ## Summary
  - Add CoverageReport trait with lcov and llvm-cov JSON parsers
  - Compute patch coverage and the uncovered-new-line list from the diff
  - Render the PR coverage comment via the markdown renderer
  ## Test plan
  - [ ] cargo test --all
  - [ ] cargo insta review
  - [ ] manual smoke against big_project.lcov

Open the PR? [y/N] y

βœ“ #890 opened: https://github.com/rust-works/omni-dev/pull/890
⚠️ Don't push empty branches
omni-dev expects the branch to have commits and be pushed to remote before create pr runs. Worktree workflows in particular often start with an empty branch β€” push at least one commit first or PR creation will fail.

Twiddle commit messages β€” AI-powered amendments

omni-dev git commit message twiddle rewrites recent commit messages to comply with project guidelines while preserving the commits themselves. It pairs with the .omni-dev/commit-guidelines.md rules in your repo.

When to use it

  • After a flurry of work-in-progress commits with sloppy messages.
  • Before opening a PR, to make the log read cleanly.
  • After resolving rebase conflicts, when the merge-of-merge messages need cleaning.

The two-phase model

1View β€” analyse commits and emit a YAML report.
omni-dev git commit message view HEAD~5..HEAD > commits.yaml
2Twiddle β€” let the AI rewrite the messages.
omni-dev git commit message twiddle --range HEAD~5..HEAD

You'll be asked to confirm before any git filter-branch-equivalent rewrite happens.

3Check β€” validate without rewriting (CI-friendly).
omni-dev git commit message check --range HEAD~5..HEAD

Sibling subcommands

SubcommandEffect
viewAnalyse commits, emit a YAML report (with field-presence tracking).
amendRewrite messages from a YAML configuration file (deterministic, no AI).
twiddleAI-driven rewrite. Reads guidelines and applies them.
checkValidate against guidelines without modifying. Exit-codes are CI-friendly.
stagedGenerate a commit message from currently staged changes and commit.

Worked example

$ git log --oneline -5
c5449c9 wip
841187f more fixes
728ce74 fix tests
4d1290d coverage stuff
52ba037 initial
$ omni-dev git commit message twiddle --range HEAD~5..HEAD

  β†’ reading .omni-dev/commit-guidelines.md
  β†’ analysing 5 commits…
  β†’ calling claude…

PROPOSED REWRITES
  c5449c9 wip              β†’  feat(coverage): wire CoverageReport into the diff analyzer
  841187f more fixes       β†’  fix(coverage): ignore branch records when computing line hits
  728ce74 fix tests        β†’  test(coverage): unblock the markdown-renderer snapshot
  4d1290d coverage stuff   β†’  feat(coverage): add patch-attribution skeleton + types
  52ba037 initial          β†’  feat(coverage): add CoverageReport trait + lcov parser

Apply rewrite? [y/N] y
βœ“ rewrote 5 commits. Force-push with care:
  git push --force-with-lease origin issue-949-diff-patch-coverage-analysis
⚠️ Rewrites public history
Twiddle changes commit SHAs. After rewriting commits that are already pushed, you need a force-push (--force-with-lease). Anyone with a local copy of the old branch must reset on top of the rewritten history. Don't twiddle commits on shared long-running branches (e.g. main, release branches).
ℹ️ How twiddle uses commit-guidelines.md
The contents of .omni-dev/commit-guidelines.md are included verbatim in the prompt. The AI is asked to rewrite messages so they conform β€” conventional-commit prefix, scope, imperative mood, body lines under N characters, etc. β€” whilst preserving the original meaning.

Composing with other commands

Twiddle plus PR creation is the canonical pre-PR cleanup:

# clean up history, then open a PR with a fresh AI-generated body
omni-dev git commit message twiddle --range HEAD~5..HEAD \
  && git push --force-with-lease \
  && omni-dev git branch create pr --draft

Browser bridge β€” drive authenticated requests through a logged-in tab

omni-dev browser bridge serve runs a long-lived local process that lets you issue HTTP requests through an authenticated browser tab. When you are poking at internal services β€” Grafana/Loki, SSO-gated dashboards, admin panels β€” the browser already holds sessions (SSO, OAuth, session cookies) that are painful to replicate from a script. The bridge borrows that authority without exfiltrating the cookies or tokens: the fetch() runs in-page, so the credentials never leave the browser.

⚠️ Confused deputy by design
The bridge lends the browser's authority to whoever can talk to it. That makes the security model the load-bearing part of the design, not an add-on β€” both planes are authenticated and default-closed. The rationale lives in ADR-0036; the operator guide is docs/browser-bridge.md.

How it works

Two local planes joined by an id-keyed correlator: an HTTP control plane you drive from the shell, and a WebSocket plane the browser tab connects to.

request flow
   operator CLI / authorized script             browser tab (DevTools console)
              β”‚                                          β”‚
   HTTP + bearer token + X-Omni-Bridge      WebSocket + token subprotocol
              β”‚                                          β”‚
              β–Ό                                          β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ omni-dev browser bridge ────────────────────────┐
  β”‚   HTTP control plane            id-correlator           WebSocket plane  β”‚
  β”‚   127.0.0.1:9998      ──►   pending: id β†’ oneshot   ──►   127.0.0.1:9999 β”‚
  β”‚   (axum)              ◄──   resolve on WS reply     ◄──   (tungstenite)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A request flows: control plane (authenticated) β†’ assign id + register a waiter β†’ serialize a command frame β†’ WebSocket β†’ browser fetch() β†’ response frame β†’ correlator resolves the waiter by id β†’ control plane returns the HTTP response.

The basic loop

1Start the bridge. It prints the bound ports, a generated session token, and a ready-to-paste JS snippet.
omni-dev browser bridge serve
2Paste the snippet into the DevTools console on the authenticated tab (e.g. https://grafana.internal/…). It connects over the WebSocket, presenting the token via the subprotocol, and reconnects with backoff.
3Drive requests from your shell. The token is in the bridge's stdout.
export OMNI_BRIDGE_TOKEN=<token printed by the bridge>
omni-dev browser bridge request --url /loki/api/v1/labels

The bridge works only while the tab is open and the snippet is running.

Two ways to issue a request

The request thin client

Reads the token (from OMNI_BRIDGE_TOKEN or --token-file), adds the required headers, POSTs to a running bridge, and prints the response envelope:

omni-dev browser bridge request --url /loki/api/v1/labels --method GET
omni-dev browser bridge request --url /api/foo --method POST --body @payload.json
omni-dev browser bridge request --url /api/foo --header "Accept: application/json"
omni-dev browser bridge request --stream --url /events | your-consumer   # SSE / chunked

The transparent proxy

Any control-plane path not under /__bridge/ is forwarded verbatim to the browser, resolved against the page's origin. Handy with curl β€” binary bodies are decoded automatically:

T="$OMNI_BRIDGE_TOKEN"
curl -H "Authorization: Bearer $T" -H "X-Omni-Bridge: 1" \
  http://localhost:9998/loki/api/v1/labels
ℹ️ The two mandatory headers
Every control-plane request must carry Authorization: Bearer <token> and X-Omni-Bridge: 1. The custom header forces a CORS preflight that the server refuses to answer β€” which is exactly what blocks simple-request CSRF from a random web page.

Routing to a specific tab

Paste the snippet into several authenticated tabs (e.g. a Grafana tab and an admin tab) and each connects independently. GET /__bridge/status lists them by connection id and Origin; select one with --target (or the X-Omni-Bridge-Target header):

omni-dev browser bridge request --target 2 --url /api/foo                 # by id
omni-dev browser bridge request --target https://admin.internal --url /x  # by origin
SituationResult
No tab connected503
Exactly one tab, no targetRoutes to it (v1 back-compat)
Several tabs, no target409 β€” specify a target (the error lists the tabs)
Target id / origin matches one tabRoutes to it
Target matches none404
Target origin matches several tabs409 β€” target by connection id instead

Security model

The trust boundary, stated once: a request is trusted only if it presents the session token AND is not a cross-origin browser request; everything else is denied. A localhost bind is necessary but not sufficient β€” it stops off-host access, not other local processes or the web pages you visit while the bridge runs.

βœ… Session token β€” mandatory on both planes
Generated at startup and printed with the snippet; never accepted as a CLI argument (argv is world-readable via ps). The control plane requires Authorization: Bearer; the WebSocket upgrade is rejected without the token in its subprotocol. An unauthenticated peer can never connect or evict the browser connection.
ℹ️ Anti-CSRF / anti-DNS-rebinding (all enforced)
Host allowlist β€” only localhost/127.0.0.1/[::1] accepted (blocks DNS rebinding). Reject browser-originated requests β€” any Origin header or Sec-Fetch-Site: cross-site/same-site is denied. Require X-Omni-Bridge: 1 on every request. No CORS allow headers; OPTIONS is never answered.
⚠️ Outbound scope β€” default-closed
The in-page snippet is not trusted to scope requests; the server enforces scope before sending the command. Relative URLs only by default (resolved against the page origin). Absolute / cross-origin URLs are rejected unless explicitly enabled with serve --allow-origin <url>, or per-request with request --allow-origin <url> (logged with a WARN, and still bounded by the browser's own CORS).

Worked example: draining Grafana / Loki logs

The bridge issues the same same-origin requests the Grafana UI makes, carrying the grafana_session cookie; CSRF passes because the fetch() runs in-page. Pagination is client-side β€” walk the window backward until a page comes back empty:

bash β€” drain loop
DS_UID=<loki-datasource-uid>
QUERY='{app="foo"}|="error"'
END=$(date +%s)000000000   # now, in nanoseconds
LIMIT=5000

while :; do
  page=$(omni-dev browser bridge request \
    --url "/api/datasources/proxy/uid/${DS_UID}/loki/api/v1/query_range?query=${QUERY}&end=${END}&limit=${LIMIT}&direction=backward")

  # The upstream JSON is the envelope's `body` string; parse it once.
  body=$(printf '%s' "$page" | jq -r '.body')

  rows=$(printf '%s' "$body" | jq '[.data.result[].values[]] | length')
  [ "$rows" -eq 0 ] && break

  printf '%s\n' "$body"   # collect / process this page

  # Oldest timestamp on this page (ns) becomes the next `end`, minus 1ns.
  oldest=$(printf '%s' "$body" | jq -r '[.data.result[].values[][0] | tonumber] | min')
  END=$((oldest - 1))
done

Each page must still fit under --max-body-bytes. For genuinely streaming endpoints (Grafana Live, SSE, chunked APIs), opt into --stream instead of paginating.

Flags worth knowing

Flag (on serve)DefaultPurpose
--control-port <PORT>9998HTTP control-plane port. 0 binds a random free port.
--ws-port <PORT>9999WebSocket-plane port. 0 binds a random free port.
--allow-origin <URL>β€”Permit this exact cross-origin for the WS upgrade and outbound URLs.
--request-timeout <SECS>30Per-request timeout before the control plane returns 504.
--max-body-bytes <N>8388608Maximum browser response body size accepted.
--max-concurrent <N>64Maximum concurrent in-flight requests.
--token-file <PATH>β€”Read the token from a 0600 file instead of generating one.
πŸ“ Caveats
A strict CSP connect-src can block the WebSocket. CORS constrains the in-page fetch(), not the bridge: same-origin (relative URLs) is unaffected; cross-origin targets depend on their Access-Control-Allow-Origin. The bridge works only while the tab is open and the snippet is running.

YouTube transcript β€” captions as SRT, VTT, TXT, or JSON

omni-dev transcript youtube fetches captions and transcripts from YouTube. The provider is the first positional argument, so the namespace stays clean as more sources (Vimeo, podcast RSS, generic VTT/SRT URLs) are added later. Three subcommands: info and list-langs inspect a video; fetch renders a track.

The three subcommands

1Inspect the video's metadata and available tracks.
omni-dev transcript youtube info jNQXAC9IVRw
omni-dev transcript youtube list-langs jNQXAC9IVRw

Both default to a human table; pass --output json for machine-readable output.

2Fetch a track and render it. Writes to stdout unless -o is given.
omni-dev transcript youtube fetch jNQXAC9IVRw --format srt -o me-at-the-zoo.srt

Locator forms

<url> accepts any of these β€” extra query params are ignored:

https://www.youtube.com/watch?v=jNQXAC9IVRw
https://youtu.be/jNQXAC9IVRw
https://www.youtube.com/shorts/<id>
https://www.youtube.com/embed/<id>
jNQXAC9IVRw                         # bare 11-character video ID

fetch flags

FlagDefaultEffect
--lang <code>enPreferred language. Prefix fallback applies β€” en matches en-US.
--format <fmt>srtOne of srt, vtt, txt, json.
--autooffAllow falling through to auto-generated (ASR) captions when no manual track matches.
--translate <lang>β€”Synthesise a translated track in <lang> when no native track matches.
-o, --outputstdoutWrite the rendered transcript to a file.
ℹ️ Manual vs. auto-generated
list-langs tags each track with a kind: manual (human-authored), auto (ASR), or translated. fetch only uses an auto track when you pass --auto β€” otherwise it fails with AutoCaptionsRequireOptIn rather than silently handing back a lower-quality transcript.

Typed errors, not raw HTTP

Failures surface as named variants, so a script can branch on them:

VariantWhen
InvalidLocatorURL did not parse, or bare ID failed validation.
LanguageNotFoundNo track matched --lang (manual or, with --auto, ASR).
AutoCaptionsRequireOptInOnly ASR matched, but --auto was not passed.
PlayabilityRefusedAge-gated, region-locked, removed, or login-required.
ParseError / HttpResponse did not match the expected shape, or a non-2xx response.

Worked example

terminal
$ omni-dev transcript youtube info jNQXAC9IVRw
Source:    youtube
ID:        jNQXAC9IVRw
Title:     Me at the zoo
Channel:   jawed
Duration:  00:19
Languages:
  - en       [manual] English
  - en       [auto]   English (auto-generated)

$ omni-dev transcript youtube fetch jNQXAC9IVRw --format txt
All right, so here we are in front of the elephants.
The cool thing about these guys is that they have really,
really, really long trunks. And that's cool. And that's
pretty much all there is to say.
πŸ“ Library-first design
The fetching logic lives in src/transcript/ with no clap dependency; the CLI is a thin bridge. Format converters consume &[Cue] and never reach back into a source, so all four output formats work for any future provider. See docs/transcript.md for the recipe to add one.

Claude history β€” export your Claude Code sessions

omni-dev ai claude history sync exports your Claude Code conversation history to a target directory as one .jsonl (and/or rendered .md) per chat. The corpus is a behavioural transcript β€” prompts, responses, thinking, tool calls, and tool-result metadata β€” sized for analyst use cases like work-log generation and behavioural review.

ℹ️ What is and isn't exported
Included: user prompts, assistant responses, thinking, tool calls and tool-result metadata. Deliberately excluded: sub-agent internals, tool-result *.txt sidecars, PDF rasters, and auto-memory.

The basic command

1Sync every session to a target directory. Both file shapes side by side:
omni-dev ai claude history sync --target ~/claude-history \
  --output-format jsonl,markdown

On-disk layout

One folder per project (the encoded working-directory slug), one file per session UUID:

~/claude-history/
  -Users-me-wrk-omni-dev/
    0f1a…2b.jsonl       # append-only event log, byte-identical across runs
    0f1a…2b.md          # rendered, human-readable transcript
  -Users-me-tmp-spike/
    a1b2…d4.jsonl
    a1b2…d4.md

Flags

FlagEffect
--target <PATH>Target directory for the export (required). Created if absent.
--source <PATH>Override the source root. Defaults to ~/.claude/projects.
--project <NAME>Restrict to one project β€” the encoded slug (-Users-me-tmp) or a decoded cwd path.
--since <DUR|DATE>Only sessions at/after this point. Relative (30s, 2h, 7d, 4w) or RFC 3339.
--output-format <LIST>On-disk shapes: jsonl (default), markdown, or both comma-separated.
--exclude-systemHide system-side events from Markdown (JSONL is unaffected).
--pruneDelete targets for sessions no longer in the source (scoped to managed extensions).
--dry-runPreview changes without touching the target.
--format <text|yaml>Shape of the run report printed to stdout.

Idempotent re-runs

Each run reports created / updated / skipped / pruned per file. A session is skipped when its source mtime is unchanged, so re-running is cheap and safe to put on a schedule:

terminal
$ omni-dev ai claude history sync --target ~/hist --output-format jsonl,markdown
created  jsonl     -Users-me-omni-dev/0f1a…2b -> ~/hist/…/0f1a…2b.jsonl (48213 bytes)
created  markdown  -Users-me-omni-dev/0f1a…2b -> ~/hist/…/0f1a…2b.md (21044 bytes)

$ omni-dev ai claude history sync --target ~/hist --output-format jsonl,markdown
skipped  jsonl     -Users-me-omni-dev/0f1a…2b (unchanged) -> ~/hist/…/0f1a…2b.jsonl
skipped  markdown  -Users-me-omni-dev/0f1a…2b (unchanged) -> ~/hist/…/0f1a…2b.md
⚠️ --prune is scoped, not a blunt delete
Only files matching <slug>/<uuid>.<ext> are eligible for deletion, and <ext> is restricted to the formats in --output-format β€” so artefacts the run was not asked to manage are preserved regardless. Pair with --dry-run the first time to preview what would go.
βœ… Versionable by design
The JSONL is append-only and byte-identical across runs (mtime alone decides freshness), so committing ~/claude-history to a git repo produces clean, meaningful diffs β€” a durable, greppable record of how the work actually happened.