commit 21a075f8cd78313d21873227a72719d5eafa6a8e Author: alexion Date: Fri Jul 10 22:37:38 2026 -0400 docs: Requirements for the project. diff --git a/.claude/CONTEXT.md b/.claude/CONTEXT.md new file mode 100644 index 0000000..40b52b2 --- /dev/null +++ b/.claude/CONTEXT.md @@ -0,0 +1,158 @@ +# gitea-axi + +A thin TypeScript CLI that calls the Gitea REST API directly via `gitea-js` to give coding agents an ergonomic, low-token interface to Gitea issues and pull requests. + +## Language + +### The tool and its host + +**gitea-axi**: The CLI tool defined by this project. +_Avoid_: wrapper, adapter, shim + +**tea**: The official Gitea CLI whose login store gitea-axi reads for credential discovery; not used for command dispatch. +_Avoid_: Gitea CLI, upstream binary + +**gitea-js**: The official TypeScript client for the Gitea REST API, generated from Gitea's OpenAPI spec; the sole HTTP layer in gitea-axi. +_Avoid_: API client, HTTP client, fetch wrapper + +**AXI (Agent eXperience Interface)**: The set of 10 design principles that govern how gitea-axi shapes its output and behavior for coding agents. +_Avoid_: agent interface, UX principles + +**axi-sdk-js**: The shared TypeScript framework package (`axi-sdk-js` on npm) that provides `runAxiCli`, `AxiError`, `exitCodeForError`, and output helpers; gitea-axi is built on it, matching gh-axi's architecture. +_Avoid_: AXI library, SDK + +### Output + +**TOON**: The structured text output format used for all gitea-axi output, encoded via `@toon-format/toon`. +_Avoid_: JSON output, structured output + +**renderList**: The output helper that formats a collection of entities as a TOON list, preceded by a count line. +_Avoid_: list formatter, table renderer + +**dashboard**: The output of `gitea-axi` with no arguments; a two-tier home view preceded by the `bin:` + `description:` header from `axi-sdk-js`. +The short tier (no flags, and what the [[SessionStart hook]] runs) matches gh-axi's home shape: up to 3 open issues (`number`, `title`, `state`, `author`) and up to 3 open PRs (`number`, `title`, `author`, `review`), plus a `help:` hint pointing at `--full`. +The full tier (`gitea-axi --full`) shows open PRs as a TOON table and open issue counts grouped by label. +Issue counts are aggregated by fetching all open issues up to a hard cap of 1000 (page size 50, 20 pages max); if capped, the count is suffixed with `+`. +Each issue contributes to all of its labels; unlabeled issues appear as a separate `unlabeled` row only when non-zero. +Full-tier PR table default fields: `number`, `title`, `author` (plucked from `user.login`), `labels` (joined label names), `review` (computed client-side, same parallel review fetch as `pr list`). +The full-tier PR table is capped at 20 rows with a standard count line (`count: 20 of T total`). +Block names: `repo:` line, then `prs:` and `issues:`. +Empty states (both tiers): `prs: 0 open` / `issues: 0 open` (raw strings, matching gh-axi's home; list commands keep `[0]: (none)`). +Outside a recognizable Gitea repo the dashboard errors with `REPO_NOT_FOUND` (help: use `-R` + `--login`) — login selection needs a hostname; the resulting hook noise in non-Gitea sessions is an accepted consequence. +_Avoid_: home view, status view + +**renderDetail**: The output helper that formats a single entity's full detail as a TOON record. +_Avoid_: detail formatter, record renderer + +**count line**: The leading line in list output that states how many results were returned and their relationship to the total, e.g. `count: N of T total`. +When a client-side filter is active, `T` is the true filtered total computed from the in-memory result set (the `X-Total-Count` header, which reflects the unfiltered total, is ignored); the bare `count: N` form does not exist. +_Avoid_: summary line, header + +**FieldDef**: A typed descriptor that extracts and formats a single field from raw Gitea API JSON, with named extractor variants (nested pluck, array join, enum map, bool-to-text, relative time). +_Avoid_: field extractor, field descriptor + +**content truncation**: Shortening body or diff text to a defined character limit and appending an inline hint — `"... (truncated, N chars total - use --full to see complete body)"` — directly into the field value. +The full content is never written to a temp file; `--full` on the relevant subcommand suppresses *all* truncation in that command's output (entity body and comment bodies alike) and returns raw values instead. +Comment bodies truncate at 800 chars wherever they appear (comment-post output and `--comments` view blocks), with cleanBody applied; `--comments` renders all comments with no count cap, matching gh-axi. +_Avoid_: truncation, clipping, temp-file approach + +**cleanBody**: A preprocessing step applied to body text before truncation, to reduce token cost. +Applied only when the raw body exceeds the truncation limit. +Normalizes Gitea issue/PR URLs (using the detected hostname) to compact form: `https://///issues/N` → `Issue#N`, `https://///pulls/N` → `PR#N`. +Also strips markdown image embeds, long URLs in markdown links, standalone long URLs, and collapses email-style quoted blocks — matching gh-axi's cleanBody transformations. +_Avoid_: body cleaning, URL normalization + +### Errors and suggestions + +**AxiError**: The typed error value with one of ten named codes that gitea-axi emits on failure (TOON-encoded to stdout). +The codes: `REPO_NOT_FOUND`, `ISSUE_NOT_FOUND`, `PR_NOT_FOUND`, `AUTH_REQUIRED`, `FORBIDDEN`, `RATE_LIMITED`, `TEA_NOT_INSTALLED`, `VALIDATION_ERROR`, `GIT_ERROR`, `UNKNOWN`. +`GIT_ERROR` classifies non-zero git subprocess exits (currently only `pr checkout`), carrying git's first stderr line — the agent's recovery is local (fix the worktree), unlike API errors. +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; `RATE_LIMITED` maps HTTP 429 from proxies in front of Gitea. +_Avoid_: error object, exception + +**next-step suggestion**: A semi-dynamic hint appended to command output that tells the agent what to call next, normalized to include the current repo context flags. +Rendered as a `help[N]:` block — the same block name used for error suggestions, matching gh-axi and canonical AXI Principle 9. +Runtime values are hybrid: list output keeps placeholders (`issue view `), single-entity output fills the actual id (`issue view 42`), matching canonical Principle 9 ("leave runtime values parameterized") and gh-axi. +_Avoid_: hint, tip, recommendation, next[] + +**suggestion normalization**: The process of rewriting a next-step suggestion to include `-R OWNER/NAME` and `--login` flags derived from the current repository context. +Only applied when the context did not come from the git remote (i.e., when `source` is `"flag"` or `"env"`). +_Avoid_: flag injection, context enrichment + +### Commands + +**issue blocks**: A Gitea-specific subcommand group for managing which issues this issue blocks. +Three sub-operations: `list ` (issues blocked by n), `add ` (make n block target), `remove `. +Idempotent: `add` of an existing relationship returns `already: true` (fetch-first check); `remove` of a nonexistent relationship is silent success; true validation failures (self-reference, cycles) still surface as `VALIDATION_ERROR`. +No gh-axi equivalent — Gitea-specific API (`/issues/{index}/blocks`). +_Avoid_: blocking, blocks list + +**issue blocked-by**: A Gitea-specific subcommand group for managing which issues block this issue (i.e., must be resolved before this one). +Three sub-operations: `list `, `add `, `remove `. +Same idempotency rules as [[issue blocks]]. +No gh-axi equivalent — Gitea-specific API (`/issues/{index}/dependencies`). +_Avoid_: depends, depends-on, dependencies + +### Gitea API patterns + +**type guard**: The defense against Gitea's unified issue/PR model, where issue endpoints also serve PRs. +Every issues-list call passes `type=issues` (issue list, dashboard aggregation, client-side-filter pagination). +Issue commands invoked with a PR number refuse with `VALIDATION_ERROR` ("issue #N is a pull request") and a `pr view` help line, detected via the fetched object's non-null `pull_request` field. +Exception: `issue comment` stays permissive — PRs genuinely share the comment endpoint. +_Avoid_: PR filtering, issue-only mode + +**reviewDecision**: A computed field (not returned by Gitea) that summarizes the overall review state of a PR. +Derived client-side from the reviews list: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false`, and no non-dismissed `REQUEST_CHANGES` review exists; `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES` exists; otherwise `REVIEW_REQUIRED`. +On `pr list`, this requires one extra parallel HTTP call per PR to fetch reviews. +_Avoid_: review status, review aggregate + +**commit status**: Gitea's CI/CD state mechanism, attached to a commit SHA via `GET /repos/{owner}/{repo}/commits/{sha}/status`. +The state is one of `pending`, `success`, `error`, `failure`, `warning`, `skipped` (`skipped` exists in modern Gitea; older instances never emit it). +gitea-axi uses this as the equivalent of GitHub Check Runs for `pr checks` and the `checks` field on `pr view`. +Conclusion mapping: `success`→`pass`; `failure`/`error`/`warning`→`fail` (matching Gitea's own `Combine()` logic, which treats `warning` as failure); `skipped`→`skip`; `pending`→`pending`. +_Avoid_: check run, CI status, pipeline status + +**fetch-then-patch**: The pattern used for additive or subtractive mutations on list fields (assignees, reviewers) where Gitea's PATCH replaces the entire list rather than adding/removing individual entries. +gitea-axi reads the current list first, computes the desired list, then sends a single PATCH with the full resulting list. +_Avoid_: read-modify-write, merge-then-patch + +**client-side filtering**: The policy applied when Gitea's API does not support a given filter parameter. +gitea-axi paginates all results from the API (using `limit=50` pages until exhausted) and filters the full result set in-process. +When any client-side filter is active, the count line emits `count: N of T total` with `T` computed from the in-memory filtered set (the unfiltered `X-Total-Count` header is ignored as misleading). +Client-side *sort* (`issue list --sort`) is not a filter: it reorders without changing membership, so `T` comes from the `X-Total-Count` header as usual, while still requiring full pagination before sorting. +_Avoid_: in-memory filtering, local filtering + +**label name lookup**: The process of resolving a `--label ` string to a Gitea label ID before calling endpoints that require numeric IDs (e.g. `pr list --label`, `issue list --label`). +Implemented via `GET /repos/{owner}/{repo}/labels`; matched case-insensitively. +`--label-id ` is a Gitea-specific shortcut flag that bypasses the lookup and passes the ID directly. +_Avoid_: label resolution, name-to-ID mapping + +### Testing + +**fixture server**: The local HTTP server used in tests, pointed to by `GITEA_AXI_API_URL`, that maps incoming request paths and methods to pre-recorded Gitea API JSON response files. +_Avoid_: mock server, stub server, fake API + +**fixture**: A pre-recorded Gitea API JSON response file stored in `fixtures/` that the fixture server returns for a given request path and method. +_Avoid_: snapshot, recording + +**test mode**: Activated when `GITEA_AXI_API_URL` is set. +In test mode: all API calls go to the fixture server; tea subprocess is bypassed (token read from `GITEA_AXI_TOKEN`); git remote detection is suppressed. +Three env vars together make tests fully hermetic: `GITEA_AXI_API_URL`, `GITEA_AXI_TOKEN`, `GITEA_AXI_REPO`. +`GITEA_AXI_REPO` and `GITEA_AXI_LOGIN` are not test-mode-specific — they are general context overrides (priority: flag > env > git remote / hostname match, mirroring gh-axi's `GH_REPO`); test mode merely relies on them. +_Avoid_: mock mode, stub mode + +### Distribution + +**Agent Skill**: The markdown file bundled inside the npm package and installed to `~/.claude/skills/` by the `setup` command. +_Avoid_: skill file, Claude skill + +**setup**: The explicit subcommand that installs the Agent Skill into `~/.claude/skills/`; gitea-axi's primary fulfillment of AXI Principle 7 (Ambient context). +Idempotent: re-running reports already-installed/updated rather than failing. +There is no postinstall script — installation of the skill is always an explicit user action. +`setup hooks` additionally opts into the [[SessionStart hook]]. +_Avoid_: postinstall, installer script + +**SessionStart hook**: An opt-in ambient-context mechanism installed by `setup hooks` via `axi-sdk-js`'s `installSessionStartHooks()` (Claude Code `settings.json`, Codex `hooks.json`, OpenCode plugin). +It runs the bare `gitea-axi` binary (the short [[dashboard]] tier) in the session's working directory at session start and injects the output into the agent's context. +The SDK's installer registers the binary with no arguments, so the hook always runs the short tier; outside a Gitea repo it produces the dashboard's `REPO_NOT_FOUND` error, an accepted noise trade-off. +_Avoid_: session hook, ambient hook, postinstall hook diff --git a/.claude/adr/0001-diff-auth-via-tea-login-list.md b/.claude/adr/0001-diff-auth-via-tea-login-list.md new file mode 100644 index 0000000..b24c6ec --- /dev/null +++ b/.claude/adr/0001-diff-auth-via-tea-login-list.md @@ -0,0 +1,10 @@ +# Authenticate diff HTTP GET via `tea login list --output json` + +`pr get --diff` fetches diff content with a direct HTTP GET, bypassing the tea subprocess. +To get the auth token for that request, gitea-axi calls `tea login list --output json`, matches the login entry whose URL matches the current repo's hostname, and uses its token. + +## Considered Options + +**Read `~/.config/tea/config.yml` directly** — one fewer subprocess call, but couples gitea-axi to tea's internal storage format rather than its stable JSON output interface. + +**Shell out to `tea login list --output json`** (chosen) — consistent with the rest of the architecture, which goes through tea's JSON interface for everything; decoupled from tea's file format internals. diff --git a/.claude/adr/0002-direct-gitea-api-over-tea-subprocess.md b/.claude/adr/0002-direct-gitea-api-over-tea-subprocess.md new file mode 100644 index 0000000..34818ec --- /dev/null +++ b/.claude/adr/0002-direct-gitea-api-over-tea-subprocess.md @@ -0,0 +1,19 @@ +# Use direct Gitea API (`gitea-js`) instead of wrapping the `tea` subprocess + +gitea-axi calls the Gitea REST API directly via `gitea-js` rather than shelling out to the `tea` binary. +Tea was the original plan because it provides auth, multi-instance login, and full command coverage out of the box, but hands-on evaluation found too many gaps that made subprocess wrapping a patchwork rather than a clean pipeline. + +## Considered Options + +**Wrap `tea` with `--output json`** (rejected) — Tea's create commands (`issues create`, `pulls create`) have no `--output json` flag, requiring text parsing plus a follow-up get call. `pulls list` has no head-branch filter, forcing a full list scan for PR idempotency checks. Tea's JSON exposes no review counts or response totals. Diff content requires a direct HTTP GET regardless. Open issues for some of these gaps (#403 for non-interactive comments) have been stale for 3+ years, making upstream fixes an unreliable dependency. + +**Contribute missing features to `tea` upstream, then wrap** — Viable long-term but blocks gitea-axi's timeline on upstream PR acceptance velocity, which is low. + +**Direct Gitea API via `gitea-js`** (chosen) — Typed responses, `X-Total-Count` headers for true pagination totals, head-branch filtering on PR list, review counts, and immediate JSON from create operations. All gaps from the tea approach disappear. Auth still comes from tea's login store via `tea login list --output json`, so the operator's existing `tea` configuration is reused without gitea-axi owning a credential store. + +## Consequences + +Tea remains a runtime dependency for credential discovery only (`tea login list --output json`). +Operators must have `tea` installed and at least one login configured. +The `TEA_NOT_INSTALLED` error code covers the case where tea is absent. +Tea improvements (especially `--output json` on create commands) should still be contributed upstream as goodwill PRs, decoupled from gitea-axi's development. diff --git a/.claude/adr/0003-inline-truncation-with-full-flag.md b/.claude/adr/0003-inline-truncation-with-full-flag.md new file mode 100644 index 0000000..c23e1e3 --- /dev/null +++ b/.claude/adr/0003-inline-truncation-with-full-flag.md @@ -0,0 +1,19 @@ +# Use inline truncation hints and `--full`, not a temp-file path + +Body and diff text that exceeds the truncation limit is shortened inline with a hint appended to the field value — `"... (truncated, N chars total - use --full to see complete body)"`. +The `--full` flag on `issue view` and `pr view` suppresses truncation and returns the full raw value. +No temp file is written. + +## Considered Options + +**Save to a temp file, emit path in `full_content` field** (rejected) — The spec draft described this approach and attributed it to gh-axi, but that description was incorrect. +Temp-file paths create a coupling to local filesystem state that does not survive across sessions or machines, and agents cannot rely on file paths persisting between calls. + +**Inline hint + `--full` flag** (chosen) — This is what gh-axi actually implements (`src/body.ts` → `truncateBody()`), and what the AXI principle 3 specifies: *"appending a size hint like '(truncated, 2847 chars total — use --full to see complete body)'"*. +Simpler, portable, consistent with the stated reference. + +## Consequences + +`issue view` and `pr view` both accept `--full` to return untruncated body. +`pr diff` truncates at 4000 chars (matching gh-axi's `DIFF_TRUNCATE_LIMIT`); when truncated, a next-step suggestion to rerun with `--full` is prepended. +The `full_content` field name and temp-file design from the spec draft are dropped entirely. diff --git a/.claude/adr/0004-use-axi-sdk-js-framework.md b/.claude/adr/0004-use-axi-sdk-js-framework.md new file mode 100644 index 0000000..01b50f4 --- /dev/null +++ b/.claude/adr/0004-use-axi-sdk-js-framework.md @@ -0,0 +1,24 @@ +# Use `axi-sdk-js` as the CLI framework + +gitea-axi is built on the `axi-sdk-js` npm package, matching gh-axi's architecture. + +## What this provides + +- `runAxiCli` — CLI runner handling `--help`, `--version`, home dispatch, error catch → stdout, and `process.exitCode` +- `AxiError` / `exitCodeForError` — typed error class and exit code mapping +- `renderOutput` / `renderError` / `homeHeaderOutput` — TOON output helpers +- Home-view `bin:` + `description:` header (AXI Principle 8) + +## Considered Options + +**Reimplement locally** — Duplicates the CLI runner, error routing, and output helpers without benefit. +Likely to drift from the AXI standard over time. + +**Use `axi-sdk-js`** (chosen) — Correct error routing (stdout, not stderr), consistent exit codes, and `--help`/`--version` handled for free. +Keeps gitea-axi aligned with the evolving AXI ecosystem without owning that surface. + +## Consequences + +Error output goes to stdout (not stderr), matching gh-axi and axi-sdk-js behaviour. +The home/dashboard view automatically gets a `bin:` + `description:` header (AXI Principle 8). +`VALIDATION_ERROR` exits with code 2; all other errors exit with code 1. diff --git a/.claude/adr/0005-client-side-filtering-policy.md b/.claude/adr/0005-client-side-filtering-policy.md new file mode 100644 index 0000000..4c0d481 --- /dev/null +++ b/.claude/adr/0005-client-side-filtering-policy.md @@ -0,0 +1,24 @@ +# Client-side filtering when the Gitea API lacks a filter parameter + +When a `--filter` flag has no corresponding query parameter in Gitea's API (e.g. `pr list --assignee`), gitea-axi paginates through all results and filters in-process. + +## Considered Options + +**Drop the flag** (rejected) — Maintaining interface parity with gh-axi is an explicit goal. +Dropping flags that gh-axi supports degrades usability and breaks agent prompts written for the gh-axi interface. + +**Emit NOT_SUPPORTED error** (rejected) — Surfacing a capability gap as a runtime error is unhelpful; the agent asked for a filtered list and received nothing. + +**Client-side filtering** (chosen) — Paginate all results (`limit=50` per page until exhausted), filter in-process, return the matching set. +This produces correct results at the cost of extra HTTP calls. +The spec explicitly calls out that API call cost on the gitea-axi side does not factor into design decisions. + +## Consequences + +- All unsupported filter flags still appear in the CLI surface with identical semantics to gh-axi. +- The count line emits `count: N of T total` with `T` computed from the in-memory filtered set when any client-side filter is active; the `X-Total-Count` header (which reflects the unfiltered total) is ignored as misleading. + Since client-side filtering paginates everything anyway, the true filtered total is always known — reporting it satisfies canonical Principle 4 ("always report total item count"). + (Amended 2026-07-10: this originally specified a bare `count: N`, which under-reported a total the tool had already computed.) +- Client-side *sort* (`issue list --sort`) is not a filter: it reorders without changing membership, so the unfiltered total remains accurate and `T` comes from the `X-Total-Count` header as usual. + Sorting still requires full pagination before ordering, like filtering. +- Full pagination is bounded by the instance's total issue/PR count, which is acceptable for single-repo agent workflows. diff --git a/.claude/adr/0006-reviewdecision-parallel-fetch.md b/.claude/adr/0006-reviewdecision-parallel-fetch.md new file mode 100644 index 0000000..6d557ba --- /dev/null +++ b/.claude/adr/0006-reviewdecision-parallel-fetch.md @@ -0,0 +1,25 @@ +# Compute reviewDecision client-side via parallel review fetches on pr list + +Gitea has no aggregated `reviewDecision` field (neither REST nor GraphQL). +gitea-axi computes it client-side from the reviews list and includes it as a default field on `pr list` and `pr view`, matching gh-axi's interface. + +## Decision + +For `pr list`, fetch the reviews list for each PR in parallel (one HTTP call per PR) alongside the main list call. +Derive `reviewDecision` using: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false` and no non-dismissed `REQUEST_CHANGES` exists; `CHANGES_REQUESTED` if any such `REQUEST_CHANGES` exists; `REVIEW_REQUIRED` otherwise. + +## Considered Options + +**Omit reviewDecision from default fields** (rejected) — The field is in gh-axi's default schema for `pr list`. +Dropping it breaks interface parity and forces agents to issue explicit follow-up calls. + +**Include as opt-in `--fields` only** (rejected) — Same problem: agents trained on gh-axi expect it by default. + +**Parallel fetch per PR** (chosen) — One extra HTTP call per PR in the list, all issued in parallel. +Accepted explicitly: API call cost does not factor into design decisions for this project. + +## Consequences + +- `pr list` with N results makes N+1 HTTP calls (list + N review fetches). +- `official` and `stale` fields are exposed on `pr view --reviews` as Gitea-specific bonus data. +- The `reviewDecision` field appears in the default schema for both `pr list` and `pr view`. diff --git a/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md b/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md new file mode 100644 index 0000000..fba51fb --- /dev/null +++ b/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md @@ -0,0 +1,29 @@ +# fetch-then-patch for additive/subtractive assignee and reviewer mutations + +Gitea's PATCH endpoints for issues and PRs replace the entire assignee/reviewer list rather than adding or removing individual entries. +Flags like `issue edit --add-assignee` and `pr edit --add-reviewer` imply additive semantics: "add X to the current list, leave the rest alone." + +## Decision + +Implement additive and subtractive assignee/reviewer mutations as a fetch-then-patch: +1. Fetch the current entity (`GET .../issues/{index}` or `GET .../pulls/{index}`). +2. Compute the new list by applying the additions and removals to the current list. +3. Send a single PATCH with the resulting full list. + +## Considered Options + +**Single PATCH with only the new entries** (rejected) — Overwrites the existing list, dropping all current assignees/reviewers not mentioned in the command. +Correct for a "replace all" semantic but wrong for `--add` / `--remove` flags. + +**Dedicated add/remove endpoints** (not available) — Gitea has additive label endpoints (`POST .../issues/{index}/labels`) but no equivalent for assignees or reviewers. + +**fetch-then-patch** (chosen) — One extra GET per mutation. +Produces correct additive/subtractive semantics. +Accepted cost: same policy as client-side filtering — extra HTTP calls do not factor into design decisions. + +## Consequences + +- Every `--add-assignee`, `--remove-assignee`, `--add-reviewer`, `--remove-reviewer` call issues one extra GET. +- The operation is not atomic: a concurrent edit between the GET and the PATCH could cause a lost update. + Accepted as a known limitation for single-agent workflows. +- `issue label --add` / `--remove` does NOT use fetch-then-patch — Gitea has dedicated additive label endpoints that are already idempotent. diff --git a/.claude/adr/0008-comment-block-name-normalization.md b/.claude/adr/0008-comment-block-name-normalization.md new file mode 100644 index 0000000..9ac72ba --- /dev/null +++ b/.claude/adr/0008-comment-block-name-normalization.md @@ -0,0 +1,20 @@ +# Normalize both issue comment and pr comment to the `comment` block name + +gh-axi uses `comment` for `issue comment` output and `commented` for `pr comment` output. +gitea-axi normalizes both to `comment`, with the same schema: `{ number, author, created, body }`. + +## Considered Options + +**Match gh-axi exactly** (rejected) — `comment` for issue comment, `commented` for PR comment. +The inconsistency in gh-axi is an artifact of delegating to different `gh` subprocesses that return different data shapes, not an intentional design. +There is no semantic reason for the names to differ. + +**`comment` for both** (chosen) — A single consistent block name for any "post a comment" operation. +gitea-axi gets the created `Comment` object directly from the Gitea API POST response for both issue and PR comments, so both can return the same schema without extra calls. + +## Consequences + +- `issue comment` and `pr comment` both emit `comment: { number, author, created, body }` (body truncated at 800 chars). +- This is a deliberate interface divergence from gh-axi. +- `number` is used instead of gh-axi's `issue` alias, since the field applies to both issue and PR numbers. +- Agents get the posted comment's data immediately (AXI Principle 4 — eliminate round trips); no follow-up `view --comments` call needed to confirm what was posted. diff --git a/.claude/adr/0009-setup-command-over-postinstall.md b/.claude/adr/0009-setup-command-over-postinstall.md new file mode 100644 index 0000000..cad8c82 --- /dev/null +++ b/.claude/adr/0009-setup-command-over-postinstall.md @@ -0,0 +1,30 @@ +# Install the Agent Skill via an explicit `setup` command, not npm postinstall + +gitea-axi fulfills AXI Principle 7 (Ambient context) through a `setup` subcommand that copies the bundled Agent Skill markdown into `~/.claude/skills/`. +There is no postinstall script. + +## Considered Options + +**npm postinstall script** (rejected) — The original spec draft had `postinstall` drop the skill file automatically. +pnpm blocks lifecycle scripts by default and npm users increasingly install with `--ignore-scripts`, so the skill would silently fail to install for those users with no signal. +A package install silently writing into `~/.claude/` is also the exact pattern security tooling flags. +Finally, the canonical principle text asks for installation "from an explicit setup command" — postinstall is implicit. + +**`setup` command** (chosen) — Matches gh-axi's command surface (its `cli.ts` registers `setup`), matches the canonical principle wording, works under pnpm and `--ignore-scripts`, and makes the `~/.claude/` write an explicit user action. +Discoverable via dashboard help suggestions. + +**Both** (rejected) — Two install paths to test, and the postinstall path retains all its failure modes. + +## Consequences + +- `npm install -g gitea-axi` delivers the CLI only; the skill requires a one-time `gitea-axi setup`. +- `setup` is idempotent: re-running reports already-installed/updated rather than failing. +- The dashboard suggestion table hints at `setup` so agents and operators discover it. + +## Addendum (2026-07-10): opt-in `setup hooks` + +Canonical Principle 7 makes SessionStart hooks the primary ambient-context mechanism, and gh-axi ships `setup hooks` via axi-sdk-js's `installSessionStartHooks()` (Claude Code, Codex, OpenCode). +gitea-axi adds the same opt-in `setup hooks`; the skill remains the default `setup` action. + +Hooks are not the default because the hook runs the dashboard in every session in every directory, and outside a Gitea repo the dashboard errors with `REPO_NOT_FOUND` — a graceful exit-0 degradation was considered and rejected in favor of keeping the error explicit, so hook noise in non-Gitea sessions is an accepted consequence for users who opt in. +The SDK registers the bare binary as the hook command, so the hook always runs the short dashboard tier (see ADR 0012). diff --git a/.claude/adr/0010-hard-deletes-refuse-missing-targets.md b/.claude/adr/0010-hard-deletes-refuse-missing-targets.md new file mode 100644 index 0000000..88aa0a8 --- /dev/null +++ b/.claude/adr/0010-hard-deletes-refuse-missing-targets.md @@ -0,0 +1,23 @@ +# Hard deletes refuse missing targets instead of reporting idempotent success + +`issue delete` on a nonexistent issue errors with `ISSUE_NOT_FOUND`; `label delete` on a nonexistent label errors with `VALIDATION_ERROR`. +This deliberately narrows a literal reading of AXI Principle 6 ("mutations should be idempotent"). + +## Considered Options + +**Idempotent success ("already deleted")** (rejected) — Consistent with the literal principle text and with the silent-success behavior of `--remove-label` and `blocks remove`. +But a missing hard-delete target usually means the agent's world-model is wrong (wrong number, wrong repo), and reporting success would confirm a false belief — the agent walks away thinking it deleted something it never identified correctly. + +**Refuse with a specific error** (chosen) — Matches gh-axi's behavior for both commands. +The destructive command is exactly the one that should refuse to guess. + +## The dividing line + +Relationship removals (`--remove-label`, `blocks remove`, `blocked-by remove`) stay silent-success: the *entity* was correctly identified and fetched; only the relationship is absent, so the desired end state already holds. +Hard deletes error: the *target itself* is missing, which signals a stale or wrong reference rather than an already-achieved goal. + +## Consequences + +- `issue delete ` on a missing issue → `ISSUE_NOT_FOUND` (falls out of the path-based 404 classification automatically). +- `label delete ` on a missing label → `VALIDATION_ERROR`, consistent with every other label-name lookup. +- Principle 6's idempotency guarantee is scoped in the spec: state transitions and relationship add/removes are idempotent; hard deletes are not. diff --git a/.claude/adr/0011-pr-checkout-via-refs-pull-head.md b/.claude/adr/0011-pr-checkout-via-refs-pull-head.md new file mode 100644 index 0000000..97c6726 --- /dev/null +++ b/.claude/adr/0011-pr-checkout-via-refs-pull-head.md @@ -0,0 +1,19 @@ +# pr checkout fetches refs/pull/{index}/head, not the head branch name + +`pr checkout ` runs `git fetch origin pull//head:` (branch named from the PR's `head.ref`) followed by `git checkout `. + +## Considered Options + +**`git fetch origin `** (rejected — original spec draft) — Fails structurally for fork PRs: the head branch lives in the contributor's fork, which is not a configured remote in the operator's clone. +This is not an edge case; fork PRs are the default contribution model. + +**Add the fork as a remote dynamically** (rejected) — Mutates the user's git configuration, requires cleanup, and needs credentials for the fork's clone URL. + +**Fetch `refs/pull/{index}/head` from the base repo** (chosen) — Gitea, like GitHub, exposes every PR's head commit on the *base* repository under `refs/pull/{index}/head`, whether the head branch lives in the same repo or a fork. +One uniform code path, no remote mutation, no fork credentials. + +## Consequences + +- Same-repo and fork PRs check out identically. +- Git subprocess failures (dirty worktree, network) classify as `GIT_ERROR`, carrying git's first stderr line. +- The created local branch does not track the contributor's fork; pushing back to a fork branch is out of scope. diff --git a/.claude/adr/0012-two-tier-dashboard.md b/.claude/adr/0012-two-tier-dashboard.md new file mode 100644 index 0000000..4bc920f --- /dev/null +++ b/.claude/adr/0012-two-tier-dashboard.md @@ -0,0 +1,23 @@ +# Two-tier dashboard: gh-axi-sized default, `--full` for the rich view + +The no-args home view has two tiers. +The short tier (bare `gitea-axi`, and what the SessionStart hook runs) matches gh-axi's home shape: up to 3 open issues and up to 3 open PRs. +The full tier (`gitea-axi --full`) is the rich view: a 20-row open-PR table with labels and review decision, plus open issue counts grouped by label (up to 1000 issues aggregated). +The short tier's help block always hints at `--full`. + +## Considered Options + +**Rich dashboard always** (rejected) — The original spec shape. +It is the heaviest command in the tool: one review fetch per listed PR (up to 20) plus up to 20 pages of issue aggregation. +With the opt-in SessionStart hook (see ADR 0009 addendum) it would run at every session start inside the SDK's 10-second hook timeout, and canonical Principle 7 asks for a "compact" dashboard. + +**Short dashboard only** (rejected) — Drops the label-aggregation view entirely, losing the at-a-glance issue-state summary that motivated the rich dashboard. + +**Two tiers** (chosen) — Cheap, hook-safe default with the rich view one flag away and discoverable via the default output's help block. + +## Consequences + +- The SessionStart hook always runs the short tier, because the SDK registers the bare binary with no arguments. +- `--full` is intentionally overloaded: on view/diff commands it suppresses truncation; on the dashboard it selects the full tier. +- The short tier costs at most 5 HTTP calls (issues, PRs, up to 3 review fetches), comfortably inside the hook timeout. +- Dashboard empty states are `issues: 0 open` / `prs: 0 open` in both tiers, matching gh-axi's home view. diff --git a/.claude/adr/0013-shadow-sdk-update-command.md b/.claude/adr/0013-shadow-sdk-update-command.md new file mode 100644 index 0000000..47712ec --- /dev/null +++ b/.claude/adr/0013-shadow-sdk-update-command.md @@ -0,0 +1,18 @@ +# Shadow the axi-sdk-js `update` built-in + +axi-sdk-js reserves `update` as a built-in self-update command (`RESERVED_COMMANDS`): it queries npmjs.org for the latest published version of the tool and updates the install, throwing its own `UPDATE_ERROR` code on failure. +gitea-axi shadows it with a handler that rejects the command. + +## Considered Options + +**Keep the built-in** (rejected) — Free functionality and consistent with other axi-sdk-js tools, but it silently adds an unspecced command to the surface and an eleventh error code (`UPDATE_ERROR`) to the documented ten. +Self-updating from inside unattended agent sessions is also a write to the operator's toolchain that should stay an explicit human action. + +**Shadow it** (chosen) — `gitea-axi update` fails with `VALIDATION_ERROR` and a help line: `` Run `npm install -g gitea-axi@latest` to update ``. +The failure is instructive rather than an opaque unknown-command error. + +## Consequences + +- The command surface and the ten-code `AxiError` list stay exactly as specified. +- Updating gitea-axi is always an explicit npm action. +- If the SDK's reserved-command list grows, each new built-in needs the same adopt-or-shadow decision. diff --git a/.claude/gh-axi-interface.md b/.claude/gh-axi-interface.md new file mode 100644 index 0000000..7597312 --- /dev/null +++ b/.claude/gh-axi-interface.md @@ -0,0 +1,1789 @@ +# gh-axi Interface Reference + +This document describes the complete user-facing interface of gh-axi as implemented in the source. +It is intended to be used as a conformance target for gitea-axi's implementation. + +All output is TOON-encoded. +The TOON block type names (`pull_requests`, `issues`, `comment`, etc.) are exact — do not substitute synonyms. + +--- + +## Table of Contents + +1. [Invocation and Installation](#invocation-and-installation) +2. [Top-Level Flags](#top-level-flags) +3. [Repository and Host Targeting](#repository-and-host-targeting) +4. [Exit Codes](#exit-codes) +5. [Output Structure and TOON Encoding](#output-structure-and-toon-encoding) +6. [Count Line Format](#count-line-format) +7. [Truncation Behaviour](#truncation-behaviour) +8. [Suggestion / Help Lines](#suggestion--help-lines) +9. [Error Handling](#error-handling) +10. [Home / Dashboard](#home--dashboard) +11. [issue list](#issue-list) +12. [issue view](#issue-view) +13. [issue create](#issue-create) +14. [issue edit](#issue-edit) +15. [issue close](#issue-close) +16. [issue reopen](#issue-reopen) +17. [issue comment](#issue-comment) +18. [issue delete](#issue-delete) +19. [issue lock](#issue-lock) +20. [issue unlock](#issue-unlock) +21. [issue pin](#issue-pin) +22. [issue unpin](#issue-unpin) +23. [issue transfer](#issue-transfer) +24. [issue subissue add](#issue-subissue-add) +25. [issue subissue remove](#issue-subissue-remove) +26. [issue subissue list](#issue-subissue-list) +27. [pr list](#pr-list) +28. [pr view](#pr-view) +29. [pr create](#pr-create) +30. [pr edit](#pr-edit) +31. [pr close](#pr-close) +32. [pr merge](#pr-merge) +33. [pr review](#pr-review) +34. [pr checks](#pr-checks) +35. [pr diff](#pr-diff) +36. [pr checkout](#pr-checkout) +37. [pr ready](#pr-ready) +38. [pr reopen](#pr-reopen) +39. [pr comment](#pr-comment) +40. [pr update-branch](#pr-update-branch) +41. [pr revert](#pr-revert) +42. [label list](#label-list) +43. [label create](#label-create) +44. [label edit](#label-edit) +45. [label delete](#label-delete) + +--- + +## Invocation and Installation + +``` +npx -y gh-axi [subcommand] [args] [flags] +``` + +gh-axi requires the `gh` CLI installed and authenticated. +When `gh` is absent, the error code is `GH_NOT_INSTALLED` with the message: +`gh CLI is not installed — see https://cli.github.com` + +--- + +## Top-Level Flags + +These flags are processed by the SDK harness before any command runs. +They are accepted anywhere in the argument list after the binary name. + +| Flag | Aliases | Description | +|------|---------|-------------| +| `--help` | `-h` | Show help for the current command | +| `--version` | `-v`, `-V` | Print the installed gh-axi version | +| `--repo ` | `-R `, `--repo=`, `-R=` | Target a specific repository | +| `--hostname ` | `--hostname=` | Target a custom GitHub Enterprise host | + +The top-level help text is: + +``` +usage: gh-axi [command] [args] [flags] +commands[14]: + (none)=dashboard, issue, pr, run, workflow, release, repo, label, project, secret, variable, search, api, setup +flags[4]: + -R/--repo (after command), --hostname (after command) or GH_HOST env, both flags accept space or equals form, --help, -v/-V/--version +examples: + gh-axi + gh-axi issue list --state open + gh-axi issue list -R owner/name + gh-axi issue list --repo=owner/name + gh-axi issue list --hostname git.example.com + gh-axi pr view 42 + gh-axi secret list + gh-axi setup hooks +``` + +--- + +## Repository and Host Targeting + +### -R / --repo flag + +The `-R`/`--repo` flag must appear **after** the command (not before it): + +``` +gh-axi issue list -R owner/name # correct +gh-axi issue list --repo owner/name # correct +gh-axi issue list --repo=owner/name # correct +gh-axi issue list -R=owner/name # correct +gh-axi -R owner/name issue list # WRONG — not accepted +``` + +The `search` command is the sole exception: `--repo` is passed through to the underlying `gh` call in addition to being used for context resolution. + +The `repo view` subcommand additionally accepts exactly one positional repository argument (`gh-axi repo view owner/name`) as a compatibility exception for `gh repo view []`. +Do not combine the positional form with `--repo`. + +### --hostname flag + +`--hostname` must also appear after the command. +An explicit `--hostname` flag takes precedence over the `GH_HOST` environment variable. +When `--hostname` is present, `GH_HOST` is set to its value for the lifetime of the child `gh` process. + +### Suggestion -R injection rule + +When the current repo context was **not** inferred from the local git checkout (i.e. `ctx.source !== "git"`), suggestion lines include the repo flag. +The flag is placed **after** `gh-axi` and **before** the sub-command tokens using the `-R owner/name` form. + +Template strings in the suggestions source use the pattern: + +``` +`gh-axi${repoFlag(c)} issue view ` +``` + +which expands to `` `gh-axi -R owner/name issue view ` `` when a non-git repo is active. + +Suggestion lines are then post-processed by `normalizeRepoFlagLine`, which rewrites the anti-pattern `` `gh-axi -R ` `` to `` `gh-axi -R ` `` (flag after command). + +When `--hostname` was supplied via flag (not env), all backtick-wrapped `gh-axi` commands in suggestions are also suffixed with ` --hostname `. + +--- + +## Exit Codes + +gh-axi inherits exit code semantics from `axi-sdk-js`. +Verified against `axi-sdk-js@0.1.8` source: `exitCodeForError` returns 2 for `VALIDATION_ERROR` and 1 for every other error. +The mapping by error code is: + +| AxiError code | Exit code | +|---|---| +| `REPO_NOT_FOUND` | 1 | +| `NOT_FOUND` | 1 | +| `AUTH_REQUIRED` | 1 | +| `FORBIDDEN` | 1 | +| `VALIDATION_ERROR` | 2 | +| `RATE_LIMITED` | 1 | +| `GH_NOT_INSTALLED` | 1 | +| `UNKNOWN` | 1 | +| Success | 0 | + +--- + +## Output Structure and TOON Encoding + +All output is TOON-encoded. +The SDK function `renderOutput(blocks: string[])` joins multiple TOON blocks into a single output string. + +Common block types used across commands: + +- `renderList(name, items, schema)` — a labelled list block; the name becomes the TOON block key (e.g. `issues:`, `pull_requests:`, `labels:`) +- `renderDetail(name, item, schema)` — a single-entity detail block +- `renderHelp(lines)` — a `help:` block containing suggestion lines +- `renderError(message, code, suggestions)` — an error block +- `encode(object)` — raw TOON encoding of a plain object + +### Field definitions (schema primitives) + +| Primitive | Description | +|---|---| +| `field(key)` | Outputs `key: value` verbatim | +| `field(key, alias)` | Outputs `alias: value` using `key` as the data lookup | +| `lower(key)` | Outputs value lowercased | +| `pluck(key, subkey, alias)` | Navigates `item[key][subkey]`, outputs as `alias` | +| `relativeTime(key, alias)` | Converts ISO timestamp to relative time string, outputs as `alias` | +| `joinArray(key, subkey, alias)` | Joins an array of objects by extracting `subkey` from each, comma-separated | +| `boolYesNo(key, alias)` | Outputs `yes` or `no` for a boolean | +| `mapEnum(key, map, default, alias)` | Maps enum strings; unknown values become `default` | +| `custom(alias, fn)` | Calls `fn(item)` and outputs result as `alias` | + +--- + +## Count Line Format + +The `formatCountLine` function produces one of four exact phrases depending on context: + +| Condition | Output | +|---|---| +| `apiLimitHit === true` | `count: N+ (GitHub search API limit reached)` | +| `totalCount` is known from GraphQL | `count: N of T total` | +| `count > displayLimit` (display truncation) | `count: N (showing first D)` | +| `count === limit && count > 0` (hit request limit, no GraphQL total) | `count: N (showing first N)` | +| Otherwise | `count: N` | + +Where: +- `N` = number of items returned +- `T` = true total from GraphQL +- `D` = display limit + +For `issue list` and `pr list`: when `count === limit` and a `RepoContext` is available, gh-axi makes a GraphQL query for `totalCount`. +If the GraphQL call succeeds, the `count: N of T total` form is used. +If it fails, it falls back to `count: N (showing first N)`. + +--- + +## Truncation Behaviour + +### Body truncation (issues and PRs) + +The `truncateBody(text, limit)` function truncates the body field. + +- Issue `view` default schema: body truncated at **500 characters**. +- PR `view` default schema: body truncated at **500 characters**. +- Issue `comment` result schema: body truncated at **800 characters**. + +When `--full` is passed to `issue view` or `pr view`, the body is output in full without truncation. + +The exact hint text appended when truncation occurs is not directly visible in the schema — it is returned by `truncateBody` from `body.ts` (not provided in the fetched sources). +The `--full` flag is the documented way to bypass truncation. + +### PR diff truncation + +The `pr diff` subcommand truncates at **4000 characters** by default. +When truncated: +- The block contains `truncated: true` and `original_length: N` +- A suggestion is prepended: `` Run `gh-axi pr diff --full` to see the complete diff `` + (with `-R owner/name` injected if non-git repo context) + +When `--full` is passed, the full diff is output. + +### Workflow log truncation + +`run view --log` and `run view --log-failed` truncate at **20,000 characters** (tail kept). +When truncated, the complete log is best-effort saved to a temp file and exposed as a `full_log` field. +A `help:` hint tells agents to grep that file for earlier context. + +--- + +## Suggestion / Help Lines + +Every command ends with a `help:` block produced by `renderHelp(suggestions)`. +The suggestions are looked up from a static table by `(domain, action, isEmpty?, state?, id?)`. + +Empty suggestions produce an empty `help:` block (not omitted). + +### Issue suggestions + +| Trigger | Suggestions | +|---|---| +| `issue list`, not empty | `` Run `gh-axi issue view ` to view details `` / `` Run `gh-axi issue create --title "..." --body-file ` to create `` | +| `issue list`, empty | `` Run `gh-axi issue create --title "..." --body-file ` to create an issue `` / `` Run `gh-axi issue list --state closed` to see closed issues `` | +| `issue view`, state=open | Comment / close / assign / search PRs suggestions | +| `issue view`, state=closed | Reopen / comment / search PRs suggestions | +| `issue create` | View / label suggestions with `id` filled | +| `issue edit` | `` Run `gh-axi issue view ` to see updated issue `` | +| `issue close` | `` Run `gh-axi issue reopen ` to reopen `` | +| `issue reopen` | Close / view suggestions | +| `issue comment` | `` Run `gh-axi issue view --comments` to see all comments `` | +| `issue delete` | `` Run `gh-axi issue list` to see remaining issues `` | +| `issue lock`, `unlock`, `pin`, `unpin` | `` Run `gh-axi issue view ` to see issue details `` | +| `issue transfer` | (empty) | + +### PR suggestions + +| Trigger | Suggestions | +|---|---| +| `pr list`, not empty | View / create suggestions | +| `pr list`, empty | Create / closed-state suggestions | +| `pr view`, state=open | Checks / approve / merge suggestions | +| `pr view`, state=closed | `` Run `gh-axi pr reopen ` to reopen `` | +| `pr view`, state=merged | `` Run `gh-axi pr revert ` to revert `` | +| `pr create` | View / checks suggestions with `id` filled | +| `pr edit` | View suggestion | +| `pr close` | Reopen suggestion | +| `pr merge` | Revert suggestion | +| `pr review` | View suggestion | +| `pr checks` | View / merge suggestions | +| `pr diff` | Approve suggestion (plus truncation hint if truncated) | +| `pr checkout` | (empty) | +| `pr ready` | View suggestion | +| `pr reopen` | View suggestion | +| `pr comment` | `` Run `gh-axi pr view --comments` to see all comments `` | +| `pr update-branch` | `` Run `gh-axi pr checks ` to monitor CI after update `` | +| `pr revert` | `` Run `gh-axi pr view ` to see the revert PR `` | + +### Label suggestions + +| Trigger | Suggestions | +|---|---| +| `label list` | `` Run `gh-axi label create --name "..." --color "..."` to create a label `` | +| `label create` | `` Run `gh-axi label list` to see all labels `` | +| `label edit` | `` Run `gh-axi label list` to see all labels `` | +| `label delete` | `` Run `gh-axi label list` to see remaining labels `` | + +### Home suggestions + +``` +Run `gh-axi ` — commands: issue, pr, run, release, repo, label, secret, variable` +``` + +Additionally, if `issues.length >= 3`, a hint is prepended: +``` +Run `gh-axi issue list` for full issue list +``` + +If `prs.length >= 3`, a hint is prepended: +``` +Run `gh-axi pr list` for full PR list +``` + +--- + +## Error Handling + +### AxiError codes + +gh-axi defines these error codes (string literals): + +``` +REPO_NOT_FOUND | NOT_FOUND | AUTH_REQUIRED | FORBIDDEN | +VALIDATION_ERROR | RATE_LIMITED | GH_NOT_INSTALLED | UNKNOWN +``` + +### mapGhError — pattern-matched gh stderr + +When a `gh` subprocess fails, `mapGhError(stderr, exitCode)` converts the raw stderr to a structured `AxiError`: + +| Matched pattern | Code | Message | +|---|---|---| +| `Could not resolve to a Repository with the name ''` | `REPO_NOT_FOUND` | `Repository "" not found` | +| `Could not resolve to an? ... with the number of N` | `NOT_FOUND` | `Item #N does not exist in this repository` | +| `issue N not found` (case-insensitive) | `NOT_FOUND` | `Issue #N does not exist` | +| `pull request N not found` (case-insensitive) | `NOT_FOUND` | `Pull request #N does not exist` | +| `release with tag "T" not found` (case-insensitive) | `NOT_FOUND` | `Release "T" not found` | +| `run N not found` (case-insensitive) | `NOT_FOUND` | `Run N not found` | +| `gh auth login` (appears in stderr) | `AUTH_REQUIRED` | `GitHub auth required — run \`gh auth login\` first` | +| `authentication token is missing required scopes [S]` | `FORBIDDEN` | `GitHub token is missing required scope(s): S` | +| `secondary rate limit` | `RATE_LIMITED` | `GitHub secondary rate limit hit — wait ~60s and retry` | +| `API rate limit ... exceeded` | `RATE_LIMITED` | `GitHub API rate limit exceeded` | +| `sub-issue is already a sub-issue of issue with number N` | `VALIDATION_ERROR` | `Issue is already a sub-issue of #N` | +| `sub-?issue.*?(cycle\|circular)` | `VALIDATION_ERROR` | `Cannot add sub-issue: would create a cycle` | +| `issue cannot be a sub-?issue of itself` | `VALIDATION_ERROR` | `An issue cannot be a sub-issue of itself` | +| `HTTP 403` | `FORBIDDEN` | `Insufficient permissions for this action` | +| `HTTP 422` | `VALIDATION_ERROR` | Extracted `"message"` field from JSON body, or `Validation error` | +| `not found` (generic fallback, case-insensitive) | `NOT_FOUND` | First line of stderr | +| (no match) | `UNKNOWN` | First line of stderr, or `gh exited with code N` | + +### Validation errors (argument-level) + +These are thrown by command handlers before calling `gh`: + +| Command | Condition | Message | +|---|---|---| +| `issue list` | `--search` flag present | `issue list does not support --search. Use \`gh-axi search issues ""\` instead for full-text search with total counts.` | +| `pr list` | `--search` flag present | `pr list does not support --search. Use \`gh-axi search prs ""\` instead for full-text search with total counts.` | +| `issue create` | `--title` absent | `--title is required` | +| `pr create` | `--title` absent | `--title is required` | +| `issue comment` | `--body` / `--body-file` absent | (from `takeBody` with `required: true`) | +| `pr comment` | `--body` / `--body-file` absent | (from `takeBody` with `required: true`) | +| `issue transfer` | `--to-repo` absent | `--to-repo is required for transfer` | +| `label create` | `--name` absent | `--name is required: gh-axi label create --name "..." --color "..."` | +| `label create` | `--color` absent | `--color is required: gh-axi label create --name "..." --color "..."` | +| `label edit` | positional name absent | `Label name is required: gh-axi label edit ` | +| `label delete` | positional name absent | `Label name is required: gh-axi label delete ` | +| `pr merge` | multiple method flags | `Choose only one merge method: --merge, --squash, or --rebase` | +| `pr merge` | `--method` and shorthand conflict | `Choose either --method or a matching merge method shorthand, not both` | +| `pr merge` | invalid `--method` value | `--method must be one of: merge, squash, rebase` | +| `issue subissue add` | no `--repo` context | `Could not determine repository — pass --repo or run inside a git checkout` | +| `issue subissue add` | no child numbers | `subissue add requires at least one child issue number` | +| `issue subissue remove` | no `--repo` context | `Could not determine repository — pass --repo or run inside a git checkout` | +| `issue subissue remove` | no child number | `subissue remove requires a child issue number` | +| `issue subissue list` | no `--repo` context | `Could not determine repository — pass --repo or run inside a git checkout` | +| `issue view`, `issue edit`, etc. | numeric arg missing | `Missing issue number` | +| `pr view`, `pr merge`, etc. | numeric arg missing | `Missing PR number` | +| issue type invalid | type name not found | `Unknown issue type "". Available types: A, B, C` | +| issue type not configured | no types configured | `Issue types are not configured for this repository. Enable them in repo settings before using --type.` | + +### Unknown subcommand errors + +| Command | Message | Code | Suggestions | +|---|---|---|---| +| `issue ` | `Unknown issue subcommand: ` | `VALIDATION_ERROR` | `Run \`gh-axi issue --help\` for usage` | +| `issue subissue ` | `Unknown subissue subcommand: ` | `VALIDATION_ERROR` | `Run \`gh-axi issue subissue --help\` for usage` | +| `pr ` | `Unknown pr subcommand: ` | `VALIDATION_ERROR` | `Run \`gh-axi pr --help\` to see available subcommands` | +| `label ` | `Unknown subcommand: ` | `VALIDATION_ERROR` | `Available subcommands: list, create, edit, delete` | + +--- + +## Home / Dashboard + +**Invocation:** `gh-axi` (no command) + +Runs `issue list` and `pr list` in parallel, each limited to 3 items. + +### Output structure + +``` +repo: owner/name ← only when RepoContext is present +issues: + - number: 1 + title: ... + state: open + author: alice +prs: + - number: 42 + title: ... + author: bob + review: none +help: + - Run `gh-axi issue list` for full issue list ← only when issues.length >= 3 + - Run `gh-axi pr list` for full PR list ← only when prs.length >= 3 + - Run `gh-axi ` — commands: issue, pr, run, release, repo, label, secret, variable +``` + +When there are no open issues: `issues: 0 open` (raw string, not a list block). +When there are no open PRs: `prs: 0 open` (raw string, not a list block). + +### Issue schema (home) + +| Output field | Source | +|---|---| +| `number` | `number` | +| `title` | `title` | +| `state` | `state` lowercased | +| `author` | `author.login` | + +### PR schema (home) + +| Output field | Source | +|---|---| +| `number` | `number` | +| `title` | `title` | +| `author` | `author.login` | +| `review` | `reviewDecision` mapped: `APPROVED`→`approved`, `CHANGES_REQUESTED`→`changes_requested`, `REVIEW_REQUIRED`→`required`, otherwise→`none` | + +--- + +## issue list + +**Invocation:** `gh-axi issue list [flags]` + +### Flags + +| Flag | Type | Default | Required | +|---|---|---|---| +| `--state ` | string | (not passed; gh default is `open`) | no | +| `--label ` | string | — | no | +| `--assignee ` | string | — | no | +| `--author ` | string | — | no | +| `--milestone ` | string | — | no | +| `--sort ` | string | — | no | +| `--limit ` | integer | `30` | no | +| `--fields ` | comma-separated | — | no | +| `--search` | boolean | — | forbidden (throws `VALIDATION_ERROR`) | + +Note: `--sort` is implemented by appending `--search sort:-desc` to the underlying `gh issue list` call. + +### Default output fields + +TOON block name: `issues` + +| Output field | Source | +|---|---| +| `number` | `number` | +| `title` | `title` | +| `state` | `state` lowercased | +| `author` | `author.login` | +| `created` | `createdAt` as relative time | + +### Extra fields via --fields + +| Name | Output field | Source | +|---|---|---| +| `body` | `body` | `body` verbatim | +| `closedAt` | `closed_at` | `closedAt` as relative time | +| `labels` | `labels` | `labels[].name` joined | +| `milestone` | `milestone` | `milestone.title` | +| `updatedAt` | `updated_at` | `updatedAt` as relative time | +| `url` | `url` | `url` verbatim | + +### Output structure + +``` +count: N [of T total | (showing first N)] +issues: + - number: 1 + title: Fix login bug + state: open + author: alice + created: 2 days ago +help: + - Run `gh-axi issue view ` to view details + - Run `gh-axi issue create --title "..." --body-file ` to create +``` + +### Empty state + +When no issues are found, `count: 0` is shown and the list block is empty. +The `help:` suggestions switch to the empty-list variant: +``` +help: + - Run `gh-axi issue create --title "..." --body-file ` to create an issue + - Run `gh-axi issue list --state closed` to see closed issues +``` + +### Count line + +- If `count < limit`: `count: N` +- If `count === limit` and GraphQL succeeds: `count: N of T total` +- If `count === limit` and GraphQL fails: `count: N (showing first N)` + +--- + +## issue view + +**Invocation:** `gh-axi issue view [flags]` + +### Flags + +| Flag | Type | Default | Required | +|---|---|---|---| +| `--comments` | boolean | false | no | +| `--full` | boolean | false | no | + +### Default output fields + +TOON block name: `issue` + +| Output field | Source | Notes | +|---|---|---| +| `number` | `number` | | +| `title` | `title` | | +| `state` | `state` lowercased | | +| `author` | `author.login` | | +| `created` | `createdAt` as relative time | | +| `type` | `issueType.name` | `"none"` if issueType absent/empty; field omitted entirely if the host does not support `issueType` in `gh issue view --json` | +| `body` | `body` truncated at 500 chars | Full text when `--full` is passed | +| `subissues` | GraphQL sub-issues | e.g. `["#20", "#101"]`; omitted if no sub-issues | +| `parent` | GraphQL parent | e.g. `"#16"`; omitted if no parent | + +Sub-issue and parent fields require `RepoContext`. +If the GraphQL call for sub-issue relationships fails, those fields are silently omitted. + +### Comments output + +When `--comments` is passed, a separate `comments` list block is appended: + +TOON block name: `comments` + +| Output field | Source | +|---|---| +| `author` | `comments[].author.login` | +| `created` | `comments[].createdAt` as relative time | +| `body` | `comments[].body` truncated at 800 chars | + +Note: the `number` (issue) field is **not** shown in the comment list when rendered as a sub-block. + +--- + +## issue create + +**Invocation:** `gh-axi issue create --title [flags]` + +### Flags + +| Flag | Type | Default | Required | +|---|---|---|---| +| `--title ` | string | — | **yes** | +| `--body ` | string | — | no | +| `--body-file ` | path | — | no | +| `--assignee ` | string | — | no | +| `--label ` | string (repeatable) | — | no | +| `--milestone ` | string | — | no | +| `--project ` | string | — | no | +| `--type ` | string | — | no | + +`--body` and `--body-file` are mutually exclusive alternatives (handled by `takeBody`). + +### Default output fields + +TOON block name: `issue` + +| Output field | Source | +|---|---| +| `number` | `number` | +| `title` | `title` | +| `state` | `state` lowercased | +| `url` | `url` | +| `type` | `issueType.name` — only present when `--type` was supplied | + +### Idempotency + +`issue create` is **not** idempotent. +Each call creates a new issue regardless of title or content. + +### Suggestions + +After creation, `id` is set to the newly created issue number: +``` +help: + - Run `gh-axi issue view ` to see the full issue + - Run `gh-axi issue edit --add-label