feat: errors that teach — every failure names its own fix (COR-13588) - #9
feat: errors that teach — every failure names its own fix (COR-13588)#9Bradenream wants to merge 4 commits into
Conversation
For both humans and AI coding agents, the error text is the documentation they actually read. Three request-lifecycle hooks (all in the non-generated hooks extension point): - No token configured: fail before the network call with the exact fix (Settings → Access tokens, export VF_TOKEN=vfp_...) and a docs_url. --dry-run stays usable without credentials. - Malformed token: warn once on stderr for legacy VF.DM. Dialog Manager keys and whitespace-wrapped tokens, without blocking the request. - 4XX responses: inject actionable hints (401 token-expiry renewal path, 403 acts-as-you semantics, 404 verified 'vf <group> list' commands, 429 Retry-After, 400/422 --dry-run guidance) and a docs_url derived from the operation's verified docs command-group page. Keys already in the body — including future server-provided hints — are never overwritten. Covered by hermetic tests against a local mock API.
There was a problem hiding this comment.
Pull request overview
Adds a non-generated SDK hook (“errors that teach”) to improve CLI error ergonomics by failing early when no token is configured and enriching 4XX responses with actionable hints and docs_url, plus a hermetic vitest suite covering the new behaviors.
Changes:
- Introduces
teachHookto preflight missing credentials, warn on recognizable malformed tokens, and non-destructively injecthints/docs_urlinto JSON error bodies. - Registers the new hook in the SDK hook lifecycle (before-request + after-error).
- Adds hermetic vitest coverage for no-token preflight, malformed-token warnings, and representative 4XX hint injection behaviors.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| test/errors-teach.test.ts | Adds hermetic vitest coverage for preflight/warnings and 4XX enrichment behaviors. |
| internal/sdk/sdkinternal/hooks/teach.go | Implements the new before-request and after-error “teaching” behavior and docs/hints mapping. |
| internal/sdk/sdkinternal/hooks/registration.go | Registers teachHook for request lifecycle events. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
BuildRequest already validates required flags, but its nullable-body
relaxation also fired for params-only commands (bodyFieldPath and
bodyFlagName both empty — get/delete with only path or query params),
stripping Required from every field. Requests left with empty required
params and failed server-side with field paths ('resources[0].id') no
CLI user or coding agent can map back to a flag.
Restrict the relaxation to commands that actually register a body flag.
'vf project get' now fails locally, before any network call, with
'missing required flag: --project-id' — the same message shape cobra
produces for enforced query params. Whole-body input (--body/stdin)
still satisfies required body fields exactly as before.
|
Added: required-param preflight (the P2 gap). Root cause turned out to be in BuildRequest itself — its nullable-body relaxation also fired for params-only commands, stripping Required from path/query params. One-condition fix (b7602c9): relaxation now only applies to commands that register a body flag. 'vf project get' fails locally with 'missing required flag: --project-id' before any network call; --body/stdin behavior unchanged. 9/9 tests. This one is upstreamable to Speakeasy verbatim. |
- Agent-mode detection: Claude Code actually sets CLAUDECODE (no
underscore) — verified in a live session. Without it in the list the
flagship coding agent never triggered agent mode at all.
- dryRunRequested now accepts every bool spelling cobra does
(--dry-run=True/T/1/...), instead of string-matching two of them.
- Piped stdin no longer bypasses required-param enforcement on
params-only commands: 'echo {} | vf project get' used to re-relax
required path params and send 'GET /project/' with an empty segment.
- 429 hint echoes Retry-After verbatim (it may be an HTTP-date, not
delta-seconds).
- io.ReadAll can return bytes alongside an error; restore whatever arrived instead of blanking the body, so a truncated error message still reaches the user. - The 403 hint gated on the operation's own command group but always emitted 'vf workspace list'. Always emit it: workspace list is the command that reveals reachable scope, and it exists for every account.
Part of COR-13588 — shipping the vf CLI as an agent-era distribution channel.
Why
For both humans and AI coding agents, the error text is the documentation they actually read. An agent that hits a dead-end error picks a competitor; one that reads the fix in the error keeps going.
What
Three request-lifecycle behaviors, all in the sanctioned non-generated hooks extension point (
internal/sdk/sdkinternal/hooks/) — zero persisted edits to generated code:export VF_TOKEN=vfp_...) and adocs_url, in both the agent-mode envelope and pretty output.--dry-runstays usable without credentials.VF.DM.Dialog Manager keys and whitespace-wrapped tokens, without blocking the request.hints(401 expiry/renewal path, 403 acts-as-you semantics, 404 with a verifiedvf <group> listcommand, 429 Retry-After, 400/422--dry-runguidance) and adocs_urlderived from the operation's verified docs command-group page. Keys already in the body — including future server-provided hints — are never overwritten.Base branch: #7.
Verification
test/errors-teach.test.ts) against a local mock API: preflight fires with zero network (unroutable server), dry-run guard, both warnings, 404/429/401 hint injectiongo vetclean; manual matrix in agent + pretty modes