Files
gitea-axi/.claude/CONTEXT.md
alexion 5422f49f38 feat(search): guide a 0-result search and fill the single-match number
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.
2026-07-19 10:17:14 -04:00

172 lines
15 KiB
Markdown

# 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