JIRA Confluence Create PRs Twiddle commit messages Browser bridge YouTube transcript Claude history

## 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

1 Authenticate 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

2 Read an issue into a local `.md` file.

    omni-dev atlassian jira read PROJ-1234 > PROJ-1234.md

3 Edit the file in your editor of choice.
4 Write 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.

Risk | Likelihood | Mitigation
--- | --- | ---
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

Subcommand | What it does
--- | ---
`search` | Run a JQL query, get hits as YAML.
`create` | Create an issue from a JFM file with no `key` in frontmatter.
`transition` | List or execute workflow transitions (e.g. In Progress → Done).
`comment` | Add, edit, list comments on an issue.
`dev` | Show linked PRs, branches, and repositories for an issue.
`sprint` | List/create/update sprints, add issues to a sprint.
`link` | Manage 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 `localId`s, 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

# | Stage | Target p50 | Target p95 | Measured (this repo)
--- | --- | --- | --- | ---
1 | Parse report (12k lines) | < 50 ms | < 120 ms | 31 ms 98 ms
2 | Compute diff | < 20 ms | < 80 ms | 12 ms 54 ms
3 | Full PR comment ⚠️ | < 300 ms | < 600 ms | 412 ms 890 ms
Why is the full comment over budget? The merge-base baseline recompute dominates when the artifact is missing. Options: Cache the baseline lcov as a CI artifact keyed on the merge-base. Skip indirect-change detection when no baseline is available. 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

Subcommand | What it does
--- | ---
`search` | CQL search across spaces.
`children` | List child pages of a page or top-level pages in a space.
`history` | List version-history metadata for a page.
`compare` | Structural diff between two versions of a page.
`download` | Recursively download a page tree as `.md` files.
`label` | Add/remove/list labels on a page.
`attachment` | List/upload/delete attachments on a page.
`move` | Reparent 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

1 Land your commits on a feature branch.

    git checkout -b feature/coverage-diff
    # ... commits ...
    git push -u origin feature/coverage-diff

2 Create the PR. omni-dev reads the commits, builds a prompt, and posts via the `gh` CLI.

    omni-dev git branch create pr

3 Review. The CLI prints the generated title and body before posting; reject if unhappy.

### Useful flags

Flag | Effect
--- | ---
`--base <branch>` | Override the auto-detected base branch.
`--draft` | Create the PR as draft.
`--ai-backend claude-cli` | Use a sandboxed nested `claude -p` session instead of the default HTTP backend. See ADR-0028.
`--claude-cli-max-budget-usd 0.50` | Cap 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

1 View — analyse commits and emit a YAML report.

    omni-dev git commit message view HEAD~5..HEAD > commits.yaml

2 Twiddle — 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.

3 Check — validate without rewriting (CI-friendly).

    omni-dev git commit message check --range HEAD~5..HEAD

### Sibling subcommands

Subcommand | Effect
--- | ---
`view` | Analyse commits, emit a YAML report (with field-presence tracking).
`amend` | Rewrite messages from a YAML configuration file (deterministic, no AI).
`twiddle` | AI-driven rewrite. Reads guidelines and applies them.
`check` | Validate against guidelines without modifying. Exit-codes are CI-friendly.
`staged` | Generate 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

1 Start the bridge. It prints the bound ports, a generated session token, and a ready-to-paste JS snippet.

    omni-dev browser bridge serve

2 Paste 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.
3 Drive 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

Situation | Result
--- | ---
No tab connected | `503`
Exactly one tab, no target | Routes to it (v1 back-compat)
Several tabs, no target | `409` — specify a target (the error lists the tabs)
Target id / origin matches one tab | Routes to it
Target matches none | `404`
Target origin matches several tabs | `409` — 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`) | Default | Purpose
--- | --- | ---
`--control-port <PORT>` | `9998` | HTTP control-plane port. `0` binds a random free port.
`--ws-port <PORT>` | `9999` | WebSocket-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>` | `30` | Per-request timeout before the control plane returns `504`.
`--max-body-bytes <N>` | `8388608` | Maximum browser response body size accepted.
`--max-concurrent <N>` | `64` | Maximum 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

1 Inspect 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.

2 Fetch 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

Flag | Default | Effect
--- | --- | ---
`--lang <code>` | `en` | Preferred language. Prefix fallback applies — `en` matches `en-US`.
`--format <fmt>` | `srt` | One of `srt`, `vtt`, `txt`, `json`.
`--auto` | off | Allow 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`, `--output` | stdout | Write 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:

Variant | When
--- | ---
`InvalidLocator` | URL did not parse, or bare ID failed validation.
`LanguageNotFound` | No track matched `--lang` (manual or, with `--auto`, ASR).
`AutoCaptionsRequireOptIn` | Only ASR matched, but `--auto` was not passed.
`PlayabilityRefused` | Age-gated, region-locked, removed, or login-required.
`ParseError` / `Http` | Response 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

1 Sync 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

Flag | Effect
--- | ---
`--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-system` | Hide system-side events from Markdown (JSONL is unaffected).
`--prune` | Delete targets for sessions no longer in the source (scoped to managed extensions).
`--dry-run` | Preview 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.