Skip to content

v0.17 — contract hardening (exit 64 + piped JSON errors)#17

Merged
wavyx merged 4 commits into
mainfrom
feat/v0.17-contract-hardening
Jun 11, 2026
Merged

v0.17 — contract hardening (exit 64 + piped JSON errors)#17
wavyx merged 4 commits into
mainfrom
feat/v0.17-contract-hardening

Conversation

@wavyx

@wavyx wavyx commented Jun 11, 2026

Copy link
Copy Markdown
Owner

v0.17 — contract hardening

The last changes to the machine-facing surface (exit codes + error output) before the 1.0 stability freeze.

Changed (BREAKING)

  • Usage errors exit 64, not 70. oclif parse errors (unknown flag, missing arg, bad value) were falling through to 70 (EX_SOFTWARE); they now exit 64 (EX_USAGE). 70 is reserved for genuine internal failures.
  • Piped errors emit JSON. Error output now follows the same format resolution as success: human in a TTY, but a {error, message, exitCode, …} envelope on stderr for any machine format (--output json|yaml|csv, a non-table default, or piped). A non-interactive consumer always gets a parseable failure. --output table forces human.

Fixed

  • OAuth token expiring during a 429 backoff is now refreshed (was gated on the first attempt; a rate-limit retry consumed the window). Refresh is one free round, independent of the retry budget — works under --no-retry.
  • --field/CSV: a field def missing field_name no longer throws when matched by hash code.
  • Error reporting no longer crashes if reading the profile default_output throws (corrupt config) while formatting another error.

Quality

  • Live-verified on the sandbox: --bogusflag → JSON error exitCode: 64; piped 404 → full JSON envelope; success output intact.
  • Contrarian review (4 dimensions, adversarial verify): added yaml/csv default-output error tests, the retry=false+OAuth-refresh lock, and the config-read guard. Dismissed a proposed attempts=maxAttempts change that would have reintroduced the pre-0.17 "refresh-but-never-retry" bug.
  • Tests now model an interactive TTY by default (test/setup.js); piped behavior is opted into per-test.

100% coverage; npm run lint clean.

wavyx added 4 commits June 11, 2026 14:14
- client: the OAuth refresh was gated on attempts===1, so a 429 backoff
  consumed the slot and a token that expired during the storm never refreshed
  (unrecoverable 401 mid-batch). Gate on a dedicated `refreshed` flag and make
  the refresh round free (attempts--), so it neither double-refreshes nor eats
  the retry budget.
- input: findField read d.field_name.toLowerCase() unguarded; add ?. to match
  lookup.js / upsert.js so a def with no field_name degrades instead of
  throwing.
Two breaking changes to the machine-facing contract, landed before the 1.0
freeze:

- oclif parse/usage errors (unknown flag, missing arg, bad enum) exited 70
  (EX_SOFTWARE, an internal bug) — they now exit 64 (EX_USAGE) per spec §9.
  Detected by oclif.exit===2 with no exitCode of ours; genuine internal throws
  still exit 70.
- Error output now mirrors success output: explicit --output, else profile
  default, else JSON when piped / human in a TTY. Previously errors stayed
  human-text unless JSON was explicitly requested, so a piped consumer got JSON
  on success and unparseable text on failure. Now stderr carries the
  {error, message, exitCode, ...} envelope whenever success would be JSON.

BREAKING CHANGE: piped invocations now emit JSON error payloads on stderr (was
human text); oclif usage errors exit 64 instead of 70.

Tests now model an interactive TTY by default (test/setup.js); piped/machine
behavior is opted into per-test. This keeps the existing rejects.toThrow(msg)
error assertions valid under the new piped-JSON behavior.
- base-command: storedDefaultOutput() now swallows a throwing config read
  (returns undefined). handleError consults it while reporting another
  failure, so a corrupt/unreadable config must never crash error reporting
  itself.
- errors: clarify that any non-table format (json/yaml/csv or piped) emits the
  JSON error envelope — there is no yaml/csv error serializer; only an
  interactive table context stays human. Added tests for yaml/csv defaults.
- client: lock the retry=false + OAuth 401 path — a refresh still happens once
  and retries with the fresh token even under --no-retry (the refresh round is
  free; it is re-auth, not a load retry).
- test setup: restore process.stdout.isTTY after each test to prevent leakage.

Dismissed the suggested attempts=maxAttempts change: it would reintroduce the
pre-v0.17 bug where retry=false + 401 refreshed but never retried (threw 69).
CHANGELOG 0.17.0 with the two BREAKING notes (usage errors exit 64; piped
errors emit JSON) + the OAuth/null-guard/error-reporting fixes. agents page:
corrected exit-code table (usage/bad-flags = 64, not 70) and the error-JSON
note now covers piped + yaml/csv defaults, not just --output json. Version bump
to 0.17.0; regenerated command reference + cli-stats.
@wavyx wavyx merged commit d1c4686 into main Jun 11, 2026
11 checks passed
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@wavyx wavyx deleted the feat/v0.17-contract-hardening branch June 16, 2026 12:53
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.

2 participants