Skip to content

fix!: dependency floors that let dg update skip this release, and exit-code + error-stream correctness - #102

Merged
GregHolmes merged 5 commits into
mainfrom
fix/release-dependency-floors
Aug 19, 2026
Merged

fix!: dependency floors that let dg update skip this release, and exit-code + error-stream correctness#102
GregHolmes merged 5 commits into
mainfrom
fix/release-dependency-floors

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Blocks #100 — merge this first, then let release-please regenerate the release PR as 0.3.0.

What this fixes

1. dg update on pip silently doesn't deliver the release (root pyproject.toml).
Root declared floors as low as >=0.0.1, and pip's default only-if-needed upgrade strategy leaves any sub-package whose installed version already satisfies its floor. Measured from published deepctl==0.2.26 with the release wheels available: only 4 of 17 released packages upgraded; dg --version reported the new number while the keys fixes — including the -o json fix that headlines the release — never arrived. (uv resolves fresh and is unaffected, so the same dg update lands two users in different states.) Floors now match the versions being published.

2. Eight packages can import a symbol their declared core floor doesn't guarantee (packages/*/pyproject.toml).
deepctl-cmd-{billing,keys,members,models,projects,read,requests,usage} import get_status_console (new in core 0.2.16) at module scope while declaring deepctl-core>=0.1.10. PyPI's latest published core is 0.2.14, so pip install --upgrade deepctl-cmd-keys alone reproduces a broken CLI: the command vanishes and the ImportError prints to stdout. Same class of hand-bump as #92; release-please has no cross-package dependency automation, so these floors are hand-maintained.

3. A crash exited 2 — the code reserved for user interrupt (src/deepctl/main.py).
Per the published contract (0 = success, 1 = error, 2 = user interrupt), main()'s generic exception handler now exits 1. Because cli() runs with standalone_mode=False, Click usage errors (bad flag, unknown command, bare dg) propagate to this same handler and move from 2 to 1 as well — consistent with the contract, which reserves 2 for interrupt. Also repairs three tests whose patch.object(cli, "__call__", ...) was inert (dunder lookup bypasses instance attributes) and adds a usage-error exit-code test.

4. …but that alone demoted a real Ctrl-C to 1 (src/deepctl/main.py).
With standalone_mode=False, Click catches a KeyboardInterrupt raised during command execution and re-raises it as click.exceptions.Abort — a RuntimeError subclass, not a KeyboardInterrupt. In this repo the path is more direct still: BaseCommand catches the interrupt itself and raises click.Abort(). So a mid-command Ctrl-C — the common case — bypassed the KeyboardInterrupt handler entirely and landed in the generic handler that 3 just changed, exiting 1 and printing an empty Error: (str(Abort()) is ""). Before 3 that path exited 2 correctly by accident. Abort is now caught alongside KeyboardInterrupt, so user cancellation (Ctrl-C, Ctrl-D at a prompt) always exits 2, with a test on the Abort delivery path.

5. Root diagnostics printed to stdout, corrupting -o json (src/deepctl/main.py).
main()'s module-level console was a plain rich Console(), which writes to stdout, and both of its handlers print through it — so a crash, bad flag, unknown command, or bare dg wrote human-readable prose to stdout. dg -o json not-a-command put Error: No such command ... on stdout and left stderr empty, so anything piping stdout into jq parsed the error text instead of JSON. This is the root-handler half of the #97 sweep: that issue's scope covered moving errors to a stderr console "so stdout stays clean", and #101 closed the sweep, but both only ever reached the command layer. Now aliases deepctl_core.output.stderr_console — the same console print_error() writes to, and the pattern deepctl-cmd-mcp already follows — so root-level and command-level diagnostics format identically, including the no-color handling for agentic/CI callers. Exit codes are unchanged and success paths still write their payload to stdout. Adds tests asserting a failing dg -o json ... writes nothing to stdout (unknown-command and bad-flag paths) and that the cancellation notice is on stderr.

Why the BREAKING CHANGE footer

The exit-code enforcement (#101) landed as fix:, so release-please would ship it as patch 0.2.28 with no version signal — and #100 currently confirms that: root reads 0.2.28 and its diff contains no ⚠ BREAKING CHANGES section at all. There is no machine-readable breaking marker anywhere in the cycle; the behavior change exists only as hand-written prose in 21b8333 / e327a5e. The break belongs to #101's already-merged code, so it cannot come from a conventional-commit type on this PR's own diff — it has to be injected where the version arithmetic can see it. With bump-minor-pre-major, that makes root 0.3.0.

The signal is deliberately stated in three places, because which one release-please actually reads depends on how this PR is merged:

  • BREAKING CHANGE: footer on 914e132 — the primary. That commit touches only src/deepctl/main.py and its tests, i.e. a root-only path, so the break is attributed to root alone and the eight sub-packages stay patch bumps. This is the one that survives a merge commit, and it is the only variant that produces the intended release shape.
  • fix!: in the title — insurance. This repo's recent PRs were squash-merged, and with squash_merge_commit_title: PR_TITLE / squash_merge_commit_message: PR_BODY a squash discards every commit message, footer included. The ! keeps root at 0.3.0 in that case.
  • BREAKING CHANGE: footer at the foot of this description — so a squash also carries the descriptive text into the ⚠ BREAKING CHANGES section rather than just the subject line.

Trade-off to know before merging: prefer a merge commit. A squash collapses all five commits into one that touches root and the eight packages/deepctl-cmd-*/pyproject.toml files, so the break gets attributed to those eight paths too — they would take minor bumps (keys 0.0.3 → 0.1.0, usage 0.1.13 → 0.2.0, …) with a breaking-change entry about CLI exit codes that has nothing to do with them, and root's floors here (>=0.0.4) would then sit below what actually published, re-seeding the staleness this PR exists to fix. Root reaches 0.3.0 either way; only the sub-package shape differs.

After merge — steps on the regenerated #100

  1. uv lock and commit (version bumps stale the lock; CI runs uv sync --locked — this is the eff5291 wall and recurs every release until release.yml regenerates the lock itself).
  2. Re-apply the behavior-change prose (git show 21b8333 e327a5e), changing 0.2.26 to 0.2.280.2.26 to 0.3.0, and fold in the exit-code and output-stream details from this PR: crashes and usage errors move 2 → 1, 2 stays reserved for user interrupt (Ctrl-C during a command still exits 2), and root error/cancellation output moves from stdout to stderr. Decide there whether the stream move gets its own ⚠ BREAKING CHANGES line or reads as a plain fix — root lands on 0.3.0 either way, so it is a notes-wording call, not a version call.
  3. Verify root reads 0.3.0 across manifest / pyproject.toml / __init__.py / CHANGELOG heading, and the ⚠ BREAKING CHANGES section renders the footer text.

Verification here: full suite 1094 passed / 6 skipped; make check clean (ruff + mypy, 115 files); uv lock --check clean after both floor commits; live probes — --version → 0, bare dg → 1, bad flag → 1, unknown command → 1, mid-command Ctrl-C → 2; dg -o json not-a-command writes 0 bytes to stdout with the error on stderr, while dg -o json models still emits valid JSON on stdout.

🤖 Generated with Claude Code

BREAKING CHANGE: dg now exits non-zero when a command fails: 1 for
errors (including crashes and usage errors), 2 for user interrupt, 0 on
success. Every command previously exited 0 regardless of outcome, so
scripts and CI steps that ignored the exit code will surface failures they
were silently swallowing. No command that succeeds changes its exit code.

…hat import get_status_console

deepctl-cmd-{billing,keys,members,models,projects,read,requests,usage} import
get_status_console at module scope, a symbol added to deepctl-core in 0.2.16,
while declaring deepctl-core>=0.1.10. PyPI's latest published core is 0.2.14
(0.2.15 was tagged in the failed 0.2.27 cycle and never published), so the
declared floor is satisfiable by a core that lacks the symbol.

Reproduced: from published deepctl==0.2.26, `pip install --upgrade
deepctl-cmd-keys` installs keys 0.0.4, leaves core at 0.2.14 (pip's default
only-if-needed strategy), and the keys command then fails to load -- exit 2
with the ImportError printed to stdout. The default `pip install deepctl`
path is unaffected (root's core floor forces 0.2.16); this closes the
sub-package-upgraded-alone path, and the metadata is wrong regardless.

Same class of fix as the hand-bump in #92; release-please has no plugins
configured, so these floors are hand-maintained.
…publishes

Root declared floors as low as >=0.0.1 and >=0.1.10 for packages whose fixes
this release exists to deliver. `dg update` runs `pip install --upgrade
deepctl`, and pip's default only-if-needed strategy leaves any sub-package
whose installed version already satisfies its floor untouched. Measured from
published deepctl==0.2.26 with this release's wheels available: only
deepctl, deepctl-core, deepctl-cmd-listen and deepctl-cmd-speak upgraded --
13 of 17 released packages stayed stale while dg --version reported the new
number. The keys fixes, including the -o json fix that headlines the
release, never arrived. (uv resolves fresh and is unaffected, so the same
dg update lands two users in different states.)

Floors now match the versions release-please is publishing in the current
release PR, so pip is forced to deliver what the changelog advertises.
@GregHolmes GregHolmes mentioned this pull request Aug 19, 2026
main()'s generic exception handler exited 2 -- the code the published
contract reserves for user interrupt -- making an internal crash
indistinguishable from Ctrl-C to any script branching on the exit code.
Because cli() runs with standalone_mode=False, Click usage errors (bad
flag, unknown command, bare `dg`) propagate to this same handler, so they
move from 2 to 1 as well. KeyboardInterrupt keeps 2.

Also repairs the three tests that patched cli.__call__ on the instance:
dunder lookup bypasses instance attributes, so the patches were inert and
the tests exercised the bare-`dg` help path instead of the paths they
named. They now patch deepctl.main.cli, and a new test pins the
usage-error code.

BREAKING CHANGE: `dg` now exits non-zero when a command fails: 1 for
errors (including crashes and usage errors), 2 for user interrupt, 0 on
success. Every command previously exited 0 regardless of outcome, so
scripts and CI steps that ignored the exit code will surface failures they
were silently swallowing. No command that succeeds changes its exit code.
@GregHolmes
GregHolmes force-pushed the fix/release-dependency-floors branch from 8f72d8e to 914e132 Compare August 19, 2026 09:51
With standalone_mode=False, Click catches a KeyboardInterrupt raised
during command execution and re-raises it as Abort (a RuntimeError), so
a mid-command interrupt -- the common case -- bypassed the
KeyboardInterrupt handler and hit the generic error handler, exiting 1
and printing an empty 'Error: '. Catch Abort alongside KeyboardInterrupt
so user cancellation (Ctrl-C, Ctrl-D at a prompt) always exits 2 per the
published contract, and add a test on the Abort delivery path.
main()'s module-level `console` was a plain rich Console(), which writes to
stdout, and both of main()'s handlers print through it. So a crash, a bad
flag, an unknown command, or a bare `dg` wrote human-readable prose to
stdout -- `dg -o json not-a-command` put `Error: No such command ...` on
stdout and left stderr empty, so anything piping stdout into jq parsed the
error text instead of JSON.

This is the root-handler half of the #97 sweep. That issue's scope covered
moving errors to a stderr Console "so stdout stays clean", and #101 closed
the sweep, but both only reached the command layer; main()'s own handlers
were never moved. Reuse deepctl_core.output.stderr_console -- the same
console print_error() writes to, and the pattern deepctl-cmd-mcp already
follows -- so root-level and command-level diagnostics format identically,
including the no-color handling for agentic/CI callers.

Exit codes are unchanged (1 for errors, 2 for interrupt, 0 on success), and
success paths still write their payload to stdout. Adds tests asserting a
failing `dg -o json ...` writes nothing to stdout for both the unknown-
command and bad-flag paths, and that the cancellation notice is on stderr --
the assertion that would have caught this during the original sweep.
@GregHolmes GregHolmes changed the title fix: dependency floors that let dg update skip this release, and crash exit codes fix: dependency floors that let dg update skip this release, and exit-code + error-stream correctness Aug 19, 2026
@GregHolmes GregHolmes changed the title fix: dependency floors that let dg update skip this release, and exit-code + error-stream correctness fix!: dependency floors that let dg update skip this release, and exit-code + error-stream correctness Aug 19, 2026
@GregHolmes
GregHolmes merged commit fd1e8a4 into main Aug 19, 2026
38 checks passed
@GregHolmes
GregHolmes deleted the fix/release-dependency-floors branch August 19, 2026 11:38
GregHolmes added a commit that referenced this pull request Aug 19, 2026
…e publishes

Two edits the release PR needs before it can go green and deliver correctly.

uv.lock: release-please rewrote `version = ...` in 11 pyproject.toml files,
which stales the lock's pinned workspace members. Every test job starts with
`uv sync --locked`, so all 16 failed in ~10s before running a test. This is
the recurring eff5291 wall and it fires on every release until release.yml
regenerates the lock itself.

Root floors: the eight command packages landed on minor bumps rather than the
patches the floors in #102 were computed against, because the #102 merge
commit carried `fix!:` plus the BREAKING CHANGE footer and its first-parent
diff spans all eight package pyproject.toml files -- so release-please
attributed the break to each of them. keys/members/models/read/requests/
billing went to 0.1.0 and projects/usage to 0.2.0, leaving root's floors below
what publishes. Delivery still worked this cycle (each floor sits above the
version currently on PyPI, so pip is forced to upgrade), but it re-seeded
exactly the staleness #102 set out to fix: next cycle a user on 0.1.0 would
satisfy `>=0.0.4` and pip would skip the upgrade. Floors now match the
manifest exactly.

Verified: `uv lock --check` and `uv sync --locked` clean, `make check` clean
(ruff + mypy, 115 files), 1094 passed / 6 skipped, and every root floor equal
to its manifest version for all 17 packages publishing a new version.
GregHolmes added a commit that referenced this pull request Aug 19, 2026
…ullet

Re-applies the prose from 21b8333 and e327a5e, which release-please discarded
when it regenerated this PR, updated for 0.3.0 and for the changes that landed
since:

* The generated `⚠ BREAKING CHANGES` list carried the exit-code paragraph
  twice. The footer exists both on 914e132 and on the #102 merge commit (whose
  body is the PR description, where the footer was repeated as insurance
  against a squash), and both touch root paths, so release-please emitted it
  once per commit. Deduped to one. The eight command packages list it once
  each and are unaffected.
* `### Behavior changes` now covers usage errors exiting 1, interrupt staying
  2, and error/cancellation output moving from stdout to stderr, alongside the
  yaml/csv and `keys` items from the original prose. Framed as additions to
  the breaking entry above rather than restating it.
* `### Previously unreleased` reads 0.2.26 -> 0.3.0, and records that the pip
  floor repair is what makes this release actually arrive.
* deepctl-core keeps its three behavior notes, with the `get_status_console()`
  entry now naming the `>=0.2.16` floor its importers need.

Mirrored into the release PR body so the published notes and the committed
changelog say the same thing.
GregHolmes added a commit that referenced this pull request Aug 19, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.3.0</summary>

## [0.3.0](v0.2.27...v0.3.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** cap mcp &lt;2 (fixes broken dg mcp), commit uv.lock, require
twine &gt;=7 ([#95](#95))
([997cd36](997cd36))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **deps:** raise root dependency floors to the versions this release
publishes
([c0b0023](c0b0023))
* exit 1, not 2, when a command crashes or is misused
([914e132](914e132))
* keep exit 2 when Ctrl-C interrupts a running command
([b0e80e2](b0e80e2))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
* **release:** bump pypi-publish action to v1.14.2 for Metadata-Version
2.5 ([#94](#94))
([582cd83](582cd83))
* send root error and interrupt output to stderr, not stdout
([f4b7c48](f4b7c48))
* **web:** repair broken Heap snippet, upgrade astro 6→7, clear all 20
npm alerts ([#96](#96))
([11928fe](11928fe))


### Behavior changes

Alongside the exit-code change above, upgrading to 0.3.0 changes these:

* The full exit-code contract is now enforced end to end: `0` = success,
`1` = error, `2` = user interrupt. Crashes **and usage errors** (bad
flag, unknown command, bare `dg`) exit `1`; `2` is reserved for
cancellation, so Ctrl-C during a running command and Ctrl-D at a prompt
both still exit `2`.
* Error and cancellation messages are written to **stderr** instead of
stdout. `dg -o json …` therefore keeps stdout machine-readable when a
command fails — previously a failure printed `Error: …` prose to stdout,
so a script piping stdout into `jq` parsed the error text instead of
JSON. Successful commands still write their payload to stdout.
* `-o yaml` and `-o csv` no longer drop square-bracketed text from
values. Output was passed through a renderer that read `[...]` as style
markup and deleted it, so an API key comment of `[ci] runner` was
emitted as `runner`. Long values are also no longer hard-wrapped
mid-field.
* `dg keys --delete KEY_ID` now asks for confirmation on stderr instead
of always reporting `Cancelled by user` without deleting. In a
non-interactive context it exits `1` and tells you to pass `--yes`.
* `dg keys --create --dry-run` now reports what it would create. It
previously failed with an internal `TypeError`.

### Previously unreleased

0.2.27 was tagged on 2026-08-17 but never reached PyPI — its publish
step failed with `InvalidDistribution: Invalid distribution metadata:
'2.5' is not a valid metadata version`, which
[#94](#94) and
[#95](#95) then fixed. PyPI
therefore goes straight from 0.2.26 to 0.3.0, and this release is the
first published build to include the 0.2.27 changes:

* SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals
([#92](#92))
([50d96cf](50d96cf))
* **speak:** default to Flux TTS (`flux-alexis-en`) instead of Aura 2
([#89](#89))
([5a0b698](5a0b698)).
This changes the default model for `dg speak`, so synthesised audio
differs unless you pass an `aura-*` model explicitly.
* **mcp:** swallow broken/closed-pipe on dg mcp startup notifications
and error path ([#88](#88))
([b24396e](b24396e))

Six packages tagged in that cycle also reach PyPI for the first time
here: `deepctl-cmd-listen` 0.0.14, `deepctl-cmd-login` 0.1.17,
`deepctl-cmd-skills` 0.0.7, `deepctl-cmd-speak` 0.0.4,
`deepctl-cmd-update` 0.2.6 and `deepctl-telemetry` 0.0.6.

Because 0.2.27 never published, `dg update` on pip also had to be
repaired for this release to arrive at all: root's inter-package
dependency floors were lower than the versions being published, so pip's
default `only-if-needed` strategy left most sub-packages stale and `dg
--version` reported the new number while the fixes never landed. Floors
now match the published versions exactly.
</details>

<details><summary>deepctl-core: 0.2.16</summary>

##
[0.2.16](deepctl-core-v0.2.15...deepctl-core-v0.2.16)
(2026-08-19)


### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))


### Behavior changes

* Commands now map their result status to a process exit code (`error` →
`1`, `cancelled` → `2`, otherwise `0`), and
`BaseCommand.exit_code_for()` exposes that mapping. Exit codes were
previously discarded, so every command exited `0`.
* `-o yaml` and `-o csv` payloads are written verbatim; the renderer no
longer interprets `[...]` as markup or wraps long values.
* New `get_status_console()` returns the shared stderr console for
status output. Commands should use it instead of declaring their own.
Packages that import it require `deepctl-core>=0.2.16`.
</details>

<details><summary>deepctl-cmd-projects: 0.2.0</summary>

##
[0.2.0](deepctl-cmd-projects-v0.1.13...deepctl-cmd-projects-v0.2.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-usage: 0.2.0</summary>

##
[0.2.0](deepctl-cmd-usage-v0.1.13...deepctl-cmd-usage-v0.2.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-mcp: 0.1.15</summary>

##
[0.1.15](deepctl-cmd-mcp-v0.1.14...deepctl-cmd-mcp-v0.1.15)
(2026-08-19)


### Bug Fixes

* **deps:** cap mcp &lt;2 (fixes broken dg mcp), commit uv.lock, require
twine &gt;=7 ([#95](#95))
([997cd36](997cd36))
</details>

<details><summary>deepctl-cmd-models: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-models-v0.0.2...deepctl-cmd-models-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-keys: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-keys-v0.0.3...deepctl-cmd-keys-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-read: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-read-v0.0.2...deepctl-cmd-read-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-requests: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-requests-v0.0.2...deepctl-cmd-requests-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-billing: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-billing-v0.0.2...deepctl-cmd-billing-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-members: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-members-v0.0.3...deepctl-cmd-members-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
GregHolmes added a commit that referenced this pull request Aug 19, 2026
Review of #103 found the guard trustworthy for the case it was written for
and quietly wrong outside it.

Rule 3 (new): root's dependency list must cover every published package.
Rule 1 only validated the floors already listed, so a package that
release-please versions and publishes but that nobody added to root's
dependencies was invisible -- `pip install --upgrade deepctl` never installs
it at all. That is the same delivery gap #100/#102 were about, through the
one door the guard left open, and the repo adds command packages regularly.
NOT_SHIPPED carries the two deliberate exclusions so the intent is stated in
the diff rather than inferred from an omission.

--fix no longer reports success after failing. The rewrite matched the
literal `"name>=X.Y.Z"` including both quotes, so it silently no-opped on any
spec with an upper bound, extra, or environment marker -- and the fix branch
never recorded the miss, so the script printed "dependency floors OK" and
exited 0 on a file it had not touched. It now rewrites the version inside the
matched spec (preserving the rest) and falls through to `problems` when it
cannot, which also puts the previously-unused third element of the floors()
tuple to work. Rule 3 backstops this: a spec form the regex cannot parse at
all now surfaces as a missing root dependency instead of being skipped.

vkey() no longer dies on PEP 440 suffixes. A single hand-set 0.4.0rc1
anywhere in the workspace turned `make floors-check` into a bare ValueError
traceback naming no package.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant