omni-dev 0.46.0

AI-powered git commit rewriter, PR generator, and MCP server for Jira, Confluence, Datadog, Gmail, and Drive.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# Request log

`omni-dev` keeps a local, append-only log of every invocation **and** the HTTP
requests it issues, and ships an `omni-dev log` subcommand to search and
pretty-print it. It is the single, durable, queryable record of *what was run*
and *what it talked to over the network* — the thing `RUST_LOG` tracing (ephemeral,
stderr-only) is not.

## What gets recorded

One JSON object per line (NDJSON). A `kind` field discriminates the two record
types, so the log is a complete invocation history, not just an HTTP history:

- **`kind: "invocation"`** — one per process run (and one per MCP tool call):
  the resolved subcommand path, full argv (with secret-bearing flag values
  redacted), exit code, duration, any top-level error, and a whitelisted
  `OMNI_DEV_*` env snapshot.
- **`kind: "http"`** — one per outbound request (recorded *inside* each client's
  retry loop, so retries and transport failures are captured too): service,
  method, URL (secret-bearing query/fragment parameter **values** redacted —
  see [Redaction posture]#redaction-posture), status, elapsed, and any error.
- **`kind: "gh"`** — one per `gh` CLI subprocess invocation. Every GitHub call
  funnels through `gh` (the token never enters our process), so these are
  subprocess records, not `http` ones. Each carries the semantic subcommand
  (`api graphql`, `pr list`, `repo view`, …) in `command`, the scrubbed argv in
  `command_line`, the `source`, the exit code, and the duration. This is what
  `omni-dev log count --kind gh` aggregates — see [Counting GitHub API calls]#counting-github-api-calls.
- **`kind: "worktree"`** — one per `omni-dev git worktree <verb>` invocation
  (thin logged wrappers over `git worktree add/remove/list/move/prune/repair`,
  #1392). Tagged `service: "worktree"`, with `command` set to
  `["git", "worktree", "<verb>"]` and the recovery-relevant metadata in the
  free-form `context` map — for `remove`: the worktree's `path`, `branch`,
  `commit` (HEAD *before* removal), `had_uncommitted`, and `used_force`; for
  `prune`: a JSON `pruned` list of `{path, branch, commit}`. See
  [Recovering a removed worktree]#recovering-a-removed-worktree.
- **`kind: "drivemutation"`** — one per `omni-dev drive rename`/`drive
  move`/`drive create`/`drive upload`/`drive edit`/`drive sheets *` attempt,
  written from
  *inside* the mutation itself so it covers every current and future caller
  (CLI today, MCP later), not just the CLI. Includes a refused move
  (`Blocked`, ADR-0070) or create/upload/edit (`Blocked` by the folder
  write-permission gate, ADR-0071, issue #1574) — no mutating API call
  happens for those, but the refusal is itself the security-relevant event.
  Tagged `service: "drive"`, with `command` set to `["drive",
  "<operation>"]` and the outcome detail in the free-form `context` map:
  `file_id`, `file_name`, `status`, and — only when present —
  `added_principals`/`removed_principals` (comma-separated,
  `move`-only) and `crosses_drive_boundary` (`move`-only), plus
  `resolved_folder_id`/`decided_by_folder_id`/`decided_by_depth`
  (the folder the write-permission gate evaluated against, and which
  configured rule, if any, decided the verdict — absent for the ungated
  `rename`/`move`), and `decided_by_file_id` when a **file** rule decided
  it (issue [#1612]https://github.com/rust-works/omni-dev/issues/1612).
  `decided_by_file_id` is mutually exclusive with `decided_by_folder_id`,
  deliberately: a file id never appears in the folder field, so an existing
  `--query decided_by_folder_id:<id>` cannot start matching a different
  kind of id. A file rule matches the target itself, so it walks no chain —
  `decided_by_depth` and `resolved_folder_id` are both absent alongside it.
  See [docs/drive.md]drive.md#write-permissions.

  Cell writes through the Sheets API (issue
  [#1589]https://github.com/rust-works/omni-dev/issues/1589,
  [ADR-0073]adrs/adr-0073.md) use this same kind, with `operation` of
  `sheets-write`/`sheets-append`/`sheets-clear`/`sheets-create`/
  `sheets-find-replace` — so
  `command` reads `["drive", "sheets-write"]` even though the CLI spells it
  `drive sheets write`. They add five more omit-if-absent context keys:
  `range` (the A1 range as composed and sent), and `updated_range`,
  `updated_rows`, `updated_columns`, `updated_cells` (what the API reported
  actually changing). `updated_range` can differ from `range` — the server
  resolves an open-ended range against the sheet's real extent — which is
  what makes `omni-dev log --query kind:drivemutation` able to answer "how
  much did that write touch", not just "a write happened".

  `sheets-find-replace` also records `occurrences_changed` and a compact
  scope/count summary. Its `--find` and `--replacement` values are document
  content and are redacted from the invocation log and absent from mutation
  records.

  Structural edits (issue
  [#1613]https://github.com/rust-works/omni-dev/issues/1613,
  [ADR-0075]adrs/adr-0075.md) use the same kind again, with `operation` of
  `sheets-add-sheet`/`sheets-rename-sheet`/`sheets-insert-rows`/
  `sheets-insert-columns` — **one record per verb**, which is also one per
  `spreadsheets.batchUpdate` request, since each verb sends a batch of
  exactly one. They add four more omit-if-absent context keys: `sheet_id`
  (the stable numeric id the API addresses, which survives a later rename
  and so is the only durable answer to "which tab was this"), `sheet_title`
  (its title at the time), `sheet_new_title` (the title a `rename-sheet`
  moved it *to* — set by that verb alone, since `sheet_title` necessarily
  holds the title the sheet had before, and without it a record could say
  which tab was renamed but not to what), and `dimension_range` (e.g.
  `"ROWS 5:7"`, 1-based inclusive like the CLI's `--at`). `dimension_range`
  is the structural analogue of `range`, for effects A1 notation cannot
  express, and is omitted for a span that spans nothing; a structural verb
  sets no `range` and no cell counts. `move-rows`/`move-columns` (issue
  [#1834]https://github.com/rust-works/omni-dev/issues/1834) add one more
  omit-if-absent context key, `move_to`: the raw 1-based `--before` value —
  `None` for every other verb. It is distinct from `dimension_range`, which
  for these two verbs holds the *source* span that moved, not the
  destination.

  Destructive edits (issue
  [#1623]https://github.com/rust-works/omni-dev/issues/1623,
  [ADR-0077]adrs/adr-0077-sheets-deletion-via-batchupdate.md) use the same
  kind again, with `operation` of `sheets-delete-sheet`/`sheets-delete-rows`/
  `sheets-delete-columns`/`sheets-delete-range` — same
  one-record-per-verb-and-per-request shape as the structural edits above,
  and the same four structural context keys (`sheet_id`, `sheet_title`,
  `dimension_range` for the two dimension-delete verbs). They add one more
  omit-if-absent context key: `grid_range` (e.g. `"rows 2-4, columns 2-3"`,
  1-based inclusive like the CLI's `--start-row`/`--end-row`/
  `--start-column`/`--end-column`) — the `deleteRange` analogue of
  `dimension_range`, which only ever spans one axis and so cannot record a
  rectangle. `delete-sheet` sets neither `dimension_range` nor `grid_range`.
  Every `--dry-run`, additive or destructive, is unlogged — `record_attempt`
  only ever runs for a real mutation attempt — and a destructive one stays
  structural-only besides: no cell content is ever read or logged for any
  structural verb, delete included.

  Formatting, data validation and protected ranges (issue
  [#1643]https://github.com/rust-works/omni-dev/issues/1643,
  [ADR-0078]adrs/adr-0078.md) use the same kind again, still **one record
  per verb**: `update-borders` can set up to four `Border` sides in a single
  request, which is the first case where one request carries more than one
  independently-toggleable effect, but it is still one request, so the
  one-record-per-verb-per-request equivalence ADR-0075 §7 established holds.
  New `operation` values: `sheets-format-cells`, `sheets-update-borders`,
  `sheets-merge-cells`, `sheets-unmerge-cells`,
  `sheets-auto-resize-dimension`, `sheets-update-dimension-properties`,
  `sheets-duplicate-sheet`, `sheets-reorder-sheet`, `sheets-hide-sheet`,
  `sheets-show-sheet`, `sheets-move-rows`, `sheets-move-columns`
  (issue [#1834]https://github.com/rust-works/omni-dev/issues/1834),
  `sheets-set-data-validation`,
  `sheets-clear-data-validation`, `sheets-protect-range`,
  `sheets-update-protection`, `sheets-unprotect-range`. Six more
  omit-if-absent context keys: `fields_changed` (a human-readable summary of
  which `CellFormat`/border/dimension-property fields a verb set — the
  formatting analogue of `dimension_range`, needed because a single request
  here can carry more than one named effect), `discarded_cells`
  (`merge-cells` only — the non-top-left, non-blank cells a merge
  discarded, as `"A1: value"` strings, so the one formatting request that
  destroys data has a record saying exactly what was lost),
  `validation_type` (the condition type a `set-data-validation` applied, or
  `"cleared"`), `protected_range_id` (the stable numeric id of a protected
  range a protection verb acted on — server-assigned for `protect-range`,
  otherwise the one resolved against), and `protection_editors_added`/
  `protection_editors_removed` (comma-separated).

  Filter views and the basic filter (issue
  [#1794]https://github.com/rust-works/omni-dev/issues/1794,
  [ADR-0081]adrs/adr-0081.md) use the same kind again. New `operation`
  values: `sheets-set-basic-filter`, `sheets-clear-basic-filter`,
  `sheets-add-filter-view`, `sheets-update-filter-view`,
  `sheets-delete-filter-view`. One more omit-if-absent context key:
  `filter_view_id` (the stable numeric id of a filter view acted on —
  server-assigned for `add-filter-view`, otherwise the one resolved
  against; absent for `set-basic-filter`/`clear-basic-filter`, which
  reuse `sheet_id` instead, since a basic filter is scoped to a sheet
  rather than id-addressed). `fields_changed` is reused as-is for
  `update-filter-view`'s changed-field summary.

  Charts and slicers (issue
  [#1797]https://github.com/rust-works/omni-dev/issues/1797,
  [ADR-0081]adrs/adr-0081.md §3) use the same kind again. New `operation`
  values: `sheets-add-chart`, `sheets-update-chart`, `sheets-delete-chart`,
  `sheets-add-slicer`, `sheets-update-slicer`, `sheets-delete-slicer`. One
  more omit-if-absent context key: `embedded_object_id` (the stable numeric
  id of a chart or slicer acted on — server-assigned for `add-chart`/
  `add-slicer`, otherwise the one resolved against; one key for both kinds,
  since `deleteEmbeddedObject` itself addresses both by the same `objectId`
  with no discriminator). `sheet_id` is reused for the sheet the object is
  anchored on (`None` for a chart put on its own new sheet, which doesn't
  exist yet); `fields_changed` is reused for the changed-field summary and,
  for `update-chart`/`update-slicer`/`delete-chart`/`delete-slicer` (every
  verb that resolves against an existing object), has the object's
  type/title/anchor appended — the ADR-0081 §3 preview text, in the audit
  trail as well as the `--dry-run` report.

  Moving/resizing an existing chart or slicer, and setting a chart's border
  colour (issue [#1837]https://github.com/rust-works/omni-dev/issues/1837)
  use the same kind again. New `operation` values: `sheets-move-chart`,
  `sheets-move-slicer`, `sheets-update-chart-border`. No new context key:
  `embedded_object_id` and `fields_changed` are reused exactly as above, and
  `sheet_id` records the destination sheet for a move — including a
  resize-only call, which carries the object's *current* anchor's sheet
  forward — and is `None` only for a `--new-sheet` move, which has no
  destination sheet yet.

  Banded ranges (issue
  [#1832]https://github.com/rust-works/omni-dev/issues/1832,
  [ADR-0082]adrs/adr-0082-banded-ranges.md) use the same kind again. New
  `operation` values: `sheets-add-banding`, `sheets-update-banding`,
  `sheets-delete-banding`. One more omit-if-absent context key:
  `banded_range_id` (the stable numeric id of a banded range acted on —
  server-assigned for `add-banding`, otherwise the one resolved against),
  mirroring `filter_view_id`'s precedent. `sheet_id` is reused for the
  sheet the banded range sits on.

  Dimension groups (issue
  [#1833]https://github.com/rust-works/omni-dev/issues/1833,
  [ADR-0084]adrs/adr-0084-dimension-groups.md) use the same kind again.
  New `operation` values: `sheets-add-dimension-group`,
  `sheets-update-dimension-group`, `sheets-delete-dimension-group`. No new
  context key: a dimension group carries no id of its own, so `sheet_id`
  and `dimension_range` (already used by the insert/delete-dimension
  structural verbs) are reused for the sheet and span a group acts on, and
  `fields_changed` (already used by the dimension-property formatting
  verbs) records `collapsed=true`/`collapsed=false` for
  `sheets-update-dimension-group`.

  `update-sheet-properties` (issue #1835,
  [ADR-0085]adrs/adr-0085.md) uses the same kind again, with
  `operation` of `sheets-update-sheet-properties`. It may set several
  properties (frozen rows/columns, hidden gridlines, tab color,
  right-to-left) in one request, so it needs no new context key: it reuses
  `fields_changed` for a comma-joined summary of what it set (e.g.
  `"frozenRowCount=2, tabColorStyle=#FF8800"`), the same key `format-cells`
  and friends already write, rather than introducing one of its own.

  `update-workbook-properties` (issue #1836,
  [ADR-0086]adrs/adr-0086-workbook-properties.md) uses the same kind
  again, with `operation` of `sheets-update-workbook-properties`. Like
  `update-sheet-properties` it may set several properties (locale, time
  zone, auto-recalc, iterative calculation) in one request, so it reuses
  `fields_changed` rather than a new key, but the summary it writes is a
  prose join rather than `update-sheet-properties`' `key=value` pairs —
  e.g. `"locale -> 'en_US', auto-recalc -> ON_CHANGE, iterative calculation
  -> on (max 50 iterations, threshold 0.01)"`, or `"iterative calculation ->
  off"` when clearing it — the same string `--dry-run`/the real-run
  confirmation print, from the one `workbook_properties_summary` helper
  that builds it. It is the one structural verb with no sheet target at
  all, so `sheet_id`/`sheet_title` are always absent on its records,
  unlike every other `sheets-*` operation.

  `insert-range` (issue #1838) uses the same kind again, with `operation`
  of `sheets-insert-range`. It is `delete-range`'s exact inverse and so
  needs no new context key either: it reuses `grid_range` (introduced
  above for `delete-range`) to record the same rectangle notation. Unlike
  the destructive edits above, `insert-range` is gated `sheets-structure`,
  not `sheets-delete` — no cell value is discarded, cells are shifted
  within the sheet and the vacated cells become empty — so `grid_range`
  is not exclusively a destructive-edit signal: a `sheets-insert-range`
  record carries it too, and only the `operation` value tells the two
  apart.

  `auto-fill` (issue #1840, [ADR-0083]adrs/adr-0083.md §1) uses the same
  kind again, with `operation` of `sheets-auto-fill` and gated
  `sheets-write`. No new context key for the destination itself: `range`
  (introduced above for `write`/`append`/`clear`) is reused for the
  destination's A1 address, and `fields_changed` for a prose summary of the
  effect — the destination, the source range it extends, the direction, and
  the alternate-series flag. That summary is deliberately **tense-neutral**:
  the same string is rendered under both `Would …` and `Applied: …`, so it
  never describes a completed mutation in the conditional.

  Two new keys. `overwritten_cells`: the non-blank cells in the destination
  that were overwritten, as **bare A1 addresses** — deliberately not
  `"A1: value"` like `discarded_cells`, since a fill's values are Sheets'
  own series detection and this crate never records them, before or after
  the request. `overwritten_cells_are_upper_bound`: present (and `"true"`)
  only for `auto-fill --range`, where Sheets decides for itself which cells
  in the named range are the source and which are filled — some of the
  listed cells are the source and were never touched. Absent means the list
  is exact, which is `--source` and every other verb.

  `text-to-columns` (issue #1843, [ADR-0083]adrs/adr-0083.md §§1, 5) uses
  the same kind and keys again, with `operation` of
  `sheets-text-to-columns`, gated by `sheets-write` **and**
  `sheets-structure`. `range` carries the source column's A1
  address (not the spill span — the request log's `range` key is always
  the value that started the write for every verb in this tranche), and
  `fields_changed` the tense-neutral summary: the source, the delimiter,
  and the upper-bound width and spill span. `overwritten_cells_are_upper_bound`
  is **always** `"true"` for this verb, unlike `auto-fill`'s form-dependent
  flag — the API decides for itself how many columns each row's split
  needs, so the locally computed span can only ever be an upper bound, and
  `overwritten_cells` never carries the split pieces or the source's
  values, only the bare A1 addresses of the cells the split might reach.
  Under `--delimiter auto` the span is not even a bound (Sheets picks the
  separator too, and was measured picking a tab, which the local split
  never tries), but the key has no third state; that caveat is carried in
  the rendered output rather than in the record.

  `trim-whitespace` and `delete-duplicates` (issue #1844,
  [ADR-0083]adrs/adr-0083.md §§1, 2, 6) use the same kind, with
  `operation` of `sheets-trim-whitespace` and `sheets-delete-duplicates`.
  They are the tranche's one pair taking **different gates** —
  `sheets-write` for the trim, `sheets-delete` for the dedupe — so a
  `decided_by_*` chain on one says nothing about the other. Neither adds a
  context key: `fields_changed` carries the server's own count
  (`cellsChangedCount`, `duplicatesRemovedCount`) plus the range, or says
  the API reported no count.

  Both counts are present as `0` rather than omitted when nothing
  changed — measured live — so `fields_changed` reading "trimmed 0
  cell(s)" is the ordinary no-op case, not a missing value. The
  "the API reported no count" wording is reserved for a reply that
  carried no response object at all.

  `delete-duplicates` deliberately records **no** cell or row list. ADR-0083
  §6 places it in the server-decided preview tier — Sheets chooses the rows
  by an equality rule this crate does not reproduce — so there is nothing
  to record that the tool could vouch for. `trim-whitespace` records none
  either: its real run reports the server's exact count instead of
  re-reading the range, so the candidate list exists only in the `--dry-run`
  outcome, which writes no mutation record at all.

  Text writes through the Docs API (issue
  [#1615]https://github.com/rust-works/omni-dev/issues/1615,
  [ADR-0076]adrs/adr-0076.md) use this same kind, with `operation` of
  `docs-replace`/`docs-append`/`docs-insert`/`docs-delete` (or `slides-replace`
  for Slides text replacement), and add three more omit-if-absent context
  keys: `occurrences_changed` (what the *server* reported changing, which
  can differ from the client-side `--dry-run` estimate), `inserted_chars`,
  and `required_revision_id` (the revision lease presented, recorded so a
  `stale-revision` refusal is as auditable as a success — an opaque,
  short-lived id, not a secret).

  Anchored edits ([ADR-0094]adrs/adr-0094.md) reuse these keys:
  insertion reports `inserted_chars`, and deletion reports its status and
  required revision. Resolved ranges, tab ids and paragraph/removal counts are
  in the command outcome, rather than new log context fields.

  **The searched, replacement, appended, inserted and deleted text, including
  anchor text, are never recorded in these metadata fields.** They
  are user prose, and often the most sensitive thing in the invocation —
  sharper than the Sheets case, where an A1 range is metadata rather than
  content. Only counts and the opaque revision id go in.

- **`kind: "audit"`** — written to the **separate** `audit.jsonl` sink, never
  to this file — see [Audit log]#audit-log. Every other `kind` above stays
  exactly as described regardless of the audit log's existence: a leased
  Drive write (issue [#1664]https://github.com/rust-works/omni-dev/issues/1664,
  [ADR-0080]adrs/adr-0080.md) still produces its normal `drivemutation`
  record here, correlated to its `audit` record by the shared
  `invocation_id`.

Every HTTP, `gh`, `worktree`, and `drivemutation` record shares an
`invocation_id` with the invocation that issued it, so you can pull a run and
all of its requests with a single `--id`.

**Coverage caveat:** `omni-dev`'s own in-process HTTP clients and its Rust `gh`
calls (daemon *and* one-shot CLI) are recorded. The one gap left is the VS Code
companion extension's `gh pr list`, which runs in a **separate Node process** and
does not write to this log.

## Location

Resolved in this order:

1. `OMNI_DEV_LOG_FILE` if set.
2. Otherwise `dirs::state_dir()` joined with `omni-dev/log.jsonl` — on Linux,
   `~/.local/state/omni-dev/log.jsonl`.
3. On platforms without a state dir (macOS), it falls back to the data dir,
   matching the daemon's convention: `~/Library/Application Support/omni-dev/log.jsonl`.

The directory is created `0700` and the file `0600`, the same posture as other
`omni-dev` runtime state. The log lives entirely on your machine.

## Environment variables

| Variable | Effect |
|----------|--------|
| `OMNI_DEV_LOG_FILE` | Override the log path. |
| `OMNI_DEV_LOG_DISABLE=1` | Disable logging entirely. |
| `OMNI_DEV_LOG_BODIES=1` | Opt in to recording request/response bodies (off by default; payloads are large and may contain customer content). |
| `OMNI_DEV_LOG_HEADERS=1` | Opt in to recording (redacted) request/response headers. |
| `OMNI_DEV_LOG_MAX_SIZE` | Enable automatic size-capped rotation on write, e.g. `10mb` (unix only; see [Bounding growth]#bounding-growth). |
| `OMNI_DEV_LOG_KEEP_FILES` | Number of rotated files to keep when rotation is enabled (default `3`). |
| `OMNI_DEV_AUDIT_LOG_FILE` | Override the **audit** log path — see [Audit log]#audit-log; none of the other variables above affect it. |

Logging is **best effort**: a write failure is swallowed (logged only at
`tracing::debug`) and can never change the command's exit code. The audit log
is the deliberate exception — see [Audit log](#audit-log).

## `omni-dev log`

```
omni-dev log [OPTIONS]          # search (default)
omni-dev log prune [OPTIONS]    # trim the log — see Bounding growth
```

With no subcommand, `omni-dev log` searches and prints the log using the filters
below. The `prune` subcommand trims it; see [Bounding growth](#bounding-growth).

### Filters

| Flag | Matches |
|------|---------|
| `--since <DUR_OR_TS>` | Lower time bound: a relative window (`30m`, `2h`, `1d`, `1w`, `45s`), a date (`2026-07-01`), or an RFC3339 timestamp. |
| `--until <DUR_OR_TS>` | Upper time bound: same forms as `--since` (a relative value means that long ago). Pair with `--since` for a bounded window. |
| `--method <METHOD>` | HTTP method (case-insensitive). |
| `--status <STATUS>` | Exact (`200`), class (`5xx`), or comma list (`4xx,5xx`). |
| `--service <NAME>` | `jira`, `confluence`, `datadog`, `browser-bridge`, `snowflake`, `transcript`, `anthropic`, `bedrock`, `openai`, `ollama`, `claude-cli`. |
| `--command <PATH>` | Resolved command-path prefix on whole segments, e.g. `"jira read"`. |
| `--url <SUBSTR>` | Substring of the request URL. |
| `--grep <REGEX>` | Regular expression against the raw JSON line. |
| `--fuzzy <TOKEN>` | Substring of the raw line; repeatable, AND-ed. |
| `--query <EXPR>` | Query expression (see below); repeatable, AND-ed. |
| `--id <ID>` | This record `id` **or** `invocation_id` — pulls a run and its requests. |

### Output

| Flag | Effect |
|------|--------|
| `-o, --output <oneline\|json\|full>` | `oneline` (default), `json` (NDJSON, byte-identical to the file — composes with `jq`), or `full` (pretty block). |
| `-n, --limit <N>` | Show at most the N most recent matching records. |
| `-f, --follow` | Tail the log, printing new matching records as they arrive. |
| `--audit` | Read `audit.jsonl` instead of `log.jsonl` — see [Audit log]#audit-log. Every filter, the `--query` mini-language, and all three output formats apply unchanged; only the file being read differs. |

### The `--query` mini-language

- **Structured terms:** `field:value` — `kind`, `source`, `service`, `method`,
  `status` (supports `5xx`), `command`, `url`, `id`, `invocation_id` (alias
  `inv`), `mcp_tool` (alias `tool`), `via_daemon`, `error`, plus the fields that
  were previously reachable only through `--grep`: `exit_code` (alias `exit`),
  `duration_ms` (aliases `duration`, `dur`), `elapsed_ms` (alias `elapsed`),
  `hostname` (alias `host`), `system_user` (alias `user`), `cwd`, and
  `auth_principal` (alias `principal`). Field matching is shared with the flags,
  so `--status 5xx` and `status:5xx` behave identically.
- **`status` is kind-aware:** for every kind but `drivemutation` it matches the
  HTTP `status_code` (class or comparator syntax, below). A `drivemutation`
  record has no `status_code` — its domain status (`blocked`, `written`,
  `stale-revision`, ...) lives in the `context` map instead, so `status:` there
  matches `context["status"]` by exact case-insensitive equality, e.g.
  `kind:drivemutation status:blocked`.
- **Numeric comparisons:** numeric fields (`exit_code`, `duration_ms`,
  `elapsed_ms`, and `status`) accept a leading comparator — `>`, `>=`, `<`,
  `<=`, or bare `=`/`N` for equality — so slow requests and failed runs are
  expressible directly (`elapsed:>1000`, `exit_code:>0`). `status` still also
  accepts its class syntax (`5xx`). Comparator/class syntax applies only to the
  HTTP `status_code` path, not the `drivemutation` case above.
- **Text fields** (`url`, `hostname`, `system_user`, `cwd`, `auth_principal`)
  match a case-insensitive substring.
- **Context fields:** any other field name falls back to the record's free-form
  `context` map (case-insensitive substring), so worktree recovery fields query
  directly: `branch:issue-1392`, `path:demo-wt`.
- **Bare tokens** are fuzzy substring matches against the raw JSON line.
- **Operators:** `AND` (also implicit between adjacent terms), `OR`, `NOT` (or a
  leading `-`), and parentheses. Use `"quotes"` for a value containing spaces.

```bash
omni-dev log --query 'kind:http AND (status:5xx OR method:POST)'
omni-dev log --query 'service:jira -status:2xx'        # jira requests that did not 2xx
omni-dev log --query 'elapsed:>1000'                   # requests slower than 1s
omni-dev log --query 'kind:invocation exit_code:>0'    # failed runs
```

### Examples

```bash
# The last 20 things you ran.
omni-dev log -n 20

# Server errors in the last two hours.
omni-dev log --since 2h --status 5xx

# Slow datadog requests (native — no jq needed).
omni-dev log --service datadog --query 'elapsed:>1000'

# A bounded historical window.
omni-dev log --since 2026-07-01 --until 2026-07-02

# A run and every request it made.
omni-dev log --id 0001718000000-0a1b2c3d4e5f6071

# Compose with jq for anything not directly expressible.
omni-dev log -o json --service datadog | jq 'select(.status_code == 429)'

# Follow live.
omni-dev log -f --service browser-bridge
```

### Recovering a removed worktree

`omni-dev git worktree remove` records the worktree's branch, HEAD commit, and
dirtiness *before* the removal, so an accidental remove is recoverable minutes
later from the log alone:

```bash
# What worktree did I just remove?
omni-dev log --command 'git worktree remove' --since 1d -o full

# Or by any recovery field directly.
omni-dev log --service worktree --since 30m
omni-dev log --query 'branch:issue-1392'
```

The `full` record's `context` carries `path`, `branch`, `commit`,
`had_uncommitted`, and `used_force` — enough to re-create the branch
(`git branch <branch> <commit>`) and re-attach a worktree
(`omni-dev git worktree add <path> <branch>`). `prune` records carry the same
triple per pruned entry in a JSON `pruned` list.

## Counting records

`omni-dev log count` aggregates the log by record **kind** and **source** over an
optional window — a quick tally without paging through matches:

```
omni-dev log count [--since <DUR_OR_TS>] [--until <DUR_OR_TS>] [--source <SOURCE>] [--kind <KIND>] [--json]
```

| Flag | Effect |
|------|--------|
| `--since <DUR_OR_TS>` | Lower time bound — same forms as `omni-dev log --since` (`1h`, `2026-07-01`, RFC3339). |
| `--until <DUR_OR_TS>` | Upper time bound (a relative value means that long ago). |
| `--source <SOURCE>` | Restrict to `cli`, `daemon`, or `mcp`. |
| `--kind <KIND>` | Restrict to `invocation`, `http`, `gh`, `worktree`, or `drivemutation`. `gh` unlocks the GitHub breakdown (below). |
| `--json` | Emit JSON (string map keys, composes with `jq`) instead of the table. |

With no `--kind` it reports the total and the split by kind and source:

```bash
# What has this machine done in the last day?
omni-dev log count --since 1d
```

### Counting GitHub API calls

Because every GitHub call is recorded as a `kind: "gh"` line (see [What gets
recorded](#what-gets-recorded)), `--kind gh` turns the log into a ground-truth
count of the GitHub API calls `omni-dev` itself makes — the *local* counterpart
to `gh api rate_limit`, which is GitHub's *server-side* view of everything the
token spent. It renders a richer breakdown by **category** — `api` (`gh api …`),
`subcommand` (`gh pr list`, `gh repo view`, …), and `local` (`gh --version`,
excluded from the API total) — as well as by subcommand and by source:

```bash
# All GitHub API calls in the last hour.
omni-dev log count --kind gh --since 1h

# Just what the daemon's pollers spent today, as JSON.
omni-dev log count --kind gh --since 1d --source daemon --json

# The raw records (composes with jq / anything log can express).
omni-dev log --query 'kind:gh'
```

The counts share the request log's storage, so they honor the same window and
are subject to `prune`/rotation. The daemon also logs a summary of these counters
to its own log (`daemon.log` / the journal) ~5 seconds after startup, every 10
minutes, and once on shutdown — a periodic, restart-delimited footprint without
anyone running a command; `daemon status` surfaces the current count too.

Those daemon figures count calls **since the daemon started**. It reads no part of
the log that was already there when it started, and each summary reads only the
records appended since the previous one, so what a summary costs does not grow with
the size of the log — a multi-gigabyte `log.jsonl` no longer delays startup,
`daemon status` or shutdown. If the log is replaced or truncated underneath it
(`log prune`, size-capped rotation), the daemon rebuilds its count once from what
the live file holds, which is what `omni-dev log count --kind gh --since <daemon
start>` reports at that point, so records rotated into `log.jsonl.1` stop being
counted. A summary that shutdown cut short, or that could not read the log, is
marked `incomplete`: in the summary line, and as `"incomplete": true` in the
`summary` op's reply.

**Not yet counted:** the VS Code companion extension's `gh pr list` runs in a
separate process and does not write to the log (a planned follow-up).

## Bounding growth

The log is default-on for every invocation **and** every outbound request, so on
an active machine it grows steadily. Two opt-in bounds keep it in check; both are
off by default, so nothing changes unless you ask for it.

### `omni-dev log prune`

Trims the log in place, by age and/or by size:

```
omni-dev log prune [--older-than <DUR>] [--max-size <SIZE>] [--dry-run]
```

| Flag | Effect |
|------|--------|
| `--older-than <DUR>` | Remove records older than a relative window (`7d`, `24h`, `2w`, `45m`). |
| `--max-size <SIZE>` | After age pruning, drop the **oldest** records until the file is at most `<SIZE>` (`10mb`, `512kb`, or a bare byte count). |
| `--dry-run` | Report what would be removed without modifying the file. |

At least one of `--older-than` / `--max-size` is required. Sizes are binary
(`kb`/`mb`/`gb` = 1024-based). Records with a missing or unparseable timestamp
are kept (age pruning only removes records it can positively date as old), and
`--max-size` always keeps at least the single most recent record.

```bash
# Keep the last 30 days; preview first.
omni-dev log prune --older-than 30d --dry-run
omni-dev log prune --older-than 30d

# Cap the file at 20 MB, dropping the oldest records to fit.
omni-dev log prune --max-size 20mb
```

Pruning rewrites the file atomically (a same-directory temp file is renamed over
the original, preserving the `0600` mode), so a concurrent reader never sees a
half-written file. It is not locked against concurrent **writers**, though: a
record appended during the rewrite may be lost, and — because `prune` is itself
a logged invocation — pruning the active log appends one new record of its own.
For exact accounting, prune with `OMNI_DEV_LOG_DISABLE=1` or when the log is idle.

### Automatic size-capped rotation

Set `OMNI_DEV_LOG_MAX_SIZE` (e.g. `10mb`) to rotate on write: before an append
that would push the file past the cap, `log.jsonl` is renamed to `log.jsonl.1`
(shifting any existing `log.jsonl.1` → `.2`, and so on) and a fresh `log.jsonl`
is started. `OMNI_DEV_LOG_KEEP_FILES` (default `3`) bounds how many rotated files
are retained; the oldest beyond that is deleted. Total on-disk use is therefore
roughly `(OMNI_DEV_LOG_KEEP_FILES + 1) × OMNI_DEV_LOG_MAX_SIZE`.

Rotation is **unix-only** and **best effort**: when it is enabled, writers
serialize on a stable `log.jsonl.lock` file (created `0600`) for the
check-rotate-append sequence, and a rotation failure falls back to appending
without rotating rather than dropping the record. The env vars are read per
write, so they must be present in the environment of whatever writes the log
(your shell for CLI runs, or the daemon's environment for daemon-served
requests). A set-but-invalid `OMNI_DEV_LOG_MAX_SIZE` is ignored (logged at
`tracing::debug`) and leaves rotation off. The `omni-dev log` reader already
tolerates truncation and rotation, so `-f/--follow` keeps working across a
rotation (it restarts from the top of the fresh file).

## Audit log

`audit.jsonl` is a **separate** file, a sibling of `log.jsonl` under the same
runtime directory, reusing the same `LogRecord` schema and the same
`omni-dev log` reader — pass `--audit` to read it instead of `log.jsonl`; see
[Filters](#filters)/[Output](#output) above, all of which apply unchanged.
It exists to serve the opposite guarantee from everything above: where
`log.jsonl` is best-effort, prunable, and can be disabled outright,
`audit.jsonl` is **fail-closed** — an operation whose audit record cannot be
written does not happen — and exempt from every growth bound this page just
described:

| | `log.jsonl` | `audit.jsonl` |
|---|---|---|
| On a write failure | Swallowed; the command's exit code is unaffected | Propagated; the caller aborts the operation it was about to audit |
| `OMNI_DEV_LOG_DISABLE=1` | Suppresses all writes | No effect |
| `omni-dev log prune` | Trims by age/size | Refuses `--audit` outright (`log prune does not support --audit`), **and** refuses any resolved log path that names the audit file — e.g. `OMNI_DEV_LOG_FILE` pointed at `audit.jsonl` directly, via a `..` segment, or via a symlink (redundant in practice since #1747: the aliasing `OMNI_DEV_LOG_FILE` fails to resolve at all before `prune` ever runs — kept as defense in depth for a caller that bypasses normal path resolution) |
| `OMNI_DEV_LOG_MAX_SIZE` rotation | Applies | Never applies, regardless of the setting or how `OMNI_DEV_LOG_FILE` is spelled |
| Path override | `OMNI_DEV_LOG_FILE` | `OMNI_DEV_AUDIT_LOG_FILE` |
| Location when unset | `<state dir>/omni-dev/log.jsonl` | `<state dir>/omni-dev/audit.jsonl` |

`--audit` is a flag on the bare search form (`omni-dev log --audit ...`) and,
separately, on `prune` (`omni-dev log prune --audit`, always refused, above);
placed before a subcommand name instead — `omni-dev log --audit prune ...` —
it is refused rather than silently ignored, since a subcommand never
consults it. If `OMNI_DEV_LOG_FILE` and `OMNI_DEV_AUDIT_LOG_FILE` are
configured to resolve to the **same file**, the request-log path itself
simply fails to resolve — compared by file identity (the `same-file` crate
— inode on unix, a file handle on Windows — falling back to a normalized
path when one side doesn't exist yet), not merely by identical spelling, so
a `..` segment, a relative-vs-absolute spelling, or a symlink can't slip
past the check. The two sinks are siblings by default but not otherwise
mutually exclusive by construction, so this misconfiguration is caught once,
centrally, at path resolution ([#1747](https://github.com/rust-works/omni-dev/issues/1747))
rather than by each consumer of the request-log path separately: neither a
best-effort append, rotation, nor `prune` can ever reach `audit.jsonl`
through an aliasing `OMNI_DEV_LOG_FILE`, since none of them get a path to
act on in the first place.

Every record carries `kind: "audit"` and, in `context`, an `integration` key
(`"drive"` today, so a later integration's audit trail is additive rather
than a rename), a `file_id`, and a `verdict` — alongside the usual
invocation/HTTP fields. `RecordKind::Audit` is the schema landed by issue
[#1664](https://github.com/rust-works/omni-dev/issues/1664)
([ADR-0080](adrs/adr-0080.md) §11); what populates it is the Drive
leased-write lifecycle, landing across that same issue's phases.

**`drive lease acquire`** writes one record per attempt, `command: ["drive",
"lease-acquire"]`, regardless of outcome — `verdict` is `acquired`,
`acquired-headless-waiver` (ADR-0080 §8/§13, issue #1677: proceeded under the
headless opt-out, no human ever prompted), `already-leased`,
`refused-native-document`, `refused-concurrent-change` (issue #1693: the
file changed while its backup was being taken, so no lease could vouch for
that backup — `error` names what moved), `denied`, `unavailable`, or
`failed`, matching `AcquireResult`'s own kebab-case status — plus, on
`already-leased`/`refused-concurrent-change`/`failed`, the
`-backup-orphaned` suffix (issue #1690): this attempt took a real backup
before discovering it isn't referenced by any ledger row, and reclaiming
that backup itself then failed, so it is now truly orphaned (`drive lease
prune` cannot see it either). An `acquired` record additionally carries
`lease_id` (the token), `version_after`/`modified_time_after` (the Drive
state actually recorded into the ledger — re-read after the backup, not the
pre-authentication snapshot, for the same TOCTOU reason the ledger itself
does), `backup_location` (a local path for a byte backup, or the backup
copy's own file id for a native-document backup),
`backup_sha256`/`backup_size` (byte backups only), and `auth_policy`
(`device-owner` or `biometrics-only`). An
`already-leased`/`refused-concurrent-change`/`failed` record also carries
`backup_location` whenever this attempt took a backup (a
`refused-concurrent-change` always did), whether or not reclaiming it
succeeded — the `-backup-orphaned` suffix is what distinguishes the two. Unlike a leased *write*'s own record (below), this
one is best-effort rather than write-ahead/fail-closed: acquiring mutates no
Drive content — by the time the record is written the consent, the backup
and the ledger row have already durably happened, so a
logging failure is warned and does not turn a successful acquisition into a
reported failure.

A leased **write** produces records in **both** files, correlated by
`invocation_id`: its ordinary `drivemutation` record here in `log.jsonl`
(ADR-0070/0071/0073/0075/0076/0077/0078), and, in `audit.jsonl`, one or
two `audit` records from `crate::drive::lease::check` — shared by every
leased-write engine (`drive edit`, the five Sheets engines and `drive docs
write`). Their `command` is `["drive", <operation>]` with the **verb's**
operation name — `["drive", "edit"]`, `["drive", "sheets-delete-sheet"]`,
`["drive", "docs-replace"]` — byte-for-byte the `command` of the same
write's `drivemutation` record, so `--query 'command:sheets-delete-sheet'`
matches both files and the audit record names the verb that ran, not
merely which engine ran it:

- A refusal that never reaches the mutating call writes **one** best-effort
  record — `verdict: refused-no-lease`, `refused-lease-expired`,
  `refused-lease-wrong-file` or `refused-lease-stale`, mirroring the
  engine's own reported result, or `failed` with the `error` when the
  ledger lock could not be taken. There is no mutation to pair a
  write-ahead record around.
- A presented lease that checks out writes a **write-ahead** `pending`
  intent record carrying `lease_id`, `version_before` and
  `modified_time_before`, `fsync`ed before the mutating API call this
  lease authorises, and fail-closed: if this record cannot be written the
  write itself is refused rather than proceeding unaudited (the one place
  in the whole ADR this contract applies, per §11). After the mutating
  call returns, a best-effort **outcome** record follows with the same
  `lease_id`: `verdict: allowed` with `version_after`/`modified_time_after`
  (from `files.update`'s own response for `drive edit`, from the post-write
  `files.get` the ledger refresh already pays for on Sheets/Docs), or
  `verdict: failed` with the API `error` — including a Docs `412` on
  `writeControl.requiredRevisionId`, which the CLI reports as
  `stale-revision`: the *lease* was not stale, the mutation simply did not
  happen, and the error says why.

- A Sheets `batchUpdate` that answers **2xx but whose reply cannot be
  parsed** is still a mutation that happened (issue #2021): its outcome
  record is `verdict: allowed` (with `version_after`, and the ledger is
  refreshed), and its `drivemutation` record has `status:
  applied-reply-unreadable`, `error` set to the parse detail, and
  `fields_changed` set to the change summary — never `failed`. Only a
  non-2xx response, or a transport failure before any status arrives,
  records `failed`.

An intent record with no matching outcome is itself the signal that
something was interrupted mid-write — the whole point of writing the
former durably before the latter can even be attempted.

**`drive lease restore <TOKEN>`** writes its own top-level, best-effort
record, `command: ["drive", "lease-restore"]`, in addition to the
generic write-ahead/outcome pair its internal fresh-lease acquisition and
restore write already produce (both keyed on the *fresh* token). This one
names **both** tokens (ADR-0080 §10/§11): `lease_id` is the fresh token
once minted (or the existing one, for `already-leased`), and
`restored_from_lease_id` is always the backup lease's own token — the
`<TOKEN>` argument — so `--query 'restored_from_lease_id:<token>'` finds
every restore attempt made from one backup regardless of outcome. `verdict`
matches `drive lease restore`'s own reported status: `restored`,
`restored-sheet`, `restored-headless-waiver`,
`restored-sheet-headless-waiver`, `sheet-already-restored`,
`no-such-backup-token`, `no-typed-restore-path`,
`backup-too-large-for-simple-upload`, `refused-no-visible-parents`,
`blocked`, `already-leased`, `refused-native-document`,
`refused-concurrent-change`, `denied`, `unavailable`, `failed`, or
`fresh-lease-but-write-failed`.

Restore *supersedes* the backup lease it restores from (issue #1685): the
internal fresh-lease acquisition releases that row in the same ledger write
that records the new one, so its `command: ["drive", "lease-acquire"]`
record additionally carries `superseded_lease_id` — the backup token — on
the `acquired` verdict, and only when a row was actually released. It is
the record to look for to answer "when did this lease stop being live?"
when that happened without a `release`.

**`drive lease release <TOKEN>`** writes one best-effort record per
attempt, `command: ["drive", "lease-release"]`, with `lease_id` the token
presented (even on a refusal, so a presented-but-dead token is greppable)
and `verdict` matching the command's own status: `released`,
`release-not-live` (already expired or already released; the row is left
untouched), `release-no-such-token`, or `failed` carrying the error. A
release makes no Drive call, so there is no `drivemutation` record to join
against.

## Redaction posture

No secret material is ever written, under any code path:

- Auth headers/tokens are **redacted centrally** before writing; only a
  non-secret `auth_principal` identity is ever kept. Redaction matches both a
  fixed list of known header names and any name containing `auth`, `token`,
  `key`, `secret`, `cookie`, `password`, `session`, `signature`, or
  `credential` (case-insensitive).
- URL query and fragment parameters whose keys look secret-bearing have their
  **values** replaced with `REDACTED` before writing: keys suffixed `token`,
  `secret`, `password`, `passwd`, `signature`, or `api_key`/`apikey`; the exact
  keys `sig`, `sas`, `jwt`, and `auth`; and the `X-Amz-*` / `X-Goog-*`
  signed-URL families. Host, path, and parameter keys are preserved, so
  `--url` substring filtering keeps working. This matters mostly for the
  browser bridge, which logs arbitrary operator-supplied target URLs
  (presigned URLs, `?access_token=…`).
- Request/response bodies are **opt-in** via `OMNI_DEV_LOG_BODIES=1`.
- The `OMNI_DEV_*` env snapshot redacts any name containing `TOKEN`, `SECRET`,
  `KEY`, `PASSWORD`, or `PASSWD`.
- Argv in the invocation record is scrubbed before writing, in both
  `--flag value` and `--flag=value` forms: `--header` values naming a sensitive
  header are redacted keeping the name (`Authorization: REDACTED`), inline
  `--body` values are redacted (`@file` references are kept), and any flag
  whose name has a `token`/`secret`/`password`/`passwd`/`key` segment has its
  value redacted (flags ending in `-file`/`-path` carry paths and are exempt).
  Every argv element is then run through the same URL query/fragment redaction
  as `--url` above, so a secret carried in a URL argument (e.g.
  `--url /path?access_token=…` or a presigned target) is redacted even though
  `--url` is not a secret-bearing flag name; benign argv passes through
  byte-identical.

### What redaction does not cover: prompt bodies

The guarantees above keep *secret material* out of the log; they do not make
prompt **content** secret. AI prompts carry whatever you asked about — repo
diffs, commit messages, JIRA/Confluence data — and that content surfaces on
three paths:

- `OMNI_DEV_LOG_BODIES=1` records AI request/response bodies, which **are** the
  prompts.
- `RUST_LOG=debug` stderr tracing emits the **full system/user prompt and full
  response** from every AI backend. Tracing is ephemeral and stderr-only, but
  if you redirect it to a file or paste it into a bug report, prompt bodies go
  with it.
- On a `claude-cli` subprocess failure, the returned error embeds the child's
  **stdout/stderr verbatim**, which can carry prompt-derived content into
  whatever captures the error (terminal, CI logs, this log's `error` field).

No API keys or tokens appear on any of these paths — this is a
data-sensitivity note, not a credential leak.

## Daemon-served requests

Requests executed inside the daemon (the browser bridge and the Snowflake
session pool) set `via_daemon: true`. Although the daemon is a separate process
from the CLI that asked it to act, such requests are still stamped with the
*originating* CLI invocation's `invocation_id`: the thin client threads its id
across to the daemon — on the control socket for Snowflake (an
`origin_invocation_id` envelope field) and on the `X-Omni-Bridge-Origin` header
for the bridge — and the daemon scopes it around the request it serves. So
`omni-dev log --id <cli-invocation>` surfaces the `via_daemon` requests that run
triggered, correlating both halves of one logical operation
([#1198](https://github.com/rust-works/omni-dev/issues/1198)).

The threaded id is non-secret (it is not a token), and the record's `source`
stays `daemon`, so a daemon-served request remains distinguishable by
`via_daemon` / `source` even while its `invocation_id` points at the caller. The
same mechanism correlates a standalone `bridge serve` (there `source` stays
`cli` and `via_daemon` is unset).

## Schema and compatibility

Records are read and written through a single forward-compatible struct: every
field is `#[serde(default)]` and every optional field is `skip_serializing_if`.
A newer reader never chokes on an older line, and an older reader never chokes on
a newer one — the same forward-rolling contract the daemon wire types use. The
record id is time-sortable, so sorting lines by `id` ≈ sorting by time.

### Slides replacement

`drive slides replace` records operation `slides-replace` in `drivemutation`
records. Outcomes include `replaced`, `blocked`, `stale-revision`, lease refusals,
and `applied-response-unreadable` (HTTP success; inspect before retrying). Records
include file identity, revision and server-reported occurrences when available.
Search/replacement prose and server diagnostics are excluded because an error
may echo request text. Dry runs emit no mutation record. Drive lease audit pairs
use `slides-replace` and conclude an unreadable successful response as `allowed`.