Pular para conteúdo

AUR-443 — a first-time user discovers aurumcode without reading source

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Outcome: a user who has never read the source code discovers what aurumcode does and can run a first review. Dogfooding on 2026-08-13 measured a first-use score of 3/10 against the real binary and found seven concrete defects. This card fixes five of them, and investigates and records two as deliberately unchanged because fixing them would break a published, done card's contract. It changes only help/error/exit surfaces: aurumcode review --base's findings output and aurumcode docs's output are byte-for-byte unchanged (see "Compatibility" below).

Command

aurumcode --help | -h | help
aurumcode --version | version
aurumcode review --help | -h
aurumcode docs --help | -h

1. Top-level help

Before this card, aurumcode --help and aurumcode -h both failed: aurumcode: unknown command "--help", exit 2 — there was no top-level help at all, only each subcommand's own --help, and a user had to already know review or docs existed to find those. aurumcode --help (-h, or the bare word help) now lists every subcommand with a one-line summary and one runnable example, on stdout, exit 0:

$ aurumcode --help
usage: aurumcode <command> [flags]

Commands:
  review   Review a git diff with an LLM (or the project's deterministic security rules) and print, gate on, or publish the findings.
  docs     Generate project documentation from source code.

Run "aurumcode <command> --help" for that command's own flags.

Example:
  aurumcode review --base HEAD~1
$ echo $?
0

aurumcode with no arguments at all is unchanged: still a usage error (usage: aurumcode <review|docs> [flags], stderr, exit 2) — the user asked for nothing, so nothing was fulfilled; --help must be explicitly requested to count as a fulfilled request (see "3." below). An unknown command (aurumcode frobnicate) is likewise unchanged: stderr, exit 2.

2. --version

Before this card there was no version and no --version; the only way to learn which build was running was to read go.mod/git log. aurumcode --version (or aurumcode version) now prints a build-stamped version string, on stdout, exit 0:

$ aurumcode --version
aurumcode dev
$ echo $?
0

dev is the default (cmd/aurumcode/main.go's var version = "dev") because this reconstruction publishes no numbered release yet. A future release pipeline injects a real value with no other code change, via Go's standard -ldflags -X mechanism:

$ go build -ldflags "-X main.version=1.2.3" -o aurumcode ./cmd/aurumcode
$ ./aurumcode --version
aurumcode 1.2.3

3. One --help convention for every subcommand

Before this card, the same binary disagreed with itself: review --help printed usage to stderr and exited 2 (the generic flag.Parse error path, unchanged since AUR-430); docs --help printed usage to stdout and exited 0 (AUR-426's deliberate choice, documented in docs/specs/AUR-426.md's "--help's own convention" as intentionally not retroactively applied to review). Both are Go conventions in active use; the two-mindedness inside one binary was the actual defect.

The convention is now the same everywhere in this binary, at the top level and both subcommands: an explicitly requested --help (or -h) is a fulfilled request — stdout, exit 0. A genuine usage error (an unrecognized flag, a value that will not parse) is a refusal — stderr, exit 2.

$ aurumcode review --help >/dev/null; echo $?
0
$ aurumcode review --help 2>&1 1>/dev/null | wc -c   # nothing on stderr
0
$ aurumcode review --bogus 1>/dev/null; echo $?
2

review's flag descriptions and every other published review behavior (--base, --fail-on, --modelo, --seguranca, --pr, --limite, --check) are untouched; only the channel and exit code --help itself produces changed (cmd/aurumcode/main.go's runReview, mirroring the buffered-then-dispatched pattern runDocs already established).

4. The provider-missing message shows the fixture's shape

Before this card, a first run without a configured provider (the common case for a user who has cloned the repository and typed the example command) failed with the correct diagnosis but no way to act on it without reading source:

aurumcode review: no LLM provider configured: set AURUMCODE_LLM_FIXTURE for offline use, or LLM_API_KEY and LLM_BASE_URL for a live provider

— naming the environment variable, but not what a valid file at that path looks like. The message now shows the minimal JSON shape inline and points at a real, versioned, already-committed example the user can point AURUMCODE_LLM_FIXTURE at directly, with no source reading required:

$ aurumcode review --base HEAD~1
aurumcode review: no LLM provider configured: set AURUMCODE_LLM_FIXTURE=<path> to a JSON file shaped like {"issues":[{"file":"<path>","line":<n>,"severity":"error|warning|info","message":"<text>"}]} for offline use (see tests/fixtures/review/known-problem-response.json for a worked example), or set LLM_API_KEY and LLM_BASE_URL for a live provider
$ echo $?
1
$ AURUMCODE_LLM_FIXTURE=tests/fixtures/review/known-problem-response.json aurumcode review --base HEAD~1
config/demo-tokens.txt:4: [error] A credential-shaped value was committed in plain text (DEMO_API_TOKEN).
$ echo $?
0

reportModelUnavailable (the --modelo-specific unavailable-model message) gets the identical pointer for the same reason. Both messages are printed through the AUR-432 redaction writer; cmd/aurumcode/redaction_wiring_test.go's TestStderrLinesSurviveTheRedactionWriter pins both as filter-identity — neither file/line/severity/message nor the fixture's path is a key/value shape the AUR-009 filter's secret-key-name rules ever match, so the example itself is never rewritten to [REDACTED] in front of the user.

5. --limite's refusal exit code: investigated, left unchanged

The dogfooding note observed that a --limite refusal exits 1, indistinguishable from "the tool broke," while --fail-on's gate has its own exit 3. This card investigated giving --limite's refusal a dedicated exit code and did not make the change, because AUR-433 is already done and its published contract pins exit 1 for exactly this outcome in three places:

  • tests/acceptance/AUR-433.sh's nominal_case, the over-limit case: [[ "$rc" -eq 1 ]] || fail over-limit-wrong-exit.
  • The same function's margin/boundary case: [[ "$rc" -eq 1 ]] || fail boundary-wrong-exit.
  • mutation_case's restoration check: [[ "$rc" -eq 1 ]] || fail 'MUT-001/restoration-broken', and the mutant assertion [[ "$rc" -ne 1 ]] || fail 'MUT-001/not-rejected'.

docs/specs/AUR-433.md's own exit-code table independently documents the same thing: "1 | The review failed, unchanged from AUR-430/431/435/436, now including: the assembled request's cost would exceed --limite. The model was never called." Any exit code other than 1 for this outcome fails all three assertions above and contradicts the published spec. Per this card's own instructions, that is a stop condition, not a defect to route around: cmd/aurumcode/cost.go is untouched by AUR-443. This card's own tests/unit/AUR-443.go re-asserts the exit code directly ("--limite refusal is still exit 1"), and the builder additionally ran tests/acceptance/AUR-433.sh against this candidate directly in the worktree and confirmed it still exits 0 (recorded in this card's evidence). tests/acceptance/AUR-433.sh itself is not a read_path for AUR-443, so tests/acceptance/AUR-443.sh cannot stage and re-run it inside the sealed acceptance profile; that check is done out of band, against the full checkout, not nested inside AUR-443's own sandboxed acceptance.

If a distinct exit code for a budget refusal is still wanted, it is a follow-up card's job: one that is allowed to touch AUR-433's own paths (tests/acceptance/AUR-433.sh, docs/specs/AUR-433.md) and re-publish that contract deliberately, not one that changes it as a side effect of a UX pass.

6. Git-repository and ref errors: one clean, actionable message

Before this card, a repository that could not be opened produced a literally duplicated phrase, because cmd/aurumcode's computeDiff wrapped internal/analyzer.OpenRepo's own already-complete message with another "is not a git repository" prefix:

aurumcode review: /tmp is not a git repository: not a git repository (no .git dir and /tmp is not bare): /tmp

and a ref that does not resolve leaked implementation detail that means nothing to a user who has not read internal/analyzer: a raw filesystem path on the pure-Go backend (the one this card's own sealed acceptance profile always exercises — no git binary in bootstrap-readonly-v1),

aurumcode review: computing diff does-not-exist..HEAD: resolving base ref "does-not-exist": resolving ref refs/heads/does-not-exist: open /path/to/repo/.git/refs/heads/does-not-exist: no such file or directory

or git's own "fatal: ..." wording on the git-binary backend (used when a git binary happens to be on PATH, e.g. an interactive developer shell):

aurumcode review: computing diff does-not-exist..HEAD: resolving base ref "does-not-exist": resolving ref "does-not-exist": git rev-parse --verify does-not-exist^{commit}: fatal: Needed a single revision

— two different leaks of internal detail for the same user mistake, depending on an implementation choice (OpenRepo's backend selection) the user cannot see. Both are now one clean, actionable sentence, identical regardless of which backend answered:

$ aurumcode review --base HEAD~1     # run outside any git repository
aurumcode review: /tmp is not a git repository (no .git directory here, and this path is not itself a bare repository)
$ echo $?
1
$ aurumcode review --base does-not-exist
aurumcode review: ref "does-not-exist" not found in this repository: expected a branch name, HEAD, a 40-character commit SHA, or a "<ref>~N" parent expression
$ echo $?
1

internal/analyzer is a read_path for this card (frozen, not owned by AUR-443), so the cleanup lives entirely in cmd/aurumcode/main.go: computeDiff no longer wraps OpenRepo's error with %w (its one error case is fully known — see internal/analyzer/gitrepo.go's OpenRepo), and a new cleanRefError helper recognizes the two prefixes analyzer.Repo.Diff always wraps with ("resolving base ref %q: " / "resolving head ref %q: ") together with the two concrete leaks measured above (a *fs.PathError in the chain, or git's own "Needed a single revision"/"unknown revision or path" wording) and replaces them with the single sentence shown above. Any other Diff failure (a corrupted tree, an ambiguous parent count, ref indirection too deep) is not concretely measured by this card's dogfooding and keeps its pre-existing, more verbose wrapped message — this card fixes the two measured leaks, it does not invent a new taxonomy of ref errors that was never observed.

6.1. Post-review correction: permission denied is not "not found"

Independent review found that the first cut of cleanRefError matched any *fs.PathError as "no such ref," which also matches a ref that exists but cannot be read — EACCES, not ENOENT. Reproduced by chmod 000 on a bare fixture's refs/heads/main and running as a non-root uid (the same shape a locked-down runtime like bootstrap-readonly-v1, or this reviewer's repro at uid 65534, naturally has): the command answered ref "main" not found, exit 1 — a confident, wrong diagnosis that directly contradicts this file's own "never silently hidden behind a guessed diagnosis" promise. The pre-AUR-443 binary's leaked, verbose message at least contained the words "permission denied"; this card's first cut regressed that to a clean but false answer.

Two separate defects, because the two backends leak differently:

  • Pure-Go backend: errors.As(err, &pathErr) alone cannot distinguish EACCES from ENOENT — both are *fs.PathError. Fixed by additionally requiring errors.Is(err, fs.ErrNotExist).
  • git-binary backend: git rev-parse --verify <ref>^{commit} prints the byte-identical fatal: Needed a single revision for both causes — verified directly (git rev-parse --verify against an unreadable ref vs. a missing one produces the same stderr text). No string-matching fix on git's own output can ever tell them apart.

The fix that closes both: cleanRefError no longer infers "not found" from either backend's ambiguous text alone. It first calls a new, independent probe, refPathPermissionDenied(repoRoot, ref), which opens the exact physical ref location either backend would have consulted (.git/refs/heads/<ref>, or .git/HEAD for ref == "HEAD") directly from cmd/aurumcode and checks whether that open fails with errors.Is(err, fs.ErrPermission). This is independent of which backend actually answered — it reads the same file cmd/aurumcode already knows the location of, so it correctly classifies the failure for both backends with one code path, without editing internal/analyzer (a read_path). The probe is conservative: a raw 40-character SHA is skipped (no loose-ref file to check), and any outcome other than a confirmed permission error — the file does not exist, or it opens fine — falls through to the existing (now-corrected) "not found" heuristic, so the probe never manufactures a diagnosis it cannot back up.

$ chmod 000 .git/refs/heads/main   # as a non-root user
$ aurumcode review --base main
aurumcode review: permission denied resolving ref "main" in this repository: this process cannot read the ref's storage under .git (or the bare repository root) -- check its file permissions
$ echo $?
1

Verified identical on both backends (tests/integration/AUR-443.go's "git-binary backend: the same unreadable ref is also a permission message" subtest asserts the git-binary path's answer for this exact scenario, alongside the pure-Go path). tests/unit/AUR-443.go's biting test ("a ref that exists but is unreadable: permission message, never ref-not-found") additionally asserts the negative: the message must not contain "not found", and the genuinely-missing-ref test ("a ref that does not resolve") asserts the opposite negative — it must not contain "permission denied" — so the two outcomes cannot silently swap.

6.2. Post-review correction: a directory is not "not found" either

A second, independent review pass (vector 3: "try to fool the logic") found a third distinct cause that fell through §6.1's fix into the same raw-chain leak: --base feature against a repository whose only branch under that name is the hierarchical feature/sub — no literal feature ref exists, only the directory refs/heads/feature/ holding sub — resolves refs/heads/feature to a directory, not a ref file. On the pure-Go backend, os.ReadFile on a directory fails with EISDIR, which is neither fs.ErrNotExist nor fs.ErrPermission — so §6.1's two checks both correctly said no, and the code fell through to the one remaining raw %w wrap, reproducing exactly the leak this card exists to close:

aurumcode review: computing diff feature..HEAD: resolving base ref "feature": resolving ref refs/heads/feature: read /tmp/.../refs/heads/feature: is a directory

— containing the literal refs/heads/ path tests/acceptance/AUR-443.sh already treats as a leak everywhere else in this file, just not (before this fix) against this specific scenario. The git-binary backend, for the same input, already said a clean but imprecise ref "feature" not found (git's own rev-parse folds this case into the same generic "no single revision" outcome) — so the two backends disagreed, and only the pure-Go one (the one bootstrap-readonly-v1 actually exercises) leaked.

The structural defect: cleanRefError, once it has matched the "resolving base ref " / "resolving head ref " prefix, is already committed to explaining a specific, named ref's resolution failure — falling through to the raw wrap at that point was always going to leak something for whatever cause the two existing checks did not recognize, EISDIR was just the first one review found. The fix removes the fallthrough entirely: every branch past the prefix match now returns a clean, ref-scoped sentence, including a new fallback ("could not resolve ref %q in this repository") for a genuinely unclassified cause, and a new refPathIsDirectory(repoRoot, ref) probe — sharing refPathCandidate with refPathPermissionDenied, the same direct-filesystem-probe approach §6.1 already established — checked before the not-found heuristic:

$ aurumcode review --base feature   # only refs/heads/feature/sub exists
aurumcode review: ref "feature" is not a branch: a directory of refs by that name exists (a hierarchical branch namespace, e.g. "feature/...") -- name the exact branch, not a namespace prefix
$ echo $?
1

Probing first, rather than only patching the pure-Go backend's own error text, makes both backends agree on this more precise answer — the git-binary backend's previous "not found" is now also replaced by "is not a branch," not merely left alone, since the probe runs regardless of which backend answered. Verified identical on both backends (tests/unit/AUR-443.go and tests/integration/AUR-443.go each add a subtest asserting the pure-Go and git-binary messages match byte for byte). tests/acceptance/AUR-443.sh's nominal case re-runs its two existing leak greps (no such file or directory, refs/heads/) against this exact scenario, per the review's instruction, so a future regression here cannot pass unnoticed.

7. The model response's unused summary field: investigated, left unchanged

The dogfooding note observed that pkg/types.ReviewResult.Summary (parsed by internal/prompt.ResponseParser from the model's "summary" field, redacted by internal/review.redactReviewResult) reaches cmd/aurumcode's result and is never printed — silent dead code from this command's point of view. This card's own instructions require a decision: print it (and document it) or remove it from the parse; never leave it silently dead.

Neither branch is reachable within this card's paths. The evidence:

  • Printing it is blocked by a published, done card's byte-pinned contract. tests/acceptance/AUR-426.sh's review-contract-broken check and tests/unit/AUR-426.go's "aurumcode review's published contract is untouched" subtest (tests/unit/AUR-426.go:319) both run aurumcode review --base HEAD~1 against the fixture {"issues":[],"summary":"Nothing to report."} — a non-empty summary — and assert stdout is exactly "No issues found.\n", byte for byte. Printing Summary on a successful review, even conditionally on it being non-empty, changes that byte sequence and fails a done card's own acceptance the next time it runs. This card's own instructions independently forbid it a second way: review --base's findings output is pinned not to change content, and a summary line is neither a help, error, nor exit-code surface.
  • Removing it from the parse is outside paths. The field is parsed in internal/prompt/parser.go's ParseReviewResponse and populated in pkg/types.ReviewResult; both are read_paths for AUR-443, not paths — this card cannot edit either file.

The resolution: the decision is made and recorded, not silently re-deferred. cmd/aurumcode/main.go's runReview carries a comment at the exact site result is consumed, citing this section and the two acceptance assertions above, so the omission reads as a deliberate, investigated choice instead of an unexplained gap. No behavior changed.

2026-09-13 postscript (AUR-490): this decision is superseded, on purpose, for review --base specifically. AUR-490's own Acceptance required --base to carry the same summary and Mermaid diagram --pr already produced, "sem configuração" -- the exact promise this product's README and site make and --base did not keep. That cannot coexist with the byte-exact "No issues found." contract above: a run's stdout cannot both always include a rendered summary and, for the zero-issue case, consist of nothing but that one line. AUR-490 chose to keep the promise and retire the byte-exact contract, not the reverse -- the whole reason that card exists is that the local path was silently missing what the product already advertises.

Two things independently made this safe to decide without reopening this card's own investigation above:

  • The blocking test no longer runs. tests/acceptance/AUR-426.sh's review-contract-broken check depends on cmd/regenerate-docs and tests/unit/AUR-426.go, both removed by the later review-only pivot (670c7f6) -- before AUR-490 touched anything, this acceptance was already unable to execute past its own docs invocation. Retiring that now-dead script is tracked separately; it is not evidence either way about whether the summary should print, only about why nothing caught this sooner.
  • The reversal is recorded at the site it happens, not just here: cmd/aurumcode/main.go's runReview, at the same call site this section originally annotated, now explains why renderLocalReport (which includes the summary) prints unconditionally, citing this postscript.

The exit-code table below has the matching amendment for the "no provider configured" row.

Exit codes (all subcommands)

Code Meaning
0 --help/-h/help (top-level or either subcommand) or --version/version: the request was fulfilled.
0 review or docs ran and did what it promises (unchanged from AUR-430/431/435/436/438/439/433/441/426).
1 A real failure: not a git repository, a ref that does not resolve, the model call failed, the response could not be parsed, --limite's estimate exceeded the ceiling (see "5." above) — all unchanged in meaning, some reworded per "4." and "6." above. "No provider configured" is no longer unconditionally in this row for review --base (AUR-490, see "7."'s postscript): that case now runs deterministic analysis and exits per --fail-on/--check/--exigir-qualidade against what it found — 0 when nothing crosses the configured threshold, 3 when something does. It remains exit 1 for the PR path (--pr) and for --base whenever --modelo was explicitly given and the named provider could not be reached.
2 Usage error: an unrecognized flag, a required flag missing, a value that will not parse — including an explicitly requested --help that fails to parse for an unrelated reason (never for --help/-h itself, see "3." above).
3 Unchanged from AUR-431/AUR-439: the --fail-on or --check gate closed on a grave finding.

Compatibility, migration, rollback

  • Compatibility. aurumcode review --base's findings output (stdout content: notices, findings, the security section, the cache-reuse note, the cost lines) and aurumcode docs's output are byte-for-byte unchanged. This card touched only: (a) new top-level --help/-h/help and --version/version handling in run(), previously unreachable code paths (every one of those tokens previously fell through to "unknown command", exit 2); (b) review's own --help/-h channel and exit code (stderr+2 → stdout+0), which no published spec or test pinned before this card (verified: no acceptance, unit, integration or e2e file for AUR-430/431/433/435/436/438/439/441 exercises review --help at all); (c) two provider-missing stderr messages (extended, not shortened — every reader relying on strings.Contains(..., "no LLM provider configured"), e.g. tests/unit/AUR-436.go:198, still matches); (d) computeDiff's git-repository/ref-resolution error text (never asserted verbatim by any published spec or test — verified by the same search). tests/acceptance/AUR-433.sh, AUR-439.sh, AUR-441.sh and AUR-426.sh are not read_paths for AUR-443, so this card's own sandboxed tests/acceptance/AUR-443.sh cannot stage and re-run them; instead, tests/unit/AUR-443.go and tests/e2e/AUR-443.sh re-assert the specific published invariants those four cards pin (byte-identical review --base output including with a non-empty summary, the --limite exit code, the finding line shape), and the builder separately ran all four sibling acceptance scripts directly against this candidate in the worktree and confirmed exit 0 in each (recorded in this card's evidence, not nested inside the sealed acceptance run).
  • Migration. Not applicable — no data or state format changes.
  • Rollback. Revert only paths; no state external to this repository is published by this card.