feat(setup): report an unwritable target as a structured error (task 0044)
All checks were successful
CI / test (22) (pull_request) Successful in 50s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 47s
CI / test (true, 24) (push) Successful in 1m4s
CI / flake (push) Successful in 3s
All checks were successful
CI / test (22) (pull_request) Successful in 50s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 47s
CI / test (true, 24) (push) Successful in 1m4s
CI / flake (push) Successful in 3s
Both halves of `setup` assumed the files they manage are writable. A declaratively managed target — read-only because a configuration manager owns it, because a file is flagged immutable, or because the path is root-owned — made the skill install raise a raw filesystem exception and the hook install surface the underlying message with no guidance. Both now fail with `TARGET_NOT_WRITABLE`, naming the file and pointing at the general remedy: it appears to be managed by another tool, so declare the skill or hook through that configuration instead. The error names no particular manager, because read-only is not diagnostic of one. A target already byte-identical to the bundled copy still succeeds — nothing needs writing, so its being read-only is beside the point.
This commit was merged in pull request #53.
This commit is contained in:
@@ -491,7 +491,7 @@ The dashboard's empty states are `prs: 0 open` / `issues: 0 open` (raw strings,
|
||||
Empty output is never silent.
|
||||
|
||||
**Principle 6 — Structured errors, exit codes, idempotent mutations, no prompts.**
|
||||
Errors are represented as a typed `AxiError` with one of ten named codes: `REPO_NOT_FOUND`, `ISSUE_NOT_FOUND`, `PR_NOT_FOUND`, `AUTH_REQUIRED`, `FORBIDDEN`, `RATE_LIMITED`, `TEA_NOT_INSTALLED`, `VALIDATION_ERROR`, `GIT_ERROR`, `UNKNOWN`.
|
||||
Errors are represented as a typed `AxiError` with one of eleven named codes: `REPO_NOT_FOUND`, `ISSUE_NOT_FOUND`, `PR_NOT_FOUND`, `AUTH_REQUIRED`, `FORBIDDEN`, `RATE_LIMITED`, `TEA_NOT_INSTALLED`, `VALIDATION_ERROR`, `GIT_ERROR`, `TARGET_NOT_WRITABLE`, `UNKNOWN`.
|
||||
The `ISSUE_NOT_FOUND`/`PR_NOT_FOUND` split (vs gh-axi's single `NOT_FOUND`) is a deliberate divergence enabled by path-based 404 classification.
|
||||
API error responses are classified by HTTP status code and calling context:
|
||||
|
||||
@@ -516,6 +516,8 @@ tea has logins but none match the detected hostname → `REPO_NOT_FOUND` (the re
|
||||
HTTP 401 from the API → `AUTH_REQUIRED` (token invalid or revoked), per the status table.
|
||||
A `--login` value naming a nonexistent profile is `VALIDATION_ERROR`, listing the available profile names.
|
||||
`GIT_ERROR` classifies non-zero git subprocess exits (currently only `pr checkout`), carrying git's first stderr line.
|
||||
`TARGET_NOT_WRITABLE` classifies a `setup` target the filesystem refuses (`EACCES`, `EPERM`, `EROFS`), naming the file and pointing at the general remedy: it appears to be managed by another tool, so the skill or hook belongs in that tool's configuration.
|
||||
It never names or infers a particular configuration manager — read-only is not diagnostic of one.
|
||||
Error output is TOON-encoded to stdout (not stderr): `error: <message>`, `code: <CODE>`, and optionally `help[N]:` with suggestion lines.
|
||||
The suggestions field is named `help`, not `hint`.
|
||||
Exit codes: 0 success, 1 error, 2 for `VALIDATION_ERROR` — covering unknown flags, missing required inputs, and server-side 422 rejections alike (the `axi-sdk-js` `exitCodeForError` mapping; see ADR 0004).
|
||||
|
||||
@@ -21,9 +21,32 @@ The failure follows the CLI's existing error convention rather than inventing a
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] An unwritable skill target produces a structured CLI error rather than an unhandled filesystem exception.
|
||||
- [ ] An unwritable hook target produces the same class of error, with the same guidance.
|
||||
- [ ] Both errors name the file that could not be written and state that it appears to be managed by another tool.
|
||||
- [ ] Neither error names or infers a specific configuration manager.
|
||||
- [ ] A skill target that is unwritable but already byte-identical to the bundled copy succeeds rather than failing, since nothing needs to be written.
|
||||
- [ ] The errors carry a code and help lines consistent with the rest of the CLI's error surface.
|
||||
- [x] An unwritable skill target produces a structured CLI error rather than an unhandled filesystem exception.
|
||||
- [x] An unwritable hook target produces the same class of error, with the same guidance.
|
||||
- [x] Both errors name the file that could not be written and state that it appears to be managed by another tool.
|
||||
- [x] Neither error names or infers a specific configuration manager.
|
||||
- [x] A skill target that is unwritable but already byte-identical to the bundled copy succeeds rather than failing, since nothing needs to be written.
|
||||
- [x] The errors carry a code and help lines consistent with the rest of the CLI's error surface.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The condition is `EACCES`, `EPERM`, or `EROFS` — the three ways a filesystem refuses a write for a reason the user has to settle outside this tool.
|
||||
The new `TARGET_NOT_WRITABLE` code joins the spec's enumerated list, alongside a paragraph describing it.
|
||||
|
||||
Two things came out of review and go slightly beyond the literal criteria.
|
||||
|
||||
The skill half now guards its comparison read as well as its write.
|
||||
A target the filesystem will not let us read is one it will not let us replace either — the same condition reached one call earlier — so a mode-`000` file reports the same error rather than the raw exception the criteria were written against.
|
||||
|
||||
The hook half collects its failures as `{path, detail}` records rather than the agent SDK's flattened `<path>: <message>` text.
|
||||
The SDK reports through a string, so the two halves are separated once at that boundary and judged apart.
|
||||
This matters for correctness, not just shape: testing the whole formatted string for an errno would misclassify an unrelated failure whose *path* happened to contain `EACCES`.
|
||||
|
||||
Two known limits, both judged acceptable rather than fixed.
|
||||
|
||||
The hook error names the target the SDK was writing, which is the intended path rather than necessarily the blocking one — if `~/.claude` were unwritable and `settings.json` absent, it would name the file rather than the directory.
|
||||
The SDK discards the error object, so its `path` is not recoverable; the skill half, which catches its own errors, does report the blocking path and is tested for it.
|
||||
|
||||
An unwritable `~/.claude/settings.json` still leaves the Codex and OpenCode integrations installed, because the SDK writes them before the failure surfaces.
|
||||
The command exits 1 having done part of its work.
|
||||
Making the hook install transactional across three integrations owned by the SDK is a larger change than this task, and re-running after fixing the permission converges correctly.
|
||||
|
||||
Reference in New Issue
Block a user