pigeon-cli 0.4.21

Pigeon: authenticate, sink, and transform personal data from external services.
Documentation
# Changelog

All notable changes to the `pigeon-cli` crate are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.4.21] - 2026-10-13

- ADR-0139: fixes `job run transform --input-file-type=jpeg` silently matching **no** `.jpg` files -- a run over a `.jpg` source pulled nothing, reported `0 rclone error(s)`, and exited 0. `parse` has accepted `jpg` as an alias for `jpeg` since ADR-0112, but `extension()` returned the single string `"jpeg"` and that became the source listing's `--include` pattern (`--ignore-case` folds case, not spelling). `extension()` is replaced by `extensions()` (a set per kind; only `Jpeg` is plural), `canonical_extension()` for display and prefix defaults, and `matches_extension()` for membership; `Lsjson` emits one `--include` per spelling. Auditing every consumer found a **second, independent gate** failing the same way -- `manifest::gather_pending` compared extensions for exact equality and would have dropped any `.jpg` already staged locally even once the listing filter was fixed. Also: jpeg's canonical spelling becomes `jpg` so ADR-0138's source and destination prefix defaults agree; a zero-match listing now warns on stderr *and* tracing (a `warn!` alone never reaches the terminal, which is how this hid); the dead `Copy.include_extension` is deleted. Closes the coverage hole that let it ship -- every prior jpeg test used a `.jpeg` fixture and none used `.jpg`, and the `--include` argv had no test at all. Amends ADR-0112, ADR-0122, and ADR-0138 ([#74](https://github.com/noisypigeon/pigeon-cli/pull/74)).

## [0.4.20] - 2026-10-13

- ADR-0138: restores composed `alias:prefix/` addressing, which ADR-0136 removed as having "no sub-path affordance" -- a regression, not a deferral, since the real deployment addresses one `source` and one `destination` bucket holding per-extension *directories*. `remote_address` becomes a `RemoteTarget` parsed from `alias[:prefix/]`: a bare alias is still the whole bucket root, the alias half is still keyring-validated, and prefixes are normalized (leading `/` stripped, one trailing `/`, `..` segments rejected). ADR-0137's `--input-file-type` inference moves off the bucket alias and onto the prefix, so `--source source` means `source:mp4/` while an explicit prefix wins verbatim -- which makes the alias mandatory again non-interactively and deletes ADR-0137's `prompt_select_bucket` default-alias hint as zero-consumer code. Applies to `transform` and `rclone copy`/`delete`; `deduplicate` keeps whole-bucket addressing pending its alias-keyed checkpoint rework ([#73](https://github.com/noisypigeon/pigeon-cli/issues/73)). Amends ADR-0136 and ADR-0137 ([#72](https://github.com/noisypigeon/pigeon-cli/pull/72)).

## [0.4.19] - 2026-10-13

- ADR-0137: `transform`'s `--source-bucket`/`--destination-bucket` rename to `--source`/`--destination` and become optional, defaulting when omitted to a bucket-config named after `--input-file-type`'s own extension (source) or output extension (destination) -- a naming convention, not content inspection. An explicit flag always wins, validated against the keyring exactly as before. `Store::prompt_select_bucket` gains a `default_alias` hint so the interactive picker highlights the inferred alias; every other caller (`rclone copy`/`delete`, `deduplicate`) is unaffected. Amends ADR-0136 ([#71](https://github.com/noisypigeon/pigeon-cli/pull/71)).

## [0.4.18] - 2026-10-13

- ADR-0136: sandboxes every `rclone` invocation to a config file pigeon itself generates from its own keyring (`src/core/rclone/config.rs`) -- never an externally-provisioned or user-general-purpose `rclone.conf` -- reversing ADR-0101/ADR-0132/ADR-0133's twice-reaffirmed "no `BucketConfig`-to-rclone translation" stance now that real usage is confirmed bucket-only. Implements ADR-0132's `RcloneCommand` trait consolidation (`Copy`/`Purge`/`Lsjson`/`CopyTo`, one shared runner replacing 6 hand-written invocations) and ADR-0133's `deduplicate`-off-`minio` migration together as the final shape: `rclone copy`/`delete`/`transform`'s `--source-path`/`--destination-path` revert to `--source-bucket`/`--destination-bucket` (ADR-0110's rename reversed), while `deduplicate` keeps its existing `--source-bucket`/`bucket_alias` identity and checkpoint shape entirely unchanged -- only its transfer mechanism moves (`--upload-concurrency` → `--transfers`/`--checkers`/`--tpslimit`). `download.rs`'s now-zero-consumer `download_with_retry`/`DownloadAnnounce` are deleted; the module is renamed `local.rs` ([#70](https://github.com/noisypigeon/pigeon-cli/pull/70)).

## [0.4.17] - 2026-10-13

- ADR-0135: retires the OS keychain entirely -- every secret (IMAP password, S3 secret key, encryption key) now lives in `keyring.toml` as a plaintext field, protected only by `0600` file permissions. Obsoletes rejected ADR-0035's unsolved macOS keychain-prompt problem and makes ADR-0085's Linux-keyutils workaround moot; `core::keyring::credentials` collapses to one infallible `resolve_secret` function still honoring `PIGEON_SECRET_<ALIAS>` (ADR-0087); the `keyring` crate is removed from `Cargo.toml`. No automated migration -- every existing entry needs re-adding via `pigeon keyring add` ([#69](https://github.com/noisypigeon/pigeon-cli/pull/69)).

## [0.4.16] - 2026-10-13

- ADR-0134: removes `pigeon job run decrypt-files` entirely -- no evidenced ongoing use, mirroring the redundancy argument that retired `sort` (ADR-0094) and `reduce` (ADR-0103). `Encryptor::decrypt` (`core::crypto`) is deleted outright, having never had a caller outside this job since ADR-0025 added it speculatively; every shared wizard helper it used had other active consumers and needed no un-hoisting ([#68](https://github.com/noisypigeon/pigeon-cli/pull/68)).

## [0.4.15] - 2026-10-12

- ADR-0133: migrates `deduplicate`'s three data legs (list, download, upload) off `minio` onto ADR-0132's `core::rclone` trait, making it the fifth `RcloneCommand` consumer. `--source-bucket`/`--destination-bucket` keyring aliases become raw `--source-path`/`--destination-path` rclone remotes; `--upload-concurrency` is removed in favor of `--transfers`/`--checkers`/`--tpslimit`; already-processed objects are never re-transferred on a resumed run (reusing ADR-0124's `--files-from-raw` skip); and the pull keeps today's download/process overlap via ADR-0116's `on_copied` channel rather than regressing to a slower barrier. Supersedes ADR-0113, the same migration decided 2026-10-10 but never implemented and since invalidated (its stated mechanism was deleted by ADR-0132; its dead-code claim about `download_with_retry`/`DownloadAnnounce` was invalidated once ADR-0131 deleted `pull_transform`, so `clippy -D warnings` now forces their deletion and `download.rs` is renamed `local.rs`). ADR-0109's bucket-alias identity re-keys to source paths throughout; `deduplicate` becomes keyring-free; pre-migration `.staging` state is structurally incompatible, with no conversion provided ([#67](https://github.com/noisypigeon/pigeon-cli/pull/67)).

## [0.4.14] - 2026-10-12

- ADR-0132: consolidates every `rclone` invocation behind one `RcloneCommand` trait plus one shared runner in a new `src/core/rclone/` (`Copy`/`Purge`/`Lsjson`/`CopyTo` implementors; `rclone_log.rs` moves in, `rclone_transfer.rs` is deleted). Six invocations were hand-written across four modules with no abstraction over "run an rclone command", so each new one copied the nearest and drifted: `run_rclone_copy` had grown to 13 positional params behind an `#[allow(clippy::too_many_arguments)]`, `run_delete_job` duplicated ~110 lines of its spawn/stderr-drain/poll loop, the two `lsjson` functions were near-identical, the "fixed" flag bundle had drifted three ways, `check_rclone_available` existed four times, and `lsjson` emitted no observability at all despite being a blocking full-tree listing on `transform`'s critical path. Also switches `--report-bucket` to `--report-path` on the four rclone-addressed jobs, leaving `transform` and `rclone copy`/`delete` keyring-free, at the cost of a new hard `rclone` dependency for `deduplicate` ([#65](https://github.com/noisypigeon/pigeon-cli/pull/65)).

## [0.4.13] - 2026-10-12

- ADR-0131: removes `job run pull-transform` entirely -- every capability it still offered is reachable by composing `rclone copy` -> `deduplicate` -> `transform` as separate, manually-verified steps, the same redundancy argument that retired `sort` (ADR-0094) and `reduce` (ADR-0103). Unlike those, not a clean deletion: `deduplicate` genuinely imports `pull_transform::archive`, so that module is first hoisted to `commands/job/archive.rs`. Un-hoists `SourceBucketInput` (deleted, 0 consumers), `EncryptionKeyInput`, `extension_of`/`after_zip_separator`, while keeping `download.rs` shared for `transform`'s sake; prunes three now-unused dependencies. 7 job types -> 6, and client-side encryption for file data is lost with no replacement ([#64](https://github.com/noisypigeon/pigeon-cli/pull/64)).

## [0.4.12] - 2026-10-12

- ADR-0130: hardens `job run transform` against four real defects found auditing it against `pull-transform` -- a source file `rclone` failed to pull vanished silently (its aggregate error count was never read, and the pull phase is now reconciled per-file against its own intended set), macOS AppleDouble `._*` stubs matched rclone's include filter and could trip the circuit breaker into aborting a whole run, `set_macro_phase` reported a one-way local-work-to-upload transition this job's per-file pipeline never makes, and there was no disk-space preflight; also closes the job's observability gap, which left `pigeon.jsonl` with no `transform` run data at all ([#61](https://github.com/noisypigeon/pigeon-cli/pull/61)).

## [0.4.11] - 2026-10-12

- ADR-0129: `job run transform` splits `--transfers`/`--checkers` into separate download and upload concurrency knobs (`--download-transfers`, `--download-checkers`, `--upload-transfers`), allowing independent tuning of pull vs. push phases; adds `--max-pending-downloads` to automatically pause/resume the download phase based on backlog depth, providing self-correcting backpressure without manual tuning ([#XYZ](https://github.com/noisypigeon/pigeon-cli/pull/XYZ)).

## [0.4.10] - 2026-10-12

- ADR-0128: adds a local Docker Compose observability stack (Prometheus + Loki + Promtail + Grafana) that scrapes pigeon's existing `/metrics` endpoint and tails `pigeon.jsonl`, standing in for the Scaleway Cockpit/Alloy pipeline now that transcode/inference jobs run bare-metal instead of on cloud compute; no `pigeon-cli` source changes needed for the observability surfaces themselves ([#59](https://github.com/noisypigeon/pigeon-cli/pull/59)).

## [0.4.9] - 2026-10-11

- ADR-0127: fixes `job run transform` OOM-crashing on an oversubscribed `--concurrency` by clamping it against the host's core count and capping each concurrent `ffmpeg` transcode's own thread count; also fixes a crash-resume bug where a file already transcoded and placed into `result/` before a crash got silently re-transcoded and then hard-failed on a false collision, instead of being pushed directly ([#58](https://github.com/noisypigeon/pigeon-cli/pull/58)).

## [0.4.8] - 2026-10-11

- ADR-0126: fixes `job run transform` still hard-failing real `.mov` files after ADR-0125, this time from ffmpeg's implicit auto-application of QuickTime Clean Aperture container-level crop metadata (FFmpeg 7.1+); `-apply_cropping:v codec` suppresses only that mechanism while preserving the long-standing codec-level crop behavior ([#57](https://github.com/noisypigeon/pigeon-cli/pull/57)).

## [0.4.7] - 2026-10-11

- ADR-0125: fixes `job run transform` hard-failing real `.mov` files whose embedded display matrix encodes a rotation plus a non-unity scale, which ffmpeg's implicit autorotate reshapes during decode; `-noautorotate` now suppresses that reshape (playback orientation unaffected, confirmed empirically) ([#56](https://github.com/noisypigeon/pigeon-cli/pull/56)).

## [0.4.6] - 2026-10-11

- ADR-0124: `job run transform` now deletes a file's local source/result copies immediately once it's durably pushed, and skips already-done files before downloading them at all instead of only after, bounding local disk usage to the in-flight window instead of the whole dataset ([#55](https://github.com/noisypigeon/pigeon-cli/pull/55)).

## [0.4.5] - 2026-10-11

- ADR-0122: adds `mov`/`m4v`/`mp4` input kinds and a `--video-quality` flag to `job run transform`, transcoding video to space-optimized H.265/HEVC `.mp4` via a small vetted `libx265` CRF menu ([#53](https://github.com/noisypigeon/pigeon-cli/pull/53)).
- ADR-0123: adds native Linux x86_64 and cross-compiled macOS x86_64 release binaries (4 matrix legs total), plus a `pigeon --version` smoke test for the 3 natively-executable ones ([#54](https://github.com/noisypigeon/pigeon-cli/pull/54)).

## [0.4.4] - 2026-10-11

- ADR-0121: transform's dimension guard now understands HEIF "Tile Grid" stream groups and compares pixel area instead of exact `(width, height)`, deterministic ffmpeg failures are no longer retried, and a sample-based circuit breaker aborts fast on systemic (not single-file) transcode incompatibility ([#52](https://github.com/noisypigeon/pigeon-cli/pull/52)).

## [0.4.3] - 2026-10-11

- ADR-0120: transform's missing transcode metric, child-process-blind resource sampling, and its ephemeral-VM-incompatible resume checkpoint. ([#51](https://github.com/noisypigeon/pigeon-cli/pull/51)).

## [0.4.2] - 2026-10-11

- ADR-0119: narrows the release workflow's pre-publish check from `mise run ci` to `fmt-check`+`lint`, dropping `mise run test` entirely -- the full suite assumes a fully-provisioned dev machine (`ffmpeg`, `rclone`, a working OS keyring session) that a fresh CI runner isn't, and every commit on `main` already passed it locally before merge ([#49](https://github.com/noisypigeon/pigeon-cli/pull/49)).
- ADR-0118: fixes the release workflow's `mise run ci` step failing on every run because a fresh runner's rustup toolchain lacks the `clippy`/`rustfmt` components it depends on -- adds an explicit `rustup component add rustfmt clippy` step ([#48](https://github.com/noisypigeon/pigeon-cli/pull/48)).
- ADR-0117: automates crates.io release and binary publishing on every merge to `main` -- a new GitHub Actions workflow (the first in this repo) bumps `Cargo.toml`'s patch version, cuts `CHANGELOG.md`'s `[Unreleased]`, publishes to crates.io, and builds+attaches macOS/Linux arm64 binaries to a GitHub Release; reverses ADR-0018's and ADR-0029's "releases stay manual" calls ([#46](https://github.com/noisypigeon/pigeon-cli/pull/46)).
- ADR-0116: fixes `job run transform` discarding every already-succeeded file whenever a single file's transcode failed -- a real run transcoded 1,605 files but pushed 0, because the old fail-fast design skipped the bulk push phase entirely on the first failure; download stays one bulk `rclone copy` but is now consumed live, and transcode (now retried) and push (new per-file `rclone copyto`) run independently per file, so one failure only costs that one file ([#45](https://github.com/noisypigeon/pigeon-cli/pull/45)).

## [0.4.1] - 2026-10-11

- ADR-0115: fixes `job run transform` failing every png/heic transcode with "Unable to find a suitable output format" -- `transcode_to_jpg` ran `ffmpeg` with no `-f` flag, so it inferred the output format from `worker.rs`'s scratch path, which ends in `.scratch` rather than a recognized extension; now forces `-f mjpeg` explicitly ([#44](https://github.com/noisypigeon/pigeon-cli/pull/44)).
- ADR-0114: renames `--yes` to `--non-interactive` on all 8 job CLI variants and widens its effect so it suppresses every wizard prompt (not only the final "proceed?" confirmation), fixing a real hang where an interactive-shell run with `--yes` and one omitted optional flag (e.g. `transform`'s `--tpslimit`) still blocked on an interactive prompt; breaking change, no migration shim ([#43](https://github.com/noisypigeon/pigeon-cli/pull/43)).
- ADR-0113: documents a planned refactor of `job run deduplicate`'s bucket actions to use `rclone` (raw `--source-path`/`--destination-path` instead of keyring-resolved `--source-bucket`/`--destination-bucket`), reusing the pull/local-work/push template `transform` (ADR-0112) already established; `--upload-only` and the pre-run preview table are explicitly kept rather than dropped. Design only -- implementation is a follow-up PR ([#42](https://github.com/noisypigeon/pigeon-cli/pull/42)).
- ADR-0112: adds `job run transform`, which shells out to `rclone` to pull files matching `--input-file-type` (`png`/`jpeg`/`heic`), transcodes/copies them locally into full-size maximum-quality `jpg`, and shells out to `rclone` again to push the result; destination filenames are derived deterministically (unique by construction, never collision-probed), and a transcode failure aborts the run while preserving already-checkpointed successes for a clean rerun ([#41](https://github.com/noisypigeon/pigeon-cli/pull/41)).
- ADR-0111: fixes `job run deduplicate` failing with a raw OS error when `--local-output` points at a directory that doesn't exist yet -- it's now created automatically, matching the pattern `rclone copy`/`rclone delete` already use ([#40](https://github.com/noisypigeon/pigeon-cli/pull/40)).
- ADR-0110: restructures `job run import` into `job run rclone copy` (renaming `--source`/`--destination` to `--source-path`/`--destination-path`) and adds a sibling `job run rclone delete` action wrapping `rclone purge` for recursive, observable bucket cleanup; breaking change, no migration shim ([#39](https://github.com/noisypigeon/pigeon-cli/pull/39)).
- ADR-0109: lets `job run deduplicate` pull from multiple source buckets in one run (`--source-bucket` is now repeatable, combined into one shared staging tree and deduplicated as a single set), renames its `--remote-output` to `--destination-bucket`, and fixes a cross-bucket checkpoint/provenance collision risk found while implementing it ([#38](https://github.com/noisypigeon/pigeon-cli/pull/38)).
- ADR-0108: exposes `job run import`'s `--transfers`/`--checkers`/`--tpslimit` rclone flags as optional CLI flags, after a Scaleway-backed 16GB transfer ran over an hour without completing under ADR-0106's fixed values and had to be killed; `--transfers`/`--checkers` keep their `8`/`16` defaults, `--tpslimit` now defaults to unset/no cap instead of ADR-0106's hardcoded `10` ([#37](https://github.com/noisypigeon/pigeon-cli/pull/37)).
- ADR-0107: fixes top-level `._*.zip` AppleDouble bucket objects reaching `deduplicate`'s/`pull-transform`'s zip-open path and deterministically failing every run (65 recurring false failures across a sampled set) via a new `archive::is_apple_double_basename` check in `process_item`, before any extension/zip-kind logic; also itemizes genuinely-failed archives (e.g. password-protected zips) by key and error in `deduplicate-report.txt`'s new manual-followup section instead of only a bare count ([#35](https://github.com/noisypigeon/pigeon-cli/pull/35)).
- ADR-0106: retunes `job run import`'s rclone flags (`--transfers` 32→8, `--checkers` 64→16, new `--tpslimit 10`) after a 33-run review found Backblaze B2 rate-limiting causing over 99.9% of ~494K sampled WARN/ERROR lines and several outright job failures; also collapses consecutive identical-cause rclone errors into one "N more" summary instead of logging each individually ([#34](https://github.com/noisypigeon/pigeon-cli/pull/34)).
- ADR-0105: fixes failed job runs uploading a 0-byte `transcript.txt` by adding `report_upload::say_error`, called at all 10 `Err`-arm call sites across every job wizard that already has a `Transcript` in scope, so the same failure message now lands in the archived transcript too ([#33](https://github.com/noisypigeon/pigeon-cli/pull/33)).
- ADR-0104: fixes the report-bucket artifact upload to log a run's own outcome (`report_upload::log_run_outcome`) before `upload_run_artifacts` runs, across all 10 affected call sites (every job wizard's `dispatch_async`/`dispatch_upload_only`), and adds `deduplicate`'s missing "upload phase complete" log line ([#31](https://github.com/noisypigeon/pigeon-cli/pull/31)).
- ADR-0107 (proposed): documents fixing the AppleDouble/`__MACOSX` zip-entry skip (ADR-0098) to also cover a top-level bucket object named `._<name>.zip` — not just entries inside an already-open zip — after sampling found 65 recurring, deterministic `"Could not find EOCD"` false archive failures across real runs, all AppleDouble sidecars; also proposes itemizing genuinely-failed archives (e.g. password-protected zips) in the report instead of only a bare count. Documentation only, no implementation yet ([#30](https://github.com/noisypigeon/pigeon-cli/pull/30)).
- ADR-0106 (proposed): documents retuning `job run import`'s fixed rclone concurrency/retry flags after real runs showed Backblaze B2 rate-limiting producing over 99.9% of ~494K sampled WARN/ERROR log lines and 4 outright job failures from exhausted retries. Documentation only, no implementation yet ([#30](https://github.com/noisypigeon/pigeon-cli/pull/30)).
- ADR-0105 (proposed): documents routing a job's failure message through its archived `transcript.txt`, not just stderr/the JSONL log — every failed run's archived transcript was found to be a 0-byte file, since only the success path writes to it today. Documentation only, no implementation yet ([#30](https://github.com/noisypigeon/pigeon-cli/pull/30)).
- ADR-0104 (proposed): documents fixing the ADR-0100 report-bucket artifact upload to capture a run's own completion event — today it uploads the shared log from inside `dispatch_async`, before the outer `run_instrumented` wrapper ever logs `"command finished"`, so the archived log for every sampled run was missing its own outcome. Documentation only, no implementation yet ([#30](https://github.com/noisypigeon/pigeon-cli/pull/30)).
- ADR-0103: removes `pigeon job run reduce` entirely — its automatic valuable/reproducible extension-table filter has been replaced by running `import` then `deduplicate` as two separate, manually-verified steps, with the keep/discard judgment now done by hand. Deletes `src/commands/job/reduce/` wholesale (mirrors ADR-0094's `sort` removal); `pigeon` goes from 7 job types to 6, upload-capable jobs from 5 to 4 ([#29](https://github.com/noisypigeon/pigeon-cli/pull/29)).
- ADR-0102: fixes `pigeon job run import` reporting `pigeon_job_macro_phase`/`pigeon_upload_bytes_total`/`pigeon_upload_outcomes_total` not at all, and its one `pigeon_job_phase_total` call only once after the whole `rclone copy` subprocess had already exited. Replaces the one-shot log parse with an incrementally-pollable `RcloneLogTailer` and switches `worker.rs` from `Command::output()` to `spawn()` plus a concurrent poll loop racing the child's exit, so metrics update live through a long-running transfer like every other job type ([#27](https://github.com/noisypigeon/pigeon-cli/pull/27)).

## [0.4.0] - 2026-10-04

- ADR-0101: adds `pigeon job run import`, a 7th job type that shells out to a single `rclone copy` invocation with fixed performance/retry flags; `--source`/`--destination` are raw rclone `remote:path` strings, never pigeon bucket-configs, since the point is reaching backends pigeon's own S3-only jobs can't. Redirects rclone's logging to a JSON-formatted, pigeon-managed per-run log, parses it after the subprocess exits into structured `tracing`/metrics events, and reuses that same log file as this job's ADR-0100 report artifact ([#25](https://github.com/noisypigeon/pigeon-cli/pull/25)).
- ADR-0100: adds a mandatory `--report-bucket` wizard step to all 6 job types, uploading that run's report, the shared `pigeon.jsonl` observability log (uploaded whole, not filtered to this run), and a transcript of its wizard-level printed narration to the configured bucket under a `{date}-{job-name}-{run-id}/` prefix, always unencrypted and best-effort — a failed upload never changes the job's own exit code. `deduplicate` keeps its existing bespoke report; the other 5 job types gain a generic report of their summary struct. `decrypt-files` gains its first-ever bucket-config dependency ([#24](https://github.com/noisypigeon/pigeon-cli/pull/24)).
- ADR-0099: fixes a zip member's synthetic `outer.zip!member` key leaking into its placed filename/extension (an extensionless member corrupted both), shared by `deduplicate` and `pull_transform`; adds `tracing::info!` phase-boundary logging to both jobs' previously-silent local phases and demotes `upload_one`'s per-file span to `level = "debug"`, cutting one real run's log volume from 143k lines to a handful by default across all five upload-capable jobs; replaces the permanent per-large-download `MultiProgress::println` announcement with a transient progress-bar message shared by `deduplicate`/`pull_transform`/`reduce`; extends zip-expansion's AppleDouble/`__MACOSX` skip to also drop `.DS_Store`/`.git`/`node_modules` entries; and adds a `kept_original_key` column to `deduplicate-report.txt` via a backward-compatible 3-field `ContentIndex` format ([#23](https://github.com/noisypigeon/pigeon-cli/pull/23)).
- ADR-0098: root-causes two real `job run deduplicate` runs' silent data loss and false failures in the shared zip-expansion path (`deduplicate` and `pull-transform`) — replaces the single run-wide 500 GiB extraction cap with a per-archive compression-ratio guard so one large legitimate bucket stops silently dropping every later zip member once the old global cap was hit; counts dropped members as real, exit-code-affecting failures instead of only logging them; excludes a zip's root key from the `.processed` checkpoint whenever any descendant fails or is dropped, so a rerun actually retries it; skips macOS AppleDouble/`__MACOSX` entries before treating anything ending in `.zip` as a nested zip, removing a class of false `"Could not find EOCD"` failures; propagates the `command` tracing span into `tokio::spawn`/`spawn_blocking` work that previously ran outside it; and fixes `upload.rs`'s `identity` span field and every non-`--upload-only` `upload_result` call site to carry the destination bucket's alias instead of the source's ([#22](https://github.com/noisypigeon/pigeon-cli/pull/22)).
- ADR-0097: adds a `"command started"` log line bracketing the existing `"command finished"` one, and hoists every job wizard's duplicated, unlogged `fail()` into one shared `commands::fail` that also emits a `tracing::error!("job failed")` event with the real reason before translating an `Err` into an exit code — previously a handled job failure only reached stderr, never the JSONL log (closes [#18](https://github.com/noisypigeon/pigeon-cli/issues/18)). Adds an `instance` (hostname) label, via a new cached `observability::instance()`, to every custom metric, and `source_bucket`/`destination_bucket` labels (the pigeon-configured alias) to `pigeon_job_phase_total` and the five `pigeon_upload_*` metrics wherever a `BucketConfig` is already in scope ([#21](https://github.com/noisypigeon/pigeon-cli/pull/21)).
- ADR-0096: renames `pigeon job run dedupe` → `pigeon job run deduplicate` (mechanical, no behavior change), and adds `pigeon job run reduce`: runs after `deduplicate`, classifies each top-level `<extension>/` directory of its already-deduplicated source bucket as either genuinely valuable or an artifact/piece of media (TV, movie, software installer, disk image) easily reproduced from an external canonical source via a curated, hardcoded extension table (overridable with repeatable `--force-valuable`/`--force-reproducible` flags), and forwards only the valuable extensions to a mandatory destination bucket — reproducible extensions are never even downloaded, avoiding the download-then-reupload round-trip every other bucket-to-bucket job pays, since this codebase has no server-side bucket-to-bucket copy primitive. Hoists `extension_of` (into `core::data`) and `SourceBucketInput` (into `shared_wizard.rs`) on `reduce`'s third-consumer trigger ([#20](https://github.com/noisypigeon/pigeon-cli/pull/20)).
- ADR-0095: fixes `dedupe`'s `place_and_report` always keeping whichever duplicate's `original_key` sorted lexicographically first, found from a real run where the source bucket's `0000-00-00-*` "unknown date" sentinel sorted ahead of real dated keys and 797 of 855 duplicates kept the undated copy over a dated one. Adds `is_undated_key` so a dated key now wins, falling back to the prior lexicographic order as a tie-break; does not remediate buckets already written under the old behavior ([#16](https://github.com/noisypigeon/pigeon-cli/pull/16)).
- ADR-0094: removes `pigeon job run sort` entirely — `dedupe` (ADR-0082) already performs the same extension-flattening as a side effect of its own placement step, and in practice every bucket handed to `sort` had already been through `dedupe` first, so `sort`'s one distinguishing capability (flattening a known-unique bucket without re-paying for a content-hash pass) was never actually exercised, leaving a redundant job type, CLI surface, and test suite along with a foot-gun ADR-0083 itself had flagged (nothing enforced `sort`'s source was actually deduped first). `pigeon` goes from 5 upload-capable job types to 4 ([#15](https://github.com/noisypigeon/pigeon-cli/pull/15)).
- ADR-0093: replaces ADR-0092's once-at-the-end job summary metrics with a live `pigeon_job_phase_total{pigeon_job,phase,outcome}` counter updating at every per-item completion point across all 5 upload-capable jobs, plus a `pigeon_job_macro_phase` gauge for the local-work/upload boundary — real mid-run progress visibility instead of only a final tally. Removes the two telemetry-shaped (not diagnostic) log lines found in the job tree (`resource_sample`, `"upload started"`, now metrics-only) and adds an `instance` (hostname) field alongside `command` on every remaining log line. Fixes two real gaps found along the way: `dedupe`'s placement failures were never counted anywhere; `pull_transform`'s `recode` failure category was declared but never incremented ([#14](https://github.com/noisypigeon/pigeon-cli/pull/14)).
- ADR-0092: adds a localhost-only Prometheus metrics endpoint (`--metrics-port`/`--no-metrics`/`$PIGEON_METRICS_PORT`, default port 9091) for an on-host observability agent (e.g. Grafana Alloy, forwarding to Scaleway Cockpit) to scrape — real counters/gauges/histograms for resource samples, per-command duration/status, upload-phase bytes/duration/failures, and per-job summary totals across the five upload-capable jobs, dual-emitted alongside (not replacing) the existing ADR-0073 JSONL log events ([#13](https://github.com/noisypigeon/pigeon-cli/pull/13)).
- ADR-0091: fixes three upload-phase performance problems found analyzing a real `job run dedupe` run's logs — `upload_if_changed` skips its pre-upload local hash when the destination key doesn't exist yet (was computed unconditionally, doubling disk reads for a fresh-destination run); a new size-scaled per-attempt upload timeout (60s floor + a 10 MiB/s-floor-throughput allowance) stops a stalled upload from holding a concurrency slot indefinitely (three real uploads each hung exactly 1800s with no timeout at all); and a new `--upload-concurrency` flag (default 16) decouples upload-phase parallelism from each job's unrelated `--concurrency`, generalized to all 5 upload-capable jobs (`dedupe`, `sort`, `pull-transform`, `email-sync`, `email-pull`) — including their `--upload-only` paths, where `--upload-concurrency` now fully replaces `--concurrency`'s effect ([#12](https://github.com/noisypigeon/pigeon-cli/pull/12)).

## [0.3.1] - 2026-10-02

- Updates the transitive `yoke-derive` dependency 0.8.3 → 0.8.4 (pulled in via `idna`/ICU4X through `url`, itself depended on by `async-native-tls`/`minio`) — 0.8.3 was yanked from crates.io with no reason given; `cargo update -p yoke-derive` confirmed an isolated bump with no other package changes.

## [0.3.0] - 2026-10-02

- ADR-0090: generalizes ADR-0088/0089's dedupe-only fixes to the rest of the job types per a per-job audit (not uniformly): `pull-transform`/`decrypt-files` get the CPU-aware `CpuConcurrencyInput` default and `spawn_blocking`-wrapped CPU-bound calls; `pull-transform`/`sort` get their in-memory dedup/placement state scoped to drop before the upload phase; `pull-transform`, `sort`, `email-sync`, and `email-pull` all gain `--upload-only` (the email jobs' version is per-identity, skipping an incomplete identity with a warning rather than failing the whole command); `sort`/`email-sync`/`email-pull`'s concurrency defaults are deliberately left untouched. Flags (without fixing) a pre-existing bug in `pull-transform`'s upload walk sweeping up its own bookkeeping dotfiles, filed as [#7](https://github.com/noisypigeon/pigeon-cli/issues/7) ([#8](https://github.com/noisypigeon/pigeon-cli/pull/8)).
- ADR-0089: adds `job run dedupe --upload-only` to resume uploading an already-completed local run after a real OOM kill left a 560k-file run stuck mid-upload with no way to resume, and fixes the root causes that let it happen: `upload_if_changed` now streams a plaintext upload straight from disk via `minio`'s `put_object_content` (multipart above 64MiB, removing the prior 5GB single-PUT cap everywhere that shares `upload.rs`) instead of buffering the whole file and cloning it per retry, and `run_dedupe_job` drops its full `ContentIndex`/merge-report/`placed_keys` state before the upload phase runs instead of holding it alive through the whole thing ([#6](https://github.com/noisypigeon/pigeon-cli/pull/6)).
- ADR-0088: defaults `job run dedupe`'s concurrency prompt to the machine's core count instead of the generic I/O-bound default of 4, and moves its SHA-256/zip hash+expand calls onto tokio's blocking-thread pool via `tokio::task::spawn_blocking` (first use of that primitive in the codebase), so a run left at its default actually uses all available cores during the CPU-bound hash phase; also adds opt-in `mise run build-release`/`pigeon-release` tasks ([#5](https://github.com/noisypigeon/pigeon-cli/pull/5)).
- ADR-0087: adds a `Dockerfile` (linux/arm64, debian-slim + ffmpeg) as an alternative way to run `pigeon` without a local Rust toolchain, plus a `PIGEON_SECRET_<ALIAS>` env var fallback in `get_secret` so a container can be used non-interactively despite the Linux kernel-keyutils backend not surviving a restart ([#4](https://github.com/noisypigeon/pigeon-cli/pull/4)).
- ADR-0086: moves the crate from `service/pigeon-cli/` to repo root now that ADR-0084 left this repo holding nothing else; collapses the two `CHANGELOG.md`s/`README.md`s/`.gitignore`s into one each, drops `--manifest-path` from every `.mise.toml` task, rewrites 40 ADRs' + `CLAUDE.md`'s historical path citations to match, and adds an explicit `Cargo.toml` `include` list so `cargo publish` doesn't regress ADR-0050's packaging-scope fix ([#3](https://github.com/noisypigeon/pigeon-cli/pull/3)).
- ADR-0085: fixes `pigeon keyring add`/`modify`/`delete`/`list` failing outright on headless Linux ("No default store has been set") by switching Linux to the kernel-keyutils backend instead of the D-Bus Secret Service backend, which needs a desktop session/daemon a headless server doesn't have ([#2](https://github.com/noisypigeon/pigeon-cli/pull/2)).
- ADR-0084: splits `service/pigeon-cli` out of this monorepo into its own repo, `noisypigeon/pigeon-cli`, via a full-history force-push followed by one ordinary prune commit in each repo -- see the root `/CHANGELOG.md` for the repo-wide record; this crate's changelog continues unaffected in the new repo ([#99](https://github.com/noisypigeon/noisypigeon/pull/99)).
- ADR-0083: implements `pigeon job run sort` -- reuses `commands/job/download.rs`/`upload.rs` directly, with a plain `stream::buffer_unordered` fetch phase (no growable queue, since there's no zip expansion) and a placement pass that always disambiguates a filename collision via `unique_path`, never a hash check ([#98](https://github.com/noisypigeon/noisypigeon/pull/98)).
- ADR-0083: adds `pigeon job run sort`, a bucket-to-bucket job that downloads an input bucket, flattens it into top-level `<extension>/` folders by literal extension, and uploads (mandatory) to an output bucket, never encrypting; designed to run after `dedupe` so a filename collision is always disambiguated via `unique_path` rather than hash-checked, with no `ContentIndex`/`Dedup`/zip-expansion machinery at all ([#96](https://github.com/noisypigeon/noisypigeon-2/pull/96)).
- ADR-0082: implements `pigeon job run dedupe` -- reuses `pull_transform::archive`'s zip expansion directly, hoists `pull-transform`'s disk-space-preflight/streaming-download helpers into a new shared `commands/job/download.rs` (refactoring `pull-transform` to use it), and scopes checkpoint/content-hash bookkeeping and the upload walk so `dedupe-report.txt` can never leak into the destination bucket ([#95](https://github.com/noisypigeon/noisypigeon-2/pull/95)).
- ADR-0082: adds `pigeon job run dedupe`, a bucket-to-bucket job that recursively scans a source bucket, always inflates zips (containers never uploaded, only their inflated contents), content-hashes everything bucket-wide to keep one byte-identical copy of each file, writes a human-readable merge report, and optionally uploads unencrypted to a different bucket-config; hoists `pull-transform`'s disk-space-preflight/streaming-download helpers into a new shared `commands/job/download.rs` ([#94](https://github.com/noisypigeon/noisypigeon/pull/94)).

- ADR-0081: implements `pigeon job run email-pull` -- fetches raw `.eml` files straight to their final location and extracts attachments into a deduplicated `attachments/` folder, with no Markdown/frontmatter transform and no encryption support, optionally uploading the result unencrypted to a bucket-config ([#93](https://github.com/noisypigeon/noisypigeon/pull/93)).
- ADR-0081: adds `pigeon job run email-pull`, a sibling to `email-sync` that pulls raw `.eml` files and unpacked attachments (no Markdown/frontmatter transform), deduplicates attachments only by content hash, never encrypts, and optionally uploads unencrypted to a bucket-config ([#92](https://github.com/noisypigeon/noisypigeon/pull/92)).
- ADR-0080: adds an optional per-identity IMAP connection cap on `Identity` (set via `pigeon keyring add/modify email`), overriding `job run email-sync`'s job-wide `--max-connections-per-identity` default for that identity only; routes `transform.rs`'s four lenient-skip warnings through `tracing::warn!` instead of `eprintln!`, so they now reach the JSONL observability log ([#87](https://github.com/noisypigeon/noisypigeon/pull/87)).
- ADR-0078: formalizes a repeatable procedure for analyzing `pigeon.jsonl` job-run logs (isolating a run, reading the resource-sample stream, cross-checking failure tallies) and packages it as the `.claude/skills/analyze-job-run` skill -- no source changes ([#85](https://github.com/noisypigeon/pigeon/pull/85)).
- ADR-0077: adds `--file-types`, `--expand-zips`, and `--image-format`/`--video-format`/`--audio-format` to `pull-transform`, letting a run filter which extensions get pulled/transformed/uploaded, opt individual zips out of expansion (upload as-is instead), and adapt the media-transcoding mapping from a small vetted per-category menu ([#84](https://github.com/noisypigeon/pigeon/pull/84)).
- ADR-0076: streams `pull-transform` downloads and zip expansion straight to disk instead of buffering whole objects/zip contents in memory, root-causing and fixing a real SIGKILL crash against 50-100GB zip archives; adds a live disk-space preflight check ([#83](https://github.com/noisypigeon/pigeon/pull/83)).
- ADR-0052: decides how to merge the separate `pigeon-do` repo's full history into this repo as `terraform/infrastructure/*`, renumbering its 12 ADRs to 0053-0064 and rewriting its module sources to point at `terraform/modules/` (documents the decision; the user performs the actual merge manually) ([#62](https://github.com/noisypigeon/pigeon/pull/62)).
- ADR-0065: fixes a `job run email-sync` crash root-caused to `imap-proto` only recognizing `MESSAGE`/`RFC822` in `BODYSTRUCTURE` -- `pull_manifest` now bisects a failing UID batch to isolate the specific unparseable message(s) with a placeholder, instead of losing the whole mailbox's manifest data ([#65](https://github.com/noisypigeon/pigeon/pull/65)).
- ADR-0068: fixes three IMAP `LOGOUT` calls that treated a logout failure as fatal even after the real work already succeeded -- `gather_pending` was discarding the whole identity's already-gathered manifest data on a logout-time disconnect; now best-effort, matching `worker.rs`'s existing pattern ([#67](https://github.com/noisypigeon/pigeon/pull/67)).
- ADR-0071: fixes a `job run email-sync` connection storm at high `--concurrency` against multiple identities ("Too many simultaneous connections") with a new per-identity connection cap (`--max-connections-per-identity`, default 6), retries a transiently-failed batch once before giving up, and collapses per-UID "missing file" warning spam into one line per batch ([#76](https://github.com/noisypigeon/pigeon/pull/76)).
- ADR-0073: adds cross-cutting observability -- structured `tracing` spans/events carrying identity/mailbox/uid/step/attempt context, a durable JSON-formatted `pigeon.jsonl` log (`--log-file`/`--log-level`), CPU/memory/disk telemetry, and a panic hook, all wired uniformly through one `Observable` trait and `run_instrumented` harness reused by every current and future command ([#80](https://github.com/noisypigeon/noisypigeon/pull/80)).
- ADR-0074: adds `pigeon job run pull-transform` -- recursively pulls a bucket-config, expands zips in-memory, recodes photos/screenshots/video/audio to a size-optimized canonical format via `ffmpeg`/`ffprobe` with verify+retry+fallback-to-original, dates media (EXIF) and documents (PDF/OOXML metadata + text scan), dedups by SHA-256, organizes by extension, and optionally encrypts and uploads to a bucket-config ([#81](https://github.com/noisypigeon/noisypigeon/pull/81)).
- ADR-0075 (amends ADR-0074): adds progress visibility to `pull-transform` -- a run against a real, large bucket looked hung after the final confirmation, since the whole download/recode/verify phase and placement pass had no progress bar or console output at all; now both phases show a live bar, plus named call-outs for large downloads and video/audio recodes specifically ([#82](https://github.com/noisypigeon/noisypigeon/pull/82)).

## [0.2.1] - 2026-09-25

- ADR-0051: GitHub repo renamed `pigeon-cli` → `pigeon`; corrects `Cargo.toml`'s `repository` field and every in-repo GitHub link, bumps to `0.2.1` in prep for the next publish ([#60](https://github.com/noisypigeon/pigeon/pull/60)).

- ADR-0029: Every substantive change now lands via branch → PR → local `mise run ci` gate → auto-merge, with a changelog entry per PR ([#1](https://github.com/noisypigeon/pigeon/pull/1)).
- ADR-0030: Root-causes silent, total attachment-upload loss to a wrong path reconstruction in `run_dedup_pass` (documents investigation and fix; fix not yet implemented) ([#2](https://github.com/noisypigeon/pigeon/pull/2)).
- ADR-0030: Implements the attachment-placement fix -- `email-sync` attachments are now correctly found, deduped, and uploaded instead of silently lost ([#3](https://github.com/noisypigeon/pigeon/pull/3)).
- ADR-0030: Amends the investigation -- real post-fix runs still lost attachments due to a second, deeper bug (attachment placement only runs for messages canonicalized in the same call), plus a separate malformed-source-message finding (documents both; fix not yet implemented) ([#4](https://github.com/noisypigeon/pigeon/pull/4)).
- ADR-0030: Implements both amendment fixes -- attachment placement now resolves each message's canonical path across runs (a re-run genuinely recovers previously-orphaned attachments), and a malformed source message's phantom zero-byte attachment part no longer rejects the whole message ([#5](https://github.com/noisypigeon/pigeon/pull/5)).
- ADR-0031: Adds `mise run adr-issue` and category labels to file/link GitHub issues for genuinely-deferred ADR Out of scope items, and backfills 33 issues across ADR-0003–0030's still-open items ([#39](https://github.com/noisypigeon/pigeon/pull/39)).
- ADR-0032: Documents adding a progress bar to `job run email-sync`'s previously-silent manifest-gathering phase, and an `ATTACHMENTS` column (an IMAP `BODYSTRUCTURE`-derived estimate) to the wizard's pre-run summary table (documents the decision; implementation not yet done) ([#43](https://github.com/noisypigeon/pigeon/pull/43)).
- ADR-0032: Implements the decision -- the manifest-gathering phase now shows a per-identity connect line and mailbox-scoped progress bar, and the summary table gains its `ATTACHMENTS` column ([#44](https://github.com/noisypigeon/pigeon/pull/44)).
- ADR-0033: Documents closing seven backlog issues raised while investigating an attachments-still-not-working report (both ADR-0030 fixes confirmed correctly present): a dedup-phase progress bar with suspend-safe warnings, structured per-category failure reporting, a narrower manifest attachment estimate shown next to the real per-run count, bounded progress-bar prefix width, and control-character escaping in frontmatter YAML values (documents the decision; implementation not yet done) ([#47](https://github.com/noisypigeon/pigeon/pull/47)).
- ADR-0033: Implements the decision -- the dedup pass now shows a progress bar with a suspend-safe warning, `email-sync`'s failure count breaks down by cause (connect/examine/batch-error/verification/parse-skipped), the manifest attachment estimate also counts a `Content-Type` `name` param and is shown next to the real per-run staged count, every progress-bar prefix is bounded to 24 characters, and `yaml_quote` escapes embedded control characters ([#47](https://github.com/noisypigeon/pigeon/pull/47)).
- ADR-0034: Root-causes the manifest `ATTACHMENTS` estimate being stuck at zero for every identity to `gather_pending`'s persisted-manifest reuse fast-path, which caches a UID's attachment count indefinitely and never invalidates it; decides to drop the reuse fast-path and always re-pull `BODYSTRUCTURE` fresh (documents the decision; implementation not yet done) ([#48](https://github.com/noisypigeon/pigeon/pull/48)).
- ADR-0034: Implements the decision -- `gather_pending` always re-pulls a fresh size/attachment-count manifest for pending UIDs instead of reusing a persisted `.manifest` entry, so the `ATTACHMENTS` estimate can no longer get stuck stale; `.manifest` is now a write-only snapshot, and the now-unused `load_manifest` is removed ([#51](https://github.com/noisypigeon/pigeon/pull/51)).
- ADR-0035: Root-causes `job run email-sync` prompting for the macOS keychain password 5-10 times per run to one distinct keychain item per identity/bucket-config/encryption-key alias, each needing its own OS-level access grant; decides that, on macOS, writing a secret should pre-authorize the running binary via `security add-generic-password -T` so future reads never prompt (documents the decision; implementation not yet done) ([#52](https://github.com/noisypigeon/pigeon/pull/52)).
- ADR-0035: Amended and **rejected** -- a live implementation attempt showed the `-T` grant doesn't suppress repeat Keychain prompts, since `/usr/bin/security` (not `pigeon`) is the process actually requesting access once reads are shelled out; no code landed, problem remains open for a future attempt ([#53](https://github.com/noisypigeon/pigeon/pull/53)).
- ADR-0036: Documents restructuring the repo for a future monorepo -- `src/`/`tests/`/`scripts/` move to `service/pigeon-cli/src/`, `service/pigeon-cli/tests/`, and `.github/scripts/`; `Cargo.toml` stays at repo root with updated `[lib]`/`[[bin]]`/`[[test]]` paths; every existing ADR's historical path citations get rewritten to match (documents the decision; implementation not yet done) ([#54](https://github.com/noisypigeon/pigeon/pull/54)).
- ADR-0036: Amended to also relocate `CHANGELOG.md` (this file) to `service/pigeon-cli/CHANGELOG.md`, and implements the restructure -- `src/`, `tests/`, `scripts/`, and `CHANGELOG.md` move via `git mv`; `Cargo.toml` gains explicit `[lib]`/`[[bin]]`/`[[test]]` paths; `.mise.toml` and `CLAUDE.md`'s Dev cycle instructions updated to match. The historical-citation rewrite across existing ADRs is a separate follow-up PR ([#55](https://github.com/noisypigeon/pigeon/pull/55)).
- ADR-0036: Rewrites every existing ADR's historical `src/`/`tests/`/`scripts/`/`CHANGELOG.md` path citations to match the restructure, occurrence-by-occurrence rather than a blind find-and-replace (idiomatic prose and a vendored crate's own internal path were confirmed false positives and left untouched) ([#56](https://github.com/noisypigeon/pigeon/pull/56)).
- ADR-0037: Merges the separate `pigeon-tf` repo's full history into this repo as `terraform/modules/{scaleway,digitalocean}/*` (all 20 release tags preserved via a `git filter-repo` rewrite), renumbers its 12 ADRs to 0038-0049, and consolidates each of the 9 modules' changelogs ([#57](https://github.com/noisypigeon/pigeon/pull/57)).
- ADR-0037: Collapses each module's `CHANGELOG.md` back to one entry after the retargeted `module-release.yml` fired on PR #57 itself and prepended a duplicate, generic auto-generated entry on top of the hand-written consolidation ([#58](https://github.com/noisypigeon/pigeon/pull/58)).
- ADR-0050: Relocates `Cargo.toml`/`Cargo.lock` into `service/pigeon-cli/` (mise stays the entry point via `--manifest-path`), fixing `cargo package`'s scope to just this crate instead of the whole repo ([#59](https://github.com/noisypigeon/pigeon/pull/59)).

## [0.2.0] - 2026-09-25

Unstable. Introduces a trait-based core architecture, a unified keyring, and end-to-end client-side encryption for `email-sync` uploads.

- ADR-0028: `pigeon job run decrypt-files` decrypts `*.enc` files from a local input directory into an output directory using a configured encryption key — `core::job::Job`'s second real implementor, alongside `email-sync`.
- ADR-0027: An encryption key can default to a bucket-config (chosen by alias in `keyring add/modify bucket`) and still be overridden per run via `--encryption-key` or interactively; non-interactive runs now pick up that default automatically instead of always skipping encryption.
- ADR-0026: `pigeon keyring add/modify encryption-key` manages symmetric encryption keys — generated or imported — in the OS keychain, selectable by alias wherever a key is needed.
- ADR-0025: `email-sync` uploads can be encrypted client-side with AES-256-GCM-SIV and a content-derived deterministic nonce, so identical plaintext still dedups correctly against S3 ETags even when encrypted.
- ADR-0024: The upload phase now runs concurrently across every selected identity (previously sequential), with its own progress bar and per-file retry-with-backoff.
- ADR-0023: Core logic reorganized into trait-based `src/core/` (`Job`, `Transform`, `Dedup`, `KeyringEntry`, `WizardInput`) with concrete implementations moved to `src/commands/`.
- ADR-0022: `pigeon email authenticate` and `pigeon dataops bucket-config` are replaced by one `pigeon keyring add/modify/delete/list`, backed by a single `keyring.toml` and OS-keychain service.
- ADR-0021: `pigeon job run email-sync` replaces `pigeon email sync` with a wizard that resolves identities/concurrency/upload target, batches work across every identity's mailboxes, and shows a pre-run manifest summary.
- ADR-0020: Dedup and transform primitives (`ContentIndex`, filename sanitization, etc.) genericized for reuse beyond email.
- ADR-0019: Sync now runs fetch → transform+dedup → upload as ordered phases with an `.uploaded` checkpoint, instead of uploading inline per message.

## [0.1.0] - 2026-09-23

Initial release; unstable. Scaffolds `pigeon email sync` end to end: IMAP connectivity, Markdown transform, S3-compatible remote storage, and deduplication.

- ADR-0018: Published to crates.io as `pigeon-cli`.
- ADR-0017: `pigeon remote` renamed to `pigeon dataops`.
- ADR-0016: Fixed a macOS keychain ACL bug that lost stored credentials across rebuilds; CLI cleanup (`--local-output`, `--remote-output`, `list`).
- ADR-0015: Progress bars made safe under concurrent mailbox processing.
- ADR-0014: Mailboxes fetched concurrently, one IMAP session per worker.
- ADR-0013: Per-mailbox progress reporting; sender-controlled attachment names sanitized against path-separator crashes.
- ADR-0012: Byte-identical messages and attachments deduplicated on output (content-hashed, merged via `also-in:` frontmatter).
- ADR-0011: `email sync` can upload its output to a configured remote, with MD5/ETag-based skip-if-unchanged.
- ADR-0010: Remote configuration simplified (`alias`, reordered prompts, a `bucket_exists` check replacing an unreliable list-buckets probe).
- ADR-0009: `pigeon remote` adds rclone-style S3-compatible storage (`configure`/`list-buckets`/`ls`/`lsd`/`copy`).
- ADR-0008: Email-specific code grouped under `src/email/` ahead of a second command group.
- ADR-0007: `sink` and `transform` merged into one `pigeon email sync` command, with `--debug` to isolate either phase.
- ADR-0006: `pigeon email transform` converts `.eml` to flat, frontmattered Markdown via `mail-parser`/`htmd`.
- ADR-0005: `pigeon email sink` downloads mail read-only via IMAP with UID-based resume.
- ADR-0004: `mise run pigeon --` runs the built binary.
- ADR-0003: IMAP auth via per-provider app/bridge passwords (not OAuth2), credentials stored in the OS keychain.
- ADR-0002: CLI scaffolded (clap, Mise-pinned toolchain, stub commands).
- ADR-0001: Initial design for authenticating, sinking, and transforming mail from Gmail, Fastmail, iCloud, and Proton.