* fix(apps): detect the no-database failure by code or message
The recovery flow for "db command against an app that has no database"
keyed on one business code (500002759). The server has since renumbered
that case to 400002465, which silently disabled the flow: users now see
the raw internal message about workspace / app-id mapping and lose the
cloud-development recovery steps entirely.
Nothing catches the regression. There is no compile error, the unit tests
compare against the same constant they set, and the dry-run E2E does not
exercise a real response — the failure only shows up against a server that
has already renumbered.
Detect on code OR message instead. Both known codes are kept, plus narrow
lowercase markers of the server's internal wording. The two channels have
opposite failure modes: a code is precise but gets renumbered, a message
survives renumbering but breaks on rewording or localization. Requiring
either to match means one channel changing degrades nothing, and only a
simultaneous change of both regresses.
Markers stay deliberately narrow. "no db branch" in particular must not
also swallow env-pull's "invalid db branch" case, which needs its own
hint; a comment records that widening them requires a test proving the
neighbours still pass through.
Classification and the cause chain are untouched: the helper still mutates
the problem in place and returns the same error value.
* test(apps): assert the full typed-error contract in no-database cases
Review feedback: the new subtests checked only Message and Hint, so a
change that reclassified the failure — or replaced the error value and
dropped the cause chain — would still have passed.
Each case now asserts Category, Subtype and Code are untouched by the
rewrite, and that the helper returns the same error value. Inputs use a
concrete subtype rather than Unknown, so a clobbered classification is
actually observable. One new case wraps a cause and asserts errors.Is
still finds it through the rewrite.
Also covers the predicate's defensive nil guard, which withAppsHint cannot
reach on its own (ProblemOf returns ok=false for untyped errors), closing
the two uncovered lines the coverage report flagged. Both withAppsHint and
isAppNoDatabaseError are now at 100%.
* fix(errclass): classify the db-domain business codes
Three codes reaching the Apps db commands were absent from the Spark
table, so BuildAPIError fell through to the CategoryAPI + SubtypeUnknown
catch-all and the envelope carried no usable classification.
"App has no database yet" registers as Validation / FailedPrecondition:
the app resolves fine and the request is well-formed, but a prerequisite
the caller must create first is missing, so retrying unchanged can never
succeed. This moves its exit code from 1 to 2 — "fix the state" rather
than "the call failed" — and a test pins that so a future
reclassification has to be deliberate. Two codes cover it because the
server renumbered the case into the 4xx band; the legacy one stays for
older servers.
"Table does not exist" registers as API / NotFound, an ordinary
missing-resource lookup with no exit-code change. SubtypeNotFound has no
APIHint default, which matters here: the Apps layer fills its
command-scoped hint only when the classifier left Hint empty, so a
context-free default would displace the more actionable one. A test
guards that too.
Failures from the minutes and video-conference commands now surface as
structured, typed errors carrying a stable category and subtype — spanning
input validation, missing permissions, network and file-I/O failures, and
remote API errors — so callers can branch on the error kind instead of
parsing free-form text. Batch commands report partial failures explicitly,
emitting per-item results with a non-zero exit instead of masking them.
Calendar commands now return structured, typed error envelopes for every
failure mode — input validation, internal faults, and API responses —
instead of legacy generic errors. Callers and AI agents get consistent
exit codes and a machine-readable shape (type / subtype / code / hint),
and can tell bad input, an internal fault, and an API rejection apart.
Validation errors are attributed to the offending flag.
Server-supplied error details (e.g. why an event time was rejected) are
surfaced on the typed error's hint via a shared classifier improvement
that benefits every domain. Multi-step operations (create-with-attendees
rollback, multi-field update) preserve the real failure's classification
and report which steps completed.
The whole calendar domain is now lint-locked against reintroducing legacy
error constructors.
* feat(mail): return typed error envelopes across the mail domain
Replace every produced error path in shortcuts/mail with typed errs.* envelopes, so consumers get stable category, subtype, param/params, hint, retryable, and log_id metadata for classification and recovery instead of free-form message text.
- Locally constructed mail errors move from output.Err* / output.Errorf / final fmt.Errorf / common legacy helpers to errs.* builders, with structured params on multi-flag validation and failed-precondition states kept non-retryable.
- API-call failures move from runtime.CallAPI / DoAPIJSON legacy boundaries to runtime.CallAPITyped or runtime.ClassifyAPIResponse, and mail-specific enrichers read errs.ProblemOf so typed code, subtype, hint, and log_id metadata are preserved.
- Batch draft-send partial failures now use runtime.OutPartialFailure so successful and failed draft sends stay in stdout while the command exits through a typed multi-status signal.
- Add mail-domain typed helpers, mail API code metadata, and guard wiring to keep shortcuts/mail from reintroducing legacy envelopes or legacy API calls.
- Keep genuine intermediate fmt.Errorf wraps in parser/builder layers annotated with nolint comments; command-facing paths wrap them into typed validation, API, network, or internal errors.
* fix(mail): report aborted draft-send batches as a single failure result
When an account-level failure interrupts a batch send after some drafts
already went out, the command previously produced two machine-readable
failure results: the partial-failure ledger on stdout and a second error
envelope on stderr. Consumers could not tell which one to recover from.
The batch ledger is now the only failure result for that case: it gains
aborted and abort_error fields carrying the typed cause, so callers can
see which drafts were sent, which failed, why the batch stopped, and how
to recover — all from stdout. A --stop-on-error stop keeps these fields
unset because stopping early there is the caller's own choice.
Drive-domain errors now leave the CLI as typed, machine-branchable
envelopes — a stable `type` plus `subtype` and named fields (param,
params, retryable, log_id, hint) — so scripts and AI agents can branch on
structure and act on a recovery hint instead of parsing prose.
Changes:
- Every error produced in the drive domain — validation, file I/O, and the
failures returned from its Lark API calls — is emitted as a typed errs.*
error; the exit code is derived from the error category. Drive's API calls
now go through a shared typed classifier, so failures carry subtype,
troubleshooter, a recovery hint, and the request's log_id whether the
server returns it in the response body or the x-tt-logid header; an
already-typed network/auth error is never downgraded into a generic API
error.
- Known API conditions (resource conflict, cross-tenant, cross-brand, ...)
carry a recovery hint keyed by their error class; a command can refine
that hint with command-specific guidance.
- Batch partial failures (+push / +pull / +sync, where some items succeed
and some fail) now report an honest ok:false multi-status result on
stdout — the summary and every per-item outcome stay machine-readable —
and exit non-zero, instead of a misleading ok:true success envelope.
- Duplicate rel_path conflicts report each colliding path as a structured
params entry (RFC 7807 invalid-params style).
- Static guards lock the drive path so legacy error construction — direct
envelopes or the auto-classifying API helpers — cannot be reintroduced,
making drive the template for the remaining domains.
Output changes worth noting for consumers:
- Error envelopes now carry typed type/subtype and named fields; exit
codes follow the error category (malformed or incomplete API responses
are reported as internal errors rather than generic API errors).
- Batch partial failures (+push / +pull / +sync) emit an ok:false result
envelope on stdout (summary + per-item items[]) and exit non-zero; the
per-item results stay on stdout rather than in a stderr error envelope.
Errors surfaced through shared cross-domain helpers (scope precheck, media
import upload, metadata lookup, save-path resolution) are not yet typed;
they migrate with the shared layer in a follow-up change.
Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
Introduce a typed error contract framework for lark-cli so in-process
Go callers can branch via errors.As(&errs.XxxError{}) and shell scripts,
AI agents, and protocol adapters can branch on stable JSON type/subtype
fields instead of regex-parsing free-form messages.
Adds:
- Canonical taxonomy under errs/ (9 categories + typed Error structs
embedding a shared Problem, RFC 7807-aligned)
- Centralized Lark code metadata + identity-aware BuildAPIError dispatch
- Typed JSON envelope writer alongside the legacy envelope writer
- MCP / OAuth (RFC 6750 Bearer) projection adapters
- Five CI lint guards preventing ad-hoc taxonomy drift
Backward compatibility: legacy *output.ExitError producers (ErrAPI,
ErrWithHint, Errorf, ErrBare) and business shortcuts that use them
continue to render the legacy envelope unchanged. SecurityPolicyError
wire format and exit code are preserved via a carve-out; taxonomy
migration is deferred to PR 2. Domain-specific business migration is
staged across PR 3+.
Framework-direct paths now return typed *errs.*Error: ErrAuth /
ErrValidation / ErrNetwork emit category literals on the wire
(authentication / validation / network), *core.ConfigError is promoted
at the cmd/root boundary with exit code aligned from 2 to 3, and Lark
API permission denials classified by BuildAPIError exit 3.
At the SDK boundary, WrapDoAPIError preserves any already-classified
error (legacy *output.ExitError or typed *errs.*) so output.ErrAuth
from missing credentials surfaces with the auth category and exit 3
intact instead of being downgraded to a network error. Policy responses
classified by BuildAPIError (codes 21000 / 21001) extract challenge_url
and the canonical hint from the response body, matching what the
auth transport already surfaces at the HTTP layer; non-https
challenge URLs are dropped.
First PR in the feat/error-contract-* series.