The next-step suggestion on `search issues`/`search prs` is now conditioned on the in-repo match count. On a miss it pointed the agent at `view <number>`, which is nonsensical when nothing matched; it now suggests the non-indexed `issue list --state all` / `pr list --state all` fallback, which recovers from both an over-narrow query and issue-indexer lag without naming the cause. On exactly one match it fills the real number (`issue view 2`), applying AXI Principle 9's single-id fill. Two or more matches keep the parameterized placeholder. Search stays a locator — it never auto-loads the detail even on a single match; ADR 0017 records that decision (a deliberate narrowing of Principle 4) and the CONTEXT.md search term is updated to match.
15 KiB
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 <noun>[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
action-block/entity-block pattern: The uniform convention for mutation output — an action-named block (created:, edited:, closed:, reopened:, merged:) when the mutation actually ran, an entity-named block (issue:, pull_request:) when it was an idempotent no-op.
Applies across issue and PR mutations alike; a deliberate departure from gh-axi, which returns entity blocks for issue-side mutation successes.
Avoid: status block, result block
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://<host>/<owner>/<repo>/issues/N → Issue#N, https://<host>/<owner>/<repo>/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 <number>), 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 <n> (issues blocked by n), add <n> <target> (make n block target), remove <n> <target>.
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 <n>, add <n> <blocker>, remove <n> <blocker>.
Same idempotency rules as issue blocks.
No gh-axi equivalent — Gitea-specific API (/issues/{index}/dependencies).
Avoid: depends, depends-on, dependencies
search: The full-text query commands (search issues <query>, search prs <query>), repo-scoped via owner param plus client-side filtering by repository (Gitea's /repos/issues/search has no repo-name filter).
Results use a locator schema (number, title, state, author, created) — search finds the number; issue view / pr view load the detail.
The next-step suggestion is conditioned on the in-repo match count: zero matches point at the non-indexed issue list --state all / pr list --state all fallback ("to list all … instead"), which recovers from both an over-narrow query and issue-indexer lag; exactly one match fills the real number (issue view <n>, Principle 9's single-id fill); two or more keep the parameterized <number> placeholder.
Search never auto-loads the detail even on a single match — it stays a locator (see ADR 0017).
The forbidden --search flag on the list commands redirects here.
Avoid: query command, find
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 with an official-first fallback: if any review is official=true, only official reviews are considered; otherwise all reviews are (unprotected repos never produce official reviews).
Within the considered set: CHANGES_REQUESTED if any non-dismissed REQUEST_CHANGES; APPROVED if any non-dismissed, non-stale review with state APPROVED; otherwise REVIEW_REQUIRED (rendered required — covers zero-review and comment-only PRs; there is no none value).
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 where Gitea's PATCH replaces the entire list rather than adding/removing individual entries — applies to assignees only.
gitea-axi reads the current list first, computes the desired list, then sends a single PATCH with the full resulting list.
Reviewers do not use this pattern: EditPullRequestOption has no reviewers field; reviewer mutations go through the dedicated POST/DELETE /pulls/{index}/requested_reviewers endpoints (see ADR 0007 amendment).
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 <name> string to a Gitea label ID before calling endpoints that require numeric IDs (e.g. pr list --label; note issue list --label does not need it — the issue-list endpoint accepts label names directly).
Implemented via GET /repos/{owner}/{repo}/labels; matched case-insensitively.
--label-id <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