Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ contract and provider behavior.
| `MINER_CODING_AGENT_CODEX_MODEL` | Any Codex model string accepted by the local `codex` CLI | Unset means `codex-cli` uses the CLI's own default model. Ignored by `noop`, `claude-cli`, and `agent-sdk`. |
| `MINER_CODING_AGENT_TIMEOUT_MS` | Positive integer milliseconds | Unset or invalid falls back to the CLI driver's default wall-clock timeout of `120000` ms. Ignored by `noop` and `agent-sdk`. |

### Recognizing a stale or missing coding-agent credential

When an attempt fails on a `claude-cli` / `codex-cli` provider, the CLI-subprocess driver folds the CLI's own output into a machine-readable `error` string on the attempt result. The credential/auth failure modes below map that exact string to a symptom and a concrete remediation — mirroring ORB's hosted-side [Recognizing a stale or missing credential](../../apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx) table. Every string is emitted by [`cli-subprocess-driver.ts`](../gittensory-engine/src/miner/cli-subprocess-driver.ts); nothing here is speculative.

| Error string / pattern | Symptom | Remediation |
| --- | --- | --- |
| `claude_code_error_<status>` | The `claude` CLI ran but its `--output-format json` envelope reported `is_error: true` (e.g. `claude_code_error_invalid_api_key`) — the OAuth token is missing, rejected, or expired. `gittensory-miner doctor` reports the same condition up front as `not authenticated: set CLAUDE_CODE_OAUTH_TOKEN`. | Regenerate a long-lived token with `claude setup-token` and set `CLAUDE_CODE_OAUTH_TOKEN`, then retry. |
| `codex_no_auth` | `codex exec` exited non-zero with no structured error in its JSONL stdout and only the `Reading prompt from stdin...` banner on stderr — its `auth.json` credential is missing or expired. The driver appends its own remediation hint (`... auth.json missing or expired -- run codex auth to authenticate`). | Run `codex auth` to re-authenticate; the next attempt reads the refreshed `auth.json` with no further restart. |
| `<command>_exit_<code>` | Generic non-zero-exit fallback when neither structured parser matched (e.g. `codex_exit_1: ...`, `claude_exit_1: ...`). The driver appends the redacted stderr slice — an auth failure that neither parser recognized still surfaces here. | Read the appended detail; if it points at authentication, re-run `claude setup-token` or `codex auth` for the failing provider, otherwise address the reported error directly. |

## Commands

```sh
Expand Down
81 changes: 81 additions & 0 deletions test/unit/miner-credential-troubleshooting-readme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

// Drift guard for the miner README's "Recognizing a stale or missing coding-agent credential" table
// (#5175). The table documents credential/auth failure modes surfaced by the CLI-subprocess driver, so
// every error string it lists MUST correspond to a literal constant actually emitted by the driver source
// -- otherwise the docs silently drift from the code. This is a documentation-only change, so there is no
// production behavior to regression-test; the driver's own error-string vocabulary is covered by
// test/unit/cli-subprocess-driver.test.ts (#5168/#5169). These tests only pin the docs↔code link.

const readmePath = join(process.cwd(), "packages/gittensory-miner/README.md");
const driverPath = join(process.cwd(), "packages/gittensory-engine/src/miner/cli-subprocess-driver.ts");

/** Each row of the README table: the error token as it appears in the first column, and the literal stem
* the CLI-subprocess driver source must actually contain to back it. Placeholders (`<status>`, `<code>`,
* `<command>`) are dynamic; the stem is the stable substring the driver emits verbatim. */
const CREDENTIAL_ERROR_ROWS = [
{ readmeToken: "claude_code_error_<status>", sourceStem: "claude_code_error_" },
{ readmeToken: "codex_no_auth", sourceStem: "codex_no_auth" },
{ readmeToken: "<command>_exit_<code>", sourceStem: "_exit_" },
] as const;

/** Slice the README down to the credential-troubleshooting section (heading → next heading). */
function readCredentialSection(): string {
const readme = readFileSync(readmePath, "utf8");
const heading = "### Recognizing a stale or missing coding-agent credential";
const start = readme.indexOf(heading);
expect(start).toBeGreaterThanOrEqual(0);
const rest = readme.slice(start + heading.length);
const nextHeading = rest.search(/\n#{1,3} /);
return nextHeading === -1 ? rest : rest.slice(0, nextHeading);
}

/** Pull every inline-code token out of the first column of each markdown table row in the section. */
function firstColumnErrorTokens(section: string): string[] {
const tokens: string[] = [];
for (const line of section.split(/\r?\n/)) {
const trimmed = line.trim();
// A data row starts with "|", excludes the header ("Error string") and the "| --- |" separator.
if (!trimmed.startsWith("|") || trimmed.includes("---") || trimmed.includes("Error string")) continue;
const firstCell = trimmed.split("|")[1]?.trim() ?? "";
const match = firstCell.match(/`([^`]+)`/);
if (match?.[1]) tokens.push(match[1]);
}
return tokens;
}

describe("gittensory-miner credential-troubleshooting README (#5175)", () => {
it("adds the credential-troubleshooting section covering the three required failure modes", () => {
const section = readCredentialSection();
expect(section).toContain("Error string / pattern");
expect(section).toContain("Symptom");
expect(section).toContain("Remediation");
// The three required cases: Claude Code envelope error, Codex auth failure, and the generic fallback.
for (const { readmeToken } of CREDENTIAL_ERROR_ROWS) {
expect(section).toContain(readmeToken);
}
});

it("backs every documented error string with a literal constant in the CLI-subprocess driver source", () => {
const driverSrc = readFileSync(driverPath, "utf8");
for (const { readmeToken, sourceStem } of CREDENTIAL_ERROR_ROWS) {
expect(driverSrc, `${readmeToken} must be backed by "${sourceStem}" in the driver`).toContain(sourceStem);
}
});

it("invariant: the table never contains a first-column error string that is not backed by a driver constant", () => {
const section = readCredentialSection();
const driverSrc = readFileSync(driverPath, "utf8");
const tokens = firstColumnErrorTokens(section);
// The parse actually found the rows (guards against a silently-empty extraction masking drift).
expect(tokens).toEqual(CREDENTIAL_ERROR_ROWS.map((r) => r.readmeToken));
for (const token of tokens) {
// Strip the dynamic `<...>` placeholders; whatever literal stem remains must exist in the driver.
const stem = token.replace(/<[^>]*>/g, "");
expect(stem.length).toBeGreaterThan(0);
expect(driverSrc, `no driver constant backs README token "${token}" (stem "${stem}")`).toContain(stem);
}
});
});