Files
gitea-axi/.claude/CONTEXT.md
alexion 4e92dde4e4
All checks were successful
CI / test (22) (pull_request) Successful in 48s
CI / test (true, 24) (pull_request) Successful in 1m4s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 55s
CI / test (true, 24) (push) Successful in 1m5s
CI / flake (push) Successful in 2s
docs: break the declarative install path into tasks (tasks 0043-0045)
Grilling task 0042's mitigation found its framing too narrow. Recording an
absolute entrypoint path is one defect; the deeper one is that `setup` is
write-only, and an operator whose agent configuration is generated
declaratively cannot let it write at all. On such a machine both halves fail —
`setup hooks` against a read-only settings file, and `setup` on an unhandled
filesystem error — and the Agent Skill gets hand-copied into the operator's own
configuration, where it silently drifts from the package that ships it.

Three tasks follow:

- 0043 records the bare binary name, resolved through PATH, so the hook
  survives an upgrade on any wrapper-based install. Fixes the marker-substring
  coupling with it and drops the derivation's build-tree rename.
- 0044 reports an unwritable target as a structured error naming no cause,
  rather than crashing.
- 0045 adds the declarative install path: a stable Skill location, the Skill
  and hook specification exposed as Nix-consumable attributes, one committed
  hook specification read by both the expression and the test suite, and a
  home-manager module wiring them. Blocked by 0043, whose bare name the
  specification declares.

CONTEXT.md gains the four terms this settled and amends `setup` and
`SessionStart hook`, which described the imperative path as the only one.
Entries for unbuilt work name the task that lands them, so the glossary does
not assert behaviour the code lacks.

The re-run-after-upgrade help text this branch added stays as it is: accurate
until 0043 removes it, which that task carries as a criterion.
2026-07-20 12:29:42 -04:00

17 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/NIssue#N, https://<host>/<owner>/<repo>/pulls/NPR#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: successpass; failure/error/warningfail (matching Gitea's own Combine() logic, which treats warning as failure); skippedskip; pendingpending. 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, or declared from the package by the home-manager module. 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. It is the imperative install path, and works only where the operator owns the target files; against a read-only target it reports the condition rather than writing. 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), or declared by the home-manager module. 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. The recorded command is currently the entrypoint's absolute path on any wrapper-based install, which rots whenever that path moves; recording the bare binary name and resolving it through PATH is agreed and lands with task 0043. Avoid: session hook, ambient hook, postinstall hook

imperative install path: Installation of the Agent Skill and the SessionStart hook by running setup, which writes into the operator's agent configuration directory. Requires the operator to own those files; a declaratively generated configuration renders them read-only and the command reports rather than writes. Contrast the declarative install path. Both are supported and neither supersedes the other. Avoid: manual install, imperative setup

declarative install path: Installation of the Agent Skill and the SessionStart hook by declaring them in a Nix configuration, which generates the agent configuration rather than mutating it. Agreed and specified; the outputs it consumes land with task 0045. Consumes the package's exposed Skill location and hook specification, either directly or through the home-manager module. Chosen where the operator's agent configuration is generated and therefore read-only; contrast the imperative install path. Avoid: nix install, declarative setup

home-manager module: The flake output that declares the Agent Skill and the SessionStart hook from the package, as the declarative install path's ergonomic front end (task 0045). Importing it does nothing until enabled; it installs the package by default, with a null package the documented way to declare configuration without installing the binary, and carries a toggle per managed piece. Avoid: nix module, HM module

hook specification: The single committed declaration of the SessionStart hook's recorded shape — command, timeout, and matcher (task 0045). Read by both the Nix expression and the test suite, so that the declarative install path and the imperative install path cannot disagree about what the hook is without failing a test. Avoid: hook config, hook schema