Skip to content

fix: carry error codes on the error, not in the message text - #183

Merged
theCodeDrift merged 1 commit into
mainfrom
fix/typed-error-codes
Aug 26, 2026
Merged

theCodeDrift merged 1 commit into
mainfrom
fix/typed-error-codes

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

The bug

rule create and rule improve chose their machine-readable error code by running a regex over a human-readable error message:

const code: CLIErrorCode = /git remote|origin/i.test(message)
  ? "NO_GITHUB_REMOTE"
  : "AUTH_REQUIRED";

The stable codes exist so a machine consumer never has to parse English. Here the code itself was picked by parsing English, in two independently driftable copies. It was correct only because both repository-URL failure messages happen to contain the words "git remote". Reword either one, translate it, or add a fourth failure mode, and every --json consumer is silently told to log in when the real problem is that the project has no repository.

This is the same shape as the styleguide's rule about not reconstructing a fact by parsing the producer's output. The producer already knew the code.

The fix

Option 1 from the issue, and it needed no new machinery: CLIError already exists in packages/cli/src/util/cli-error.ts and already carries an optional CLIErrorCode, which the telemetry runner already reads. Every expected failure inside resolveIdentity now throws a CLIError with its code attached:

  • packages/cli/src/auth/identity.ts throws AUTH_REQUIRED when there is no token.
  • packages/cli/src/util/git-remote.ts throws NO_GITHUB_REMOTE from both of its failure sites.

Both call sites in packages/cli/src/commands/rules.ts read the field through one shared identityFailureCode(error) helper, so they cannot drift and neither can invent a code of its own.

What resolveOrgSubject actually throws

Nothing. This was the fourth failure origin in the issue, and it turns out not to be a failure origin at all:

  • fetchWhoami wraps the whole request in try/catch, returns undefined on any thrown error, and returns undefined on an HTTP error response too.
  • resolveCurrentOrg returns undefined when no org matches the repo's remotes.
  • resolveOrgSubject then falls back to decodeOrgId(token) ?? NIL_ORG_ID, so a subject is always produced.

So there is no failure to give a code to, and no new CLIErrorCode was added. Anything that does escape that step is a CLI bug rather than a state the caller can act on, and identityFailureCode reports it as INTERNAL_ERROR. A test in error-envelope.test.ts points the CLI at an unroutable API base url and asserts that resolveOrgSubject resolves rather than rejecting, so if it ever grows a throw, that test fails and the throw gets a code of its own deliberately.

Origins 2 and 3 stay collapsed on NO_GITHUB_REMOTE

"Not a git repository / no origin" and "origin is not on github.com" remain one code, for two reasons:

  • CLIErrorCode is a public interface (the recipes name these codes in their ## Errors sections), so adding one needs a consumer that would act differently, and there isn't one yet. Both conditions mean the same thing to a caller: no verifiable GitHub org identity for this project, so the remote tier cannot run. The remedy differs (git init plus a GitHub remote, versus the tier genuinely not applying to a GitLab or Gitea project), but nothing in the CLI branches on that today.
  • Splitting them is now a one-line change instead of a regex rewrite. The code lives at each throw site, so A GitHub remote should not be a precondition for Taskless #179's capability-boundary work can separate them, or introduce a distinct code for the non-GitHub case, by editing the throw rather than by teaching a matcher a new phrase. That is the property this PR is actually buying.

Both are asserted separately in tests, so the day they should diverge, the tests are already there to record it.

Behavior change for --json consumers

The emitted code is unchanged for every reachable scenario: AUTH_REQUIRED when not logged in, NO_GITHUB_REMOTE for both repository-URL failures. One code changes in a scenario that is unreachable today: an unexpected throw from the org-resolution step now reports INTERNAL_ERROR instead of whichever of the two codes its wording happened to match. The changeset says this plainly.

Tests

packages/cli/test/error-envelope.test.ts gains a resolveIdentity failure codes (--json) block asserting the envelope's code for each origin, on both rule create and rule improve:

Origin Setup Asserted code
no token temp dir, TASKLESS_TOKEN stripped AUTH_REQUIRED
not a git repo / no origin temp dir with a token NO_GITHUB_REMOTE
non-GitHub origin git init plus a gitlab.com / gitea remote NO_GITHUB_REMOTE
org resolution unroutable API base url does not throw

One further test pins the property that replaced the regex directly: the AUTH_REQUIRED failure keeps its code while its message contains neither "git remote" nor "origin". Under the old implementation that message was the only thing standing between a consumer and the wrong code.

runCli now strips an ambient TASKLESS_TOKEN from the child environment, so a logged-in developer and CI exercise the same first step of resolveIdentity.

pnpm typecheck, pnpm lint, and the full suite (837 tests, 53 files) all pass.

Fixes #181
Refs #179

Copilot AI lite review requested due to automatic review settings August 26, 2026 04:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@theCodeDrift
theCodeDrift force-pushed the fix/typed-error-codes branch from bfe033f to 1769de6 Compare August 26, 2026 04:41
`rule create` and `rule improve` picked between `AUTH_REQUIRED` and
`NO_GITHUB_REMOTE` by running `/git remote|origin/i` over the human-readable
message `resolveIdentity` threw. The stable codes exist so a machine consumer
never parses English, and the code itself was chosen by parsing English. It
was correct only because both repository-URL messages happen to contain the
words "git remote".

Each expected failure inside `resolveIdentity` now throws a `CLIError`
carrying its own `CLIErrorCode`, following the class the CLI already uses for
this. Both call sites read that field through one shared
`identityFailureCode` helper, so they cannot drift.

The org-resolution step gets no code because it cannot fail: `fetchWhoami`
swallows every network and HTTP error, and `decodeOrgId` falls back to
`NIL_ORG_ID`. An unexpected throw there reports `INTERNAL_ERROR`.

Tests assert the `--json` envelope's `code` for every failure origin, on both
commands, so a reworded message fails a test instead of silently changing
what a consumer is told.

Fixes #181
Refs #179
@theCodeDrift
theCodeDrift force-pushed the fix/typed-error-codes branch from 1769de6 to 534ee6a Compare August 26, 2026 05:02
@theCodeDrift
theCodeDrift merged commit 5031fbd into main Aug 26, 2026
2 checks passed
@theCodeDrift
theCodeDrift deleted the fix/typed-error-codes branch August 26, 2026 05:07
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.

Error codes are chosen by regexing the error message

2 participants