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
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).
200 lines
18 KiB
Markdown
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
|