Files
gitea-axi/.claude/CONTEXT.md
alexion 534097121d
All checks were successful
CI / test (22) (pull_request) Successful in 52s
CI / test (true, 24) (pull_request) Successful in 1m4s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 48s
CI / test (true, 24) (push) Successful in 1m2s
CI / flake (push) Successful in 3s
feat(nix): install the CLI unconditionally, gate context per harness (task 0046)
Reshape the home-manager module so `programs.gitea-axi.enable` installs the
binary always, and the Claude Code context follows the harness. The two
per-artefact toggles and their assertion are replaced by one per-harness
toggle, `enableClaudeCodeIntegration` (default true); its artefacts land only
when `programs.claude-code.enable` is also on, silently absent otherwise.

The Agent Skill is now written through home.file into Claude Code's skills
directory, rather than contributed to `programs.claude-code.skills`, so it
composes with both the attribute-set and whole-directory forms of an operator's
own skills option. The Skill write is gated on `claude-code.enable` explicitly
(keeping package realisation lazy); the hook keeps its sibling-module gate for
free, and the asymmetry is commented. Supersedes three decisions of ADR 0020;
recorded in ADR 0021.

INSTALL.md is updated to the new option surface and the path-form limitation
paragraph removed. Adds the parent spec and the follow-up task 0047 (the flake
check proving composition, implemented separately).
2026-07-20 19:50:44 -04:00

200 lines
18 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, 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 `programs.gitea-axi.enable` is set, which installs the binary unconditionally; a null package is the documented way to declare the context without installing the binary.
The agent context is gated by one [[harness integration toggle]] per harness rather than a toggle per artefact (ADR 0021).
The hook is declared through `programs.claude-code`'s settings option so home-manager merges it with the operator's own; the Skill is written through home-manager's file mechanism directly, which composes with both forms of the operator's own skills option and fixes the path-form collision ADR 0020 could only escape.
_Avoid_: nix module, HM module
**harness integration toggle**: The module option that declares a harness's agent context — for Claude Code, `programs.gitea-axi.enableClaudeCodeIntegration`, defaulting on, covering both the [[Agent Skill]] and the [[SessionStart hook]] (ADR 0021).
Named after home-manager's own `enableBashIntegration` convention, so a future harness reads as an `enableCodexIntegration` sibling.
Both artefacts land only when the harness's own module is enabled, silently and without an assertion; the Skill carries an explicit `programs.claude-code.enable` gate because, unlike the hook, it does not inherit that module's own gate.
_Avoid_: skill toggle, hook toggle, per-artefact toggle
**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