docs: Requirements for the project.
This commit is contained in:
158
.claude/CONTEXT.md
Normal file
158
.claude/CONTEXT.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
### 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: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false`, and no non-dismissed `REQUEST_CHANGES` review exists; `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES` exists; otherwise `REVIEW_REQUIRED`.
|
||||||
|
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 (assignees, reviewers) where Gitea's PATCH replaces the entire list rather than adding/removing individual entries.
|
||||||
|
gitea-axi reads the current list first, computes the desired list, then sends a single PATCH with the full resulting list.
|
||||||
|
_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`, `issue list --label`).
|
||||||
|
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
|
||||||
10
.claude/adr/0001-diff-auth-via-tea-login-list.md
Normal file
10
.claude/adr/0001-diff-auth-via-tea-login-list.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Authenticate diff HTTP GET via `tea login list --output json`
|
||||||
|
|
||||||
|
`pr get --diff` fetches diff content with a direct HTTP GET, bypassing the tea subprocess.
|
||||||
|
To get the auth token for that request, gitea-axi calls `tea login list --output json`, matches the login entry whose URL matches the current repo's hostname, and uses its token.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Read `~/.config/tea/config.yml` directly** — one fewer subprocess call, but couples gitea-axi to tea's internal storage format rather than its stable JSON output interface.
|
||||||
|
|
||||||
|
**Shell out to `tea login list --output json`** (chosen) — consistent with the rest of the architecture, which goes through tea's JSON interface for everything; decoupled from tea's file format internals.
|
||||||
19
.claude/adr/0002-direct-gitea-api-over-tea-subprocess.md
Normal file
19
.claude/adr/0002-direct-gitea-api-over-tea-subprocess.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Use direct Gitea API (`gitea-js`) instead of wrapping the `tea` subprocess
|
||||||
|
|
||||||
|
gitea-axi calls the Gitea REST API directly via `gitea-js` rather than shelling out to the `tea` binary.
|
||||||
|
Tea was the original plan because it provides auth, multi-instance login, and full command coverage out of the box, but hands-on evaluation found too many gaps that made subprocess wrapping a patchwork rather than a clean pipeline.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Wrap `tea` with `--output json`** (rejected) — Tea's create commands (`issues create`, `pulls create`) have no `--output json` flag, requiring text parsing plus a follow-up get call. `pulls list` has no head-branch filter, forcing a full list scan for PR idempotency checks. Tea's JSON exposes no review counts or response totals. Diff content requires a direct HTTP GET regardless. Open issues for some of these gaps (#403 for non-interactive comments) have been stale for 3+ years, making upstream fixes an unreliable dependency.
|
||||||
|
|
||||||
|
**Contribute missing features to `tea` upstream, then wrap** — Viable long-term but blocks gitea-axi's timeline on upstream PR acceptance velocity, which is low.
|
||||||
|
|
||||||
|
**Direct Gitea API via `gitea-js`** (chosen) — Typed responses, `X-Total-Count` headers for true pagination totals, head-branch filtering on PR list, review counts, and immediate JSON from create operations. All gaps from the tea approach disappear. Auth still comes from tea's login store via `tea login list --output json`, so the operator's existing `tea` configuration is reused without gitea-axi owning a credential store.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Tea remains a runtime dependency for credential discovery only (`tea login list --output json`).
|
||||||
|
Operators must have `tea` installed and at least one login configured.
|
||||||
|
The `TEA_NOT_INSTALLED` error code covers the case where tea is absent.
|
||||||
|
Tea improvements (especially `--output json` on create commands) should still be contributed upstream as goodwill PRs, decoupled from gitea-axi's development.
|
||||||
19
.claude/adr/0003-inline-truncation-with-full-flag.md
Normal file
19
.claude/adr/0003-inline-truncation-with-full-flag.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Use inline truncation hints and `--full`, not a temp-file path
|
||||||
|
|
||||||
|
Body and diff text that exceeds the truncation limit is shortened inline with a hint appended to the field value — `"... (truncated, N chars total - use --full to see complete body)"`.
|
||||||
|
The `--full` flag on `issue view` and `pr view` suppresses truncation and returns the full raw value.
|
||||||
|
No temp file is written.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Save to a temp file, emit path in `full_content` field** (rejected) — The spec draft described this approach and attributed it to gh-axi, but that description was incorrect.
|
||||||
|
Temp-file paths create a coupling to local filesystem state that does not survive across sessions or machines, and agents cannot rely on file paths persisting between calls.
|
||||||
|
|
||||||
|
**Inline hint + `--full` flag** (chosen) — This is what gh-axi actually implements (`src/body.ts` → `truncateBody()`), and what the AXI principle 3 specifies: *"appending a size hint like '(truncated, 2847 chars total — use --full to see complete body)'"*.
|
||||||
|
Simpler, portable, consistent with the stated reference.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
`issue view` and `pr view` both accept `--full` to return untruncated body.
|
||||||
|
`pr diff` truncates at 4000 chars (matching gh-axi's `DIFF_TRUNCATE_LIMIT`); when truncated, a next-step suggestion to rerun with `--full` is prepended.
|
||||||
|
The `full_content` field name and temp-file design from the spec draft are dropped entirely.
|
||||||
24
.claude/adr/0004-use-axi-sdk-js-framework.md
Normal file
24
.claude/adr/0004-use-axi-sdk-js-framework.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Use `axi-sdk-js` as the CLI framework
|
||||||
|
|
||||||
|
gitea-axi is built on the `axi-sdk-js` npm package, matching gh-axi's architecture.
|
||||||
|
|
||||||
|
## What this provides
|
||||||
|
|
||||||
|
- `runAxiCli` — CLI runner handling `--help`, `--version`, home dispatch, error catch → stdout, and `process.exitCode`
|
||||||
|
- `AxiError` / `exitCodeForError` — typed error class and exit code mapping
|
||||||
|
- `renderOutput` / `renderError` / `homeHeaderOutput` — TOON output helpers
|
||||||
|
- Home-view `bin:` + `description:` header (AXI Principle 8)
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Reimplement locally** — Duplicates the CLI runner, error routing, and output helpers without benefit.
|
||||||
|
Likely to drift from the AXI standard over time.
|
||||||
|
|
||||||
|
**Use `axi-sdk-js`** (chosen) — Correct error routing (stdout, not stderr), consistent exit codes, and `--help`/`--version` handled for free.
|
||||||
|
Keeps gitea-axi aligned with the evolving AXI ecosystem without owning that surface.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Error output goes to stdout (not stderr), matching gh-axi and axi-sdk-js behaviour.
|
||||||
|
The home/dashboard view automatically gets a `bin:` + `description:` header (AXI Principle 8).
|
||||||
|
`VALIDATION_ERROR` exits with code 2; all other errors exit with code 1.
|
||||||
24
.claude/adr/0005-client-side-filtering-policy.md
Normal file
24
.claude/adr/0005-client-side-filtering-policy.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Client-side filtering when the Gitea API lacks a filter parameter
|
||||||
|
|
||||||
|
When a `--filter` flag has no corresponding query parameter in Gitea's API (e.g. `pr list --assignee`), gitea-axi paginates through all results and filters in-process.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Drop the flag** (rejected) — Maintaining interface parity with gh-axi is an explicit goal.
|
||||||
|
Dropping flags that gh-axi supports degrades usability and breaks agent prompts written for the gh-axi interface.
|
||||||
|
|
||||||
|
**Emit NOT_SUPPORTED error** (rejected) — Surfacing a capability gap as a runtime error is unhelpful; the agent asked for a filtered list and received nothing.
|
||||||
|
|
||||||
|
**Client-side filtering** (chosen) — Paginate all results (`limit=50` per page until exhausted), filter in-process, return the matching set.
|
||||||
|
This produces correct results at the cost of extra HTTP calls.
|
||||||
|
The spec explicitly calls out that API call cost on the gitea-axi side does not factor into design decisions.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- All unsupported filter flags still appear in the CLI surface with identical semantics to gh-axi.
|
||||||
|
- The count line emits `count: N of T total` with `T` computed from the in-memory filtered set when any client-side filter is active; the `X-Total-Count` header (which reflects the unfiltered total) is ignored as misleading.
|
||||||
|
Since client-side filtering paginates everything anyway, the true filtered total is always known — reporting it satisfies canonical Principle 4 ("always report total item count").
|
||||||
|
(Amended 2026-07-10: this originally specified a bare `count: N`, which under-reported a total the tool had already computed.)
|
||||||
|
- Client-side *sort* (`issue list --sort`) is not a filter: it reorders without changing membership, so the unfiltered total remains accurate and `T` comes from the `X-Total-Count` header as usual.
|
||||||
|
Sorting still requires full pagination before ordering, like filtering.
|
||||||
|
- Full pagination is bounded by the instance's total issue/PR count, which is acceptable for single-repo agent workflows.
|
||||||
25
.claude/adr/0006-reviewdecision-parallel-fetch.md
Normal file
25
.claude/adr/0006-reviewdecision-parallel-fetch.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Compute reviewDecision client-side via parallel review fetches on pr list
|
||||||
|
|
||||||
|
Gitea has no aggregated `reviewDecision` field (neither REST nor GraphQL).
|
||||||
|
gitea-axi computes it client-side from the reviews list and includes it as a default field on `pr list` and `pr view`, matching gh-axi's interface.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
For `pr list`, fetch the reviews list for each PR in parallel (one HTTP call per PR) alongside the main list call.
|
||||||
|
Derive `reviewDecision` using: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false` and no non-dismissed `REQUEST_CHANGES` exists; `CHANGES_REQUESTED` if any such `REQUEST_CHANGES` exists; `REVIEW_REQUIRED` otherwise.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Omit reviewDecision from default fields** (rejected) — The field is in gh-axi's default schema for `pr list`.
|
||||||
|
Dropping it breaks interface parity and forces agents to issue explicit follow-up calls.
|
||||||
|
|
||||||
|
**Include as opt-in `--fields` only** (rejected) — Same problem: agents trained on gh-axi expect it by default.
|
||||||
|
|
||||||
|
**Parallel fetch per PR** (chosen) — One extra HTTP call per PR in the list, all issued in parallel.
|
||||||
|
Accepted explicitly: API call cost does not factor into design decisions for this project.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `pr list` with N results makes N+1 HTTP calls (list + N review fetches).
|
||||||
|
- `official` and `stale` fields are exposed on `pr view --reviews` as Gitea-specific bonus data.
|
||||||
|
- The `reviewDecision` field appears in the default schema for both `pr list` and `pr view`.
|
||||||
29
.claude/adr/0007-fetch-then-patch-assignees-reviewers.md
Normal file
29
.claude/adr/0007-fetch-then-patch-assignees-reviewers.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# fetch-then-patch for additive/subtractive assignee and reviewer mutations
|
||||||
|
|
||||||
|
Gitea's PATCH endpoints for issues and PRs replace the entire assignee/reviewer list rather than adding or removing individual entries.
|
||||||
|
Flags like `issue edit --add-assignee` and `pr edit --add-reviewer` imply additive semantics: "add X to the current list, leave the rest alone."
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement additive and subtractive assignee/reviewer mutations as a fetch-then-patch:
|
||||||
|
1. Fetch the current entity (`GET .../issues/{index}` or `GET .../pulls/{index}`).
|
||||||
|
2. Compute the new list by applying the additions and removals to the current list.
|
||||||
|
3. Send a single PATCH with the resulting full list.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Single PATCH with only the new entries** (rejected) — Overwrites the existing list, dropping all current assignees/reviewers not mentioned in the command.
|
||||||
|
Correct for a "replace all" semantic but wrong for `--add` / `--remove` flags.
|
||||||
|
|
||||||
|
**Dedicated add/remove endpoints** (not available) — Gitea has additive label endpoints (`POST .../issues/{index}/labels`) but no equivalent for assignees or reviewers.
|
||||||
|
|
||||||
|
**fetch-then-patch** (chosen) — One extra GET per mutation.
|
||||||
|
Produces correct additive/subtractive semantics.
|
||||||
|
Accepted cost: same policy as client-side filtering — extra HTTP calls do not factor into design decisions.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every `--add-assignee`, `--remove-assignee`, `--add-reviewer`, `--remove-reviewer` call issues one extra GET.
|
||||||
|
- The operation is not atomic: a concurrent edit between the GET and the PATCH could cause a lost update.
|
||||||
|
Accepted as a known limitation for single-agent workflows.
|
||||||
|
- `issue label --add` / `--remove` does NOT use fetch-then-patch — Gitea has dedicated additive label endpoints that are already idempotent.
|
||||||
20
.claude/adr/0008-comment-block-name-normalization.md
Normal file
20
.claude/adr/0008-comment-block-name-normalization.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Normalize both issue comment and pr comment to the `comment` block name
|
||||||
|
|
||||||
|
gh-axi uses `comment` for `issue comment` output and `commented` for `pr comment` output.
|
||||||
|
gitea-axi normalizes both to `comment`, with the same schema: `{ number, author, created, body }`.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Match gh-axi exactly** (rejected) — `comment` for issue comment, `commented` for PR comment.
|
||||||
|
The inconsistency in gh-axi is an artifact of delegating to different `gh` subprocesses that return different data shapes, not an intentional design.
|
||||||
|
There is no semantic reason for the names to differ.
|
||||||
|
|
||||||
|
**`comment` for both** (chosen) — A single consistent block name for any "post a comment" operation.
|
||||||
|
gitea-axi gets the created `Comment` object directly from the Gitea API POST response for both issue and PR comments, so both can return the same schema without extra calls.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `issue comment` and `pr comment` both emit `comment: { number, author, created, body }` (body truncated at 800 chars).
|
||||||
|
- This is a deliberate interface divergence from gh-axi.
|
||||||
|
- `number` is used instead of gh-axi's `issue` alias, since the field applies to both issue and PR numbers.
|
||||||
|
- Agents get the posted comment's data immediately (AXI Principle 4 — eliminate round trips); no follow-up `view --comments` call needed to confirm what was posted.
|
||||||
30
.claude/adr/0009-setup-command-over-postinstall.md
Normal file
30
.claude/adr/0009-setup-command-over-postinstall.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Install the Agent Skill via an explicit `setup` command, not npm postinstall
|
||||||
|
|
||||||
|
gitea-axi fulfills AXI Principle 7 (Ambient context) through a `setup` subcommand that copies the bundled Agent Skill markdown into `~/.claude/skills/`.
|
||||||
|
There is no postinstall script.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**npm postinstall script** (rejected) — The original spec draft had `postinstall` drop the skill file automatically.
|
||||||
|
pnpm blocks lifecycle scripts by default and npm users increasingly install with `--ignore-scripts`, so the skill would silently fail to install for those users with no signal.
|
||||||
|
A package install silently writing into `~/.claude/` is also the exact pattern security tooling flags.
|
||||||
|
Finally, the canonical principle text asks for installation "from an explicit setup command" — postinstall is implicit.
|
||||||
|
|
||||||
|
**`setup` command** (chosen) — Matches gh-axi's command surface (its `cli.ts` registers `setup`), matches the canonical principle wording, works under pnpm and `--ignore-scripts`, and makes the `~/.claude/` write an explicit user action.
|
||||||
|
Discoverable via dashboard help suggestions.
|
||||||
|
|
||||||
|
**Both** (rejected) — Two install paths to test, and the postinstall path retains all its failure modes.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `npm install -g gitea-axi` delivers the CLI only; the skill requires a one-time `gitea-axi setup`.
|
||||||
|
- `setup` is idempotent: re-running reports already-installed/updated rather than failing.
|
||||||
|
- The dashboard suggestion table hints at `setup` so agents and operators discover it.
|
||||||
|
|
||||||
|
## Addendum (2026-07-10): opt-in `setup hooks`
|
||||||
|
|
||||||
|
Canonical Principle 7 makes SessionStart hooks the primary ambient-context mechanism, and gh-axi ships `setup hooks` via axi-sdk-js's `installSessionStartHooks()` (Claude Code, Codex, OpenCode).
|
||||||
|
gitea-axi adds the same opt-in `setup hooks`; the skill remains the default `setup` action.
|
||||||
|
|
||||||
|
Hooks are not the default because the hook runs the dashboard in every session in every directory, and outside a Gitea repo the dashboard errors with `REPO_NOT_FOUND` — a graceful exit-0 degradation was considered and rejected in favor of keeping the error explicit, so hook noise in non-Gitea sessions is an accepted consequence for users who opt in.
|
||||||
|
The SDK registers the bare binary as the hook command, so the hook always runs the short dashboard tier (see ADR 0012).
|
||||||
23
.claude/adr/0010-hard-deletes-refuse-missing-targets.md
Normal file
23
.claude/adr/0010-hard-deletes-refuse-missing-targets.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Hard deletes refuse missing targets instead of reporting idempotent success
|
||||||
|
|
||||||
|
`issue delete` on a nonexistent issue errors with `ISSUE_NOT_FOUND`; `label delete` on a nonexistent label errors with `VALIDATION_ERROR`.
|
||||||
|
This deliberately narrows a literal reading of AXI Principle 6 ("mutations should be idempotent").
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Idempotent success ("already deleted")** (rejected) — Consistent with the literal principle text and with the silent-success behavior of `--remove-label` and `blocks remove`.
|
||||||
|
But a missing hard-delete target usually means the agent's world-model is wrong (wrong number, wrong repo), and reporting success would confirm a false belief — the agent walks away thinking it deleted something it never identified correctly.
|
||||||
|
|
||||||
|
**Refuse with a specific error** (chosen) — Matches gh-axi's behavior for both commands.
|
||||||
|
The destructive command is exactly the one that should refuse to guess.
|
||||||
|
|
||||||
|
## The dividing line
|
||||||
|
|
||||||
|
Relationship removals (`--remove-label`, `blocks remove`, `blocked-by remove`) stay silent-success: the *entity* was correctly identified and fetched; only the relationship is absent, so the desired end state already holds.
|
||||||
|
Hard deletes error: the *target itself* is missing, which signals a stale or wrong reference rather than an already-achieved goal.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `issue delete <n>` on a missing issue → `ISSUE_NOT_FOUND` (falls out of the path-based 404 classification automatically).
|
||||||
|
- `label delete <name>` on a missing label → `VALIDATION_ERROR`, consistent with every other label-name lookup.
|
||||||
|
- Principle 6's idempotency guarantee is scoped in the spec: state transitions and relationship add/removes are idempotent; hard deletes are not.
|
||||||
19
.claude/adr/0011-pr-checkout-via-refs-pull-head.md
Normal file
19
.claude/adr/0011-pr-checkout-via-refs-pull-head.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# pr checkout fetches refs/pull/{index}/head, not the head branch name
|
||||||
|
|
||||||
|
`pr checkout <n>` runs `git fetch origin pull/<n>/head:<branch>` (branch named from the PR's `head.ref`) followed by `git checkout <branch>`.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**`git fetch origin <head-branch>`** (rejected — original spec draft) — Fails structurally for fork PRs: the head branch lives in the contributor's fork, which is not a configured remote in the operator's clone.
|
||||||
|
This is not an edge case; fork PRs are the default contribution model.
|
||||||
|
|
||||||
|
**Add the fork as a remote dynamically** (rejected) — Mutates the user's git configuration, requires cleanup, and needs credentials for the fork's clone URL.
|
||||||
|
|
||||||
|
**Fetch `refs/pull/{index}/head` from the base repo** (chosen) — Gitea, like GitHub, exposes every PR's head commit on the *base* repository under `refs/pull/{index}/head`, whether the head branch lives in the same repo or a fork.
|
||||||
|
One uniform code path, no remote mutation, no fork credentials.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Same-repo and fork PRs check out identically.
|
||||||
|
- Git subprocess failures (dirty worktree, network) classify as `GIT_ERROR`, carrying git's first stderr line.
|
||||||
|
- The created local branch does not track the contributor's fork; pushing back to a fork branch is out of scope.
|
||||||
23
.claude/adr/0012-two-tier-dashboard.md
Normal file
23
.claude/adr/0012-two-tier-dashboard.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Two-tier dashboard: gh-axi-sized default, `--full` for the rich view
|
||||||
|
|
||||||
|
The no-args home view has two tiers.
|
||||||
|
The short tier (bare `gitea-axi`, and what the SessionStart hook runs) matches gh-axi's home shape: up to 3 open issues and up to 3 open PRs.
|
||||||
|
The full tier (`gitea-axi --full`) is the rich view: a 20-row open-PR table with labels and review decision, plus open issue counts grouped by label (up to 1000 issues aggregated).
|
||||||
|
The short tier's help block always hints at `--full`.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Rich dashboard always** (rejected) — The original spec shape.
|
||||||
|
It is the heaviest command in the tool: one review fetch per listed PR (up to 20) plus up to 20 pages of issue aggregation.
|
||||||
|
With the opt-in SessionStart hook (see ADR 0009 addendum) it would run at every session start inside the SDK's 10-second hook timeout, and canonical Principle 7 asks for a "compact" dashboard.
|
||||||
|
|
||||||
|
**Short dashboard only** (rejected) — Drops the label-aggregation view entirely, losing the at-a-glance issue-state summary that motivated the rich dashboard.
|
||||||
|
|
||||||
|
**Two tiers** (chosen) — Cheap, hook-safe default with the rich view one flag away and discoverable via the default output's help block.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The SessionStart hook always runs the short tier, because the SDK registers the bare binary with no arguments.
|
||||||
|
- `--full` is intentionally overloaded: on view/diff commands it suppresses truncation; on the dashboard it selects the full tier.
|
||||||
|
- The short tier costs at most 5 HTTP calls (issues, PRs, up to 3 review fetches), comfortably inside the hook timeout.
|
||||||
|
- Dashboard empty states are `issues: 0 open` / `prs: 0 open` in both tiers, matching gh-axi's home view.
|
||||||
18
.claude/adr/0013-shadow-sdk-update-command.md
Normal file
18
.claude/adr/0013-shadow-sdk-update-command.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Shadow the axi-sdk-js `update` built-in
|
||||||
|
|
||||||
|
axi-sdk-js reserves `update` as a built-in self-update command (`RESERVED_COMMANDS`): it queries npmjs.org for the latest published version of the tool and updates the install, throwing its own `UPDATE_ERROR` code on failure.
|
||||||
|
gitea-axi shadows it with a handler that rejects the command.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Keep the built-in** (rejected) — Free functionality and consistent with other axi-sdk-js tools, but it silently adds an unspecced command to the surface and an eleventh error code (`UPDATE_ERROR`) to the documented ten.
|
||||||
|
Self-updating from inside unattended agent sessions is also a write to the operator's toolchain that should stay an explicit human action.
|
||||||
|
|
||||||
|
**Shadow it** (chosen) — `gitea-axi update` fails with `VALIDATION_ERROR` and a help line: `` Run `npm install -g gitea-axi@latest` to update ``.
|
||||||
|
The failure is instructive rather than an opaque unknown-command error.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The command surface and the ten-code `AxiError` list stay exactly as specified.
|
||||||
|
- Updating gitea-axi is always an explicit npm action.
|
||||||
|
- If the SDK's reserved-command list grows, each new built-in needs the same adopt-or-shadow decision.
|
||||||
1789
.claude/gh-axi-interface.md
Normal file
1789
.claude/gh-axi-interface.md
Normal file
File diff suppressed because it is too large
Load Diff
1337
.claude/gh-axi.md
Normal file
1337
.claude/gh-axi.md
Normal file
File diff suppressed because it is too large
Load Diff
983
.claude/gitea-api.md
Normal file
983
.claude/gitea-api.md
Normal file
@@ -0,0 +1,983 @@
|
|||||||
|
# Gitea REST API Reference
|
||||||
|
|
||||||
|
Source: gitea-js v1.23.0 TypeScript declarations (generated from Gitea OpenAPI spec).
|
||||||
|
Base URL: `https://<host>/api/v1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
**Header:** `Authorization: token <TOKEN>` — Gitea requires the word `token`, **not** `Bearer`.
|
||||||
|
|
||||||
|
**Token format:** SHA1 hash string (40 hex chars), e.g. `9fcb1158165773dd010fca5f0cf7174316c3e37d`.
|
||||||
|
Returned once on creation via `POST /users/{username}/tokens`; not stored in plain text.
|
||||||
|
|
||||||
|
**Token scopes** (Gitea 1.19+): fine-grained scopes like `read:issue`, `write:repository`, etc.
|
||||||
|
Older tokens have no scopes and are effectively admin-level for the user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
All list endpoints use `page` (1-based, default 1) and `limit` (page size).
|
||||||
|
**Not** `per_page` — GitHub uses `per_page`, Gitea uses `limit`.
|
||||||
|
|
||||||
|
**Response header:** `x-total-count` (lowercase) — total item count across all pages.
|
||||||
|
Also returns a `Link` header with `rel="next"` / `rel="last"` URLs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
### List Issues
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues
|
||||||
|
```
|
||||||
|
| Param | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `state` | `open\|closed\|all` | default `open` |
|
||||||
|
| `type` | `issues\|pulls` | filter by type |
|
||||||
|
| `labels` | string | comma-separated label names |
|
||||||
|
| `milestones` | string | comma-separated milestone names or IDs |
|
||||||
|
| `since` | date-time | RFC 3339; updated after |
|
||||||
|
| `before` | date-time | RFC 3339; updated before |
|
||||||
|
| `created_by` | string | filter by creator username |
|
||||||
|
| `assigned_by` | string | filter by assignee username |
|
||||||
|
| `mentioned_by` | string | filter by mentioned username |
|
||||||
|
| `page` | int | 1-based |
|
||||||
|
| `limit` | int | page size |
|
||||||
|
|
||||||
|
Returns: `Issue[]`
|
||||||
|
|
||||||
|
### Search Issues (cross-repo)
|
||||||
|
```
|
||||||
|
GET /repos/issues/search
|
||||||
|
```
|
||||||
|
| Param | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `state` | `open\|closed\|all` | default `open` |
|
||||||
|
| `type` | `issues\|pulls` | |
|
||||||
|
| `labels` | string | comma-separated |
|
||||||
|
| `milestones` | string | comma-separated |
|
||||||
|
| `q` | string | search string |
|
||||||
|
| `priority_repo_id` | int64 | repo ID to rank higher |
|
||||||
|
| `since` / `before` | date-time | |
|
||||||
|
| `assigned` | bool | assigned to authed user |
|
||||||
|
| `created` | bool | created by authed user |
|
||||||
|
| `mentioned` | bool | mentioning authed user |
|
||||||
|
| `review_requested` | bool | review requested from authed user |
|
||||||
|
| `reviewed` | bool | reviewed by authed user |
|
||||||
|
| `owner` | string | filter by repo owner |
|
||||||
|
| `team` | string | requires `owner` |
|
||||||
|
| `page` / `limit` | int | |
|
||||||
|
|
||||||
|
Returns: `Issue[]`
|
||||||
|
|
||||||
|
### Get Issue
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}
|
||||||
|
```
|
||||||
|
Returns: `Issue`
|
||||||
|
|
||||||
|
### Create Issue
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/issues
|
||||||
|
```
|
||||||
|
Body: `CreateIssueOption`
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `title` | string | yes | |
|
||||||
|
| `body` | string | no | |
|
||||||
|
| `assignees` | string[] | no | usernames |
|
||||||
|
| `assignee` | string | no | deprecated, use `assignees` |
|
||||||
|
| `milestone` | int64 | no | milestone ID |
|
||||||
|
| `labels` | number[] | no | label IDs |
|
||||||
|
| `due_date` | date-time | no | only date part used |
|
||||||
|
| `closed` | bool | no | create already-closed |
|
||||||
|
| `ref` | string | no | branch/commit ref |
|
||||||
|
|
||||||
|
Returns: `Issue` (HTTP 201)
|
||||||
|
|
||||||
|
### Edit Issue (close/reopen/update)
|
||||||
|
```
|
||||||
|
PATCH /repos/{owner}/{repo}/issues/{index}
|
||||||
|
```
|
||||||
|
Body: `EditIssueOption`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | string | |
|
||||||
|
| `body` | string | |
|
||||||
|
| `state` | string | `"open"` or `"closed"` — this is how you close/reopen |
|
||||||
|
| `assignees` | string[] | replaces all assignees |
|
||||||
|
| `assignee` | string | deprecated |
|
||||||
|
| `milestone` | int64 | milestone ID (0 to clear) |
|
||||||
|
| `due_date` | date-time | |
|
||||||
|
| `unset_due_date` | bool | set true to clear deadline |
|
||||||
|
| `ref` | string | |
|
||||||
|
|
||||||
|
Returns: `Issue`
|
||||||
|
|
||||||
|
### Delete Issue
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}
|
||||||
|
```
|
||||||
|
Returns: HTTP 204 (requires admin/owner)
|
||||||
|
|
||||||
|
### Pin / Unpin Issue
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/issues/{index}/pin
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}/pin
|
||||||
|
```
|
||||||
|
No body. Returns: HTTP 204
|
||||||
|
|
||||||
|
### Move Pin Position
|
||||||
|
```
|
||||||
|
PATCH /repos/{owner}/{repo}/issues/{index}/pin/{position}
|
||||||
|
```
|
||||||
|
`position` is a 1-based integer. Returns: HTTP 204
|
||||||
|
|
||||||
|
### List Pinned Issues
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/pinned
|
||||||
|
```
|
||||||
|
Returns: `Issue[]`
|
||||||
|
|
||||||
|
### Check New Pin Allowed
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/new_pin_allowed
|
||||||
|
```
|
||||||
|
Returns: `NewIssuePinsAllowed { issues: bool, pull_requests: bool }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Object Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Issue {
|
||||||
|
id?: number; // global DB ID (not the display number)
|
||||||
|
number?: number; // repo-scoped issue number (use this in URLs)
|
||||||
|
title?: string;
|
||||||
|
body?: string;
|
||||||
|
state?: StateType; // "open" | "closed"
|
||||||
|
user?: User; // creator
|
||||||
|
assignee?: User;
|
||||||
|
assignees?: User[];
|
||||||
|
labels?: Label[];
|
||||||
|
milestone?: Milestone;
|
||||||
|
comments?: number; // comment count
|
||||||
|
created_at?: string; // ISO 8601
|
||||||
|
updated_at?: string;
|
||||||
|
closed_at?: string;
|
||||||
|
due_date?: string;
|
||||||
|
pull_request?: PullRequestMeta; // non-null if this issue is a PR
|
||||||
|
is_locked?: boolean;
|
||||||
|
pin_order?: number; // 0 if not pinned; position otherwise
|
||||||
|
ref?: string;
|
||||||
|
repository?: RepositoryMeta;
|
||||||
|
original_author?: string; // for migrated issues
|
||||||
|
original_author_id?: number;
|
||||||
|
html_url?: string;
|
||||||
|
url?: string;
|
||||||
|
assets?: Attachment[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gitea-specific vs GitHub:**
|
||||||
|
- `number` is the repo-scoped index; `id` is the global DB ID. GitHub calls the display number `number` too, but Gitea has both.
|
||||||
|
- `pin_order` — no GitHub equivalent.
|
||||||
|
- `original_author` / `original_author_id` — for migrated content, no GitHub equivalent.
|
||||||
|
- `is_locked` is present but there's no dedicated lock/unlock endpoint in the public API.
|
||||||
|
- `due_date` — Gitea has native deadline support; GitHub does not.
|
||||||
|
- `StateType` is typed as `string` in TypeScript; values are `"open"` and `"closed"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Dependencies (Blocking)
|
||||||
|
|
||||||
|
Gitea models two directions: A **blocks** B (A must be resolved before B), and A **depends on** B.
|
||||||
|
From any issue's perspective: `/blocks` = issues that this issue blocks, `/dependencies` = issues that block this issue.
|
||||||
|
|
||||||
|
### List issues blocked BY this issue (this issue blocks them)
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}/blocks
|
||||||
|
```
|
||||||
|
Query: `page`, `limit`
|
||||||
|
Returns: `Issue[]` — the downstream issues that can't proceed until `{index}` is resolved.
|
||||||
|
|
||||||
|
### Add a blocking relationship (make this issue block another)
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/issues/{index}/blocks
|
||||||
|
```
|
||||||
|
Body: `IssueMeta { owner: string, repo: string, index: number }`
|
||||||
|
|
||||||
|
Returns: `Issue` (the issue that is now blocked)
|
||||||
|
|
||||||
|
**Note:** `{index}` in the URL path is typed as `string` in gitea-js (accepts number as string).
|
||||||
|
Body `IssueMeta.index` is `number`.
|
||||||
|
|
||||||
|
### Remove a blocking relationship
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}/blocks
|
||||||
|
```
|
||||||
|
Body: `IssueMeta { owner: string, repo: string, index: number }`
|
||||||
|
Returns: `Issue`
|
||||||
|
|
||||||
|
### List dependencies of this issue (issues that block this one)
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}/dependencies
|
||||||
|
```
|
||||||
|
Query: `page`, `limit`
|
||||||
|
Returns: `Issue[]` — issues that must be resolved before `{index}` can proceed.
|
||||||
|
|
||||||
|
### Add a dependency (make this issue depend on another)
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/issues/{index}/dependencies
|
||||||
|
```
|
||||||
|
Body: `IssueMeta { owner: string, repo: string, index: number }`
|
||||||
|
Returns: `Issue`
|
||||||
|
|
||||||
|
### Remove a dependency
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}/dependencies
|
||||||
|
```
|
||||||
|
Body: `IssueMeta { owner: string, repo: string, index: number }`
|
||||||
|
Returns: `Issue`
|
||||||
|
|
||||||
|
**Terminology clarification:**
|
||||||
|
- `GET /issues/{index}/blocks` → "issues blocked by {index}" = downstream dependents
|
||||||
|
- `GET /issues/{index}/dependencies` → "issues blocking {index}" = upstream blockers
|
||||||
|
- GitHub has no equivalent API; this is Gitea-only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Comments
|
||||||
|
|
||||||
|
### List comments on an issue
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}/comments
|
||||||
|
```
|
||||||
|
| Param | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `since` | date-time, RFC 3339 |
|
||||||
|
| `before` | date-time, RFC 3339 |
|
||||||
|
|
||||||
|
Returns: `Comment[]`
|
||||||
|
Note: no `page`/`limit` on this specific endpoint (lists all comments).
|
||||||
|
|
||||||
|
### List all comments in a repo
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/comments
|
||||||
|
```
|
||||||
|
Query: `since`, `before`, `page`, `limit`
|
||||||
|
Returns: `Comment[]`
|
||||||
|
|
||||||
|
### Get single comment
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/comments/{id}
|
||||||
|
```
|
||||||
|
Note: comment ID is the global DB ID, not a per-issue sequence.
|
||||||
|
Returns: `Comment`
|
||||||
|
|
||||||
|
### Create comment
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/issues/{index}/comments
|
||||||
|
```
|
||||||
|
Body: `{ body: string }` (required)
|
||||||
|
Returns: `Comment`
|
||||||
|
|
||||||
|
### Edit comment
|
||||||
|
```
|
||||||
|
PATCH /repos/{owner}/{repo}/issues/comments/{id}
|
||||||
|
```
|
||||||
|
Body: `{ body: string }` (required)
|
||||||
|
Returns: `Comment`
|
||||||
|
|
||||||
|
Deprecated variant: `PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id}`
|
||||||
|
|
||||||
|
### Delete comment
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/comments/{id}
|
||||||
|
```
|
||||||
|
Deprecated variant: `DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id}`
|
||||||
|
|
||||||
|
### Comment schema
|
||||||
|
```typescript
|
||||||
|
interface Comment {
|
||||||
|
id?: number; // global DB ID
|
||||||
|
body?: string;
|
||||||
|
user?: User;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
html_url?: string;
|
||||||
|
issue_url?: string;
|
||||||
|
pull_request_url?: string;
|
||||||
|
original_author?: string;
|
||||||
|
original_author_id?: number;
|
||||||
|
assets?: Attachment[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Timeline (comments + events)
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}/timeline
|
||||||
|
```
|
||||||
|
Query: `since`, `before`, `page`, `limit`
|
||||||
|
Returns: `TimelineComment[]` — includes all events (label changes, state changes, etc.) not just text comments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Labels
|
||||||
|
|
||||||
|
### Repo label CRUD
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/labels → Label[] (page, limit)
|
||||||
|
POST /repos/{owner}/{repo}/labels → Label (CreateLabelOption)
|
||||||
|
GET /repos/{owner}/{repo}/labels/{id} → Label
|
||||||
|
PATCH /repos/{owner}/{repo}/labels/{id} → Label (EditLabelOption)
|
||||||
|
DELETE /repos/{owner}/{repo}/labels/{id} → 204
|
||||||
|
```
|
||||||
|
|
||||||
|
### Label schema
|
||||||
|
```typescript
|
||||||
|
interface Label {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
color?: string; // hex without #, e.g. "00aabb"
|
||||||
|
description?: string;
|
||||||
|
exclusive?: boolean; // Gitea-only: exclusive label (scoped labels)
|
||||||
|
is_archived?: boolean; // Gitea-only: archived/hidden label
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gitea-specific:** `exclusive` labels are scoped — only one label with `exclusive=true` in a group can be applied at a time (like GitHub's scoped labels, but implemented differently). `is_archived` hides labels from UI while preserving existing uses.
|
||||||
|
|
||||||
|
### CreateLabelOption
|
||||||
|
```typescript
|
||||||
|
{ color: string, name: string, description?: string, exclusive?: boolean, is_archived?: boolean }
|
||||||
|
```
|
||||||
|
`color` must include the `#`, e.g. `"#00aabb"`.
|
||||||
|
|
||||||
|
### EditLabelOption
|
||||||
|
```typescript
|
||||||
|
{ color?: string, name?: string, description?: string, exclusive?: boolean, is_archived?: boolean }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Labels on Issues/PRs
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/issues/{index}/labels → Label[]
|
||||||
|
POST /repos/{owner}/{repo}/issues/{index}/labels → Label[] (add labels)
|
||||||
|
PUT /repos/{owner}/{repo}/issues/{index}/labels → Label[] (replace all labels)
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}/labels → 204 (remove ALL labels)
|
||||||
|
DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id} → 204 (remove one label)
|
||||||
|
```
|
||||||
|
|
||||||
|
Body for POST and PUT: `IssueLabelsOption`
|
||||||
|
```typescript
|
||||||
|
interface IssueLabelsOption {
|
||||||
|
labels?: (number | string)[]; // label IDs or label names (mixed array supported)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gitea-specific:** Labels can be specified by ID (int) or by name (string) in the same array.
|
||||||
|
GitHub only supports IDs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/milestones → Milestone[]
|
||||||
|
POST /repos/{owner}/{repo}/milestones → Milestone
|
||||||
|
GET /repos/{owner}/{repo}/milestones/{id} → Milestone
|
||||||
|
PATCH /repos/{owner}/{repo}/milestones/{id} → Milestone
|
||||||
|
DELETE /repos/{owner}/{repo}/milestones/{id} → 204
|
||||||
|
```
|
||||||
|
|
||||||
|
List query params: `state` (`open|closed|all`), `name` (filter by name), `page`, `limit`.
|
||||||
|
|
||||||
|
### Milestone schema
|
||||||
|
```typescript
|
||||||
|
interface Milestone {
|
||||||
|
id?: number;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
state?: StateType; // "open" | "closed"
|
||||||
|
open_issues?: number;
|
||||||
|
closed_issues?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
closed_at?: string;
|
||||||
|
due_on?: string; // Note: GitHub calls this "due_on" too
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CreateMilestoneOption
|
||||||
|
```typescript
|
||||||
|
{ title?: string, description?: string, due_on?: date-time, state?: 'open'|'closed' }
|
||||||
|
```
|
||||||
|
|
||||||
|
### EditMilestoneOption
|
||||||
|
```typescript
|
||||||
|
{ title?: string, description?: string, due_on?: date-time, state?: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
### List PRs
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls
|
||||||
|
```
|
||||||
|
| Param | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `state` | `open\|closed\|all` | default `open` |
|
||||||
|
| `sort` | string | `oldest\|recentupdate\|leastupdate\|mostcomment\|leastcomment\|priority` |
|
||||||
|
| `milestone` | int64 | milestone ID |
|
||||||
|
| `labels` | number[] | label IDs |
|
||||||
|
| `poster` | string | filter by PR author username |
|
||||||
|
| `page` | int | 1-based |
|
||||||
|
| `limit` | int | |
|
||||||
|
|
||||||
|
Returns: `PullRequest[]`
|
||||||
|
|
||||||
|
### Get PR
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}
|
||||||
|
```
|
||||||
|
Returns: `PullRequest`
|
||||||
|
|
||||||
|
### Get PR by base and head
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{base}/{head}
|
||||||
|
```
|
||||||
|
Returns: `PullRequest`
|
||||||
|
|
||||||
|
### Create PR
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls
|
||||||
|
```
|
||||||
|
Body: `CreatePullRequestOption`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | string | |
|
||||||
|
| `body` | string | |
|
||||||
|
| `head` | string | source branch (or `fork:branch`) |
|
||||||
|
| `base` | string | target branch |
|
||||||
|
| `assignees` | string[] | |
|
||||||
|
| `assignee` | string | deprecated |
|
||||||
|
| `labels` | number[] | label IDs |
|
||||||
|
| `milestone` | int64 | |
|
||||||
|
| `reviewers` | string[] | usernames |
|
||||||
|
| `team_reviewers` | string[] | team slugs |
|
||||||
|
| `due_date` | date-time | |
|
||||||
|
| `allow_maintainer_edit` | bool | (not in gitea-js CreatePullRequestOption but supported in API) |
|
||||||
|
|
||||||
|
**Draft PR:** The gitea-js `CreatePullRequestOption` does not include a `draft` field.
|
||||||
|
The underlying Go struct (`CreatePullRequestOption`) also has no `draft` field as of v1.23.
|
||||||
|
**There is no API to create a draft PR or convert draft to ready.** This is a known Gitea limitation.
|
||||||
|
|
||||||
|
Returns: `PullRequest`
|
||||||
|
|
||||||
|
### Edit PR
|
||||||
|
```
|
||||||
|
PATCH /repos/{owner}/{repo}/pulls/{index}
|
||||||
|
```
|
||||||
|
Body: `EditPullRequestOption`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | string | |
|
||||||
|
| `body` | string | |
|
||||||
|
| `state` | string | `"open"` or `"closed"` to close/reopen |
|
||||||
|
| `base` | string | change target branch |
|
||||||
|
| `assignees` | string[] | replaces all |
|
||||||
|
| `assignee` | string | deprecated |
|
||||||
|
| `labels` | number[] | replaces all |
|
||||||
|
| `milestone` | int64 | |
|
||||||
|
| `due_date` | date-time | |
|
||||||
|
| `unset_due_date` | bool | clear deadline |
|
||||||
|
| `allow_maintainer_edit` | bool | |
|
||||||
|
|
||||||
|
**No `draft` or `ready_for_review` field.** Draft status cannot be changed via API.
|
||||||
|
|
||||||
|
Returns: `PullRequest`
|
||||||
|
|
||||||
|
### Get PR diff / patch
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}.{diffType}
|
||||||
|
```
|
||||||
|
`diffType`: `diff` or `patch`
|
||||||
|
Query: `binary` (bool) — include binary changes (makes patch applicable via `git apply`)
|
||||||
|
Returns: raw string
|
||||||
|
|
||||||
|
### Get PR commits
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/commits
|
||||||
|
```
|
||||||
|
Query: `page`, `limit`, `verification` (bool, default true), `files` (bool, default true)
|
||||||
|
Returns: `Commit[]`
|
||||||
|
|
||||||
|
### Get changed files
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/files
|
||||||
|
```
|
||||||
|
| Param | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `skip-to` | filename to start from (cursor-style) |
|
||||||
|
| `whitespace` | `ignore-all\|ignore-change\|ignore-eol\|show-all` |
|
||||||
|
| `page`, `limit` | |
|
||||||
|
|
||||||
|
Returns: `ChangedFile[]`
|
||||||
|
|
||||||
|
### Check if merged
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/merge
|
||||||
|
```
|
||||||
|
Returns: HTTP 204 if merged, HTTP 404 if not.
|
||||||
|
|
||||||
|
### Merge PR
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/merge
|
||||||
|
```
|
||||||
|
Body: `MergePullRequestOption`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `Do` | string | **required**: `merge\|rebase\|rebase-merge\|squash\|fast-forward-only\|manually-merged` |
|
||||||
|
| `MergeCommitID` | string | for `manually-merged` |
|
||||||
|
| `MergeMessageField` | string | commit message body |
|
||||||
|
| `MergeTitleField` | string | commit message title |
|
||||||
|
| `delete_branch_after_merge` | bool | |
|
||||||
|
| `force_merge` | bool | override merge checks |
|
||||||
|
| `head_commit_id` | string | guard against race condition |
|
||||||
|
| `merge_when_checks_succeed` | bool | schedule auto-merge |
|
||||||
|
|
||||||
|
**Gitea-specific:** `manually-merged` value marks an already-merged PR without actually merging.
|
||||||
|
`fast-forward-only` is supported (not in GitHub API).
|
||||||
|
|
||||||
|
### Cancel auto-merge
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/pulls/{index}/merge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update PR branch (merge base into head)
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/update
|
||||||
|
```
|
||||||
|
Query: `style` — `merge` or `rebase`
|
||||||
|
|
||||||
|
### PullRequest schema
|
||||||
|
```typescript
|
||||||
|
interface PullRequest {
|
||||||
|
id?: number; // global DB ID
|
||||||
|
number?: number; // repo-scoped PR number
|
||||||
|
title?: string;
|
||||||
|
body?: string;
|
||||||
|
state?: StateType; // "open" | "closed"
|
||||||
|
draft?: boolean; // Gitea-specific: draft PR flag (read-only via API)
|
||||||
|
user?: User;
|
||||||
|
assignee?: User;
|
||||||
|
assignees?: User[];
|
||||||
|
labels?: Label[];
|
||||||
|
milestone?: Milestone;
|
||||||
|
base?: PRBranchInfo; // target branch info
|
||||||
|
head?: PRBranchInfo; // source branch info
|
||||||
|
merge_base?: string; // SHA of common ancestor
|
||||||
|
merge_commit_sha?: string; // SHA of merge commit (null if not merged)
|
||||||
|
merged?: boolean;
|
||||||
|
merged_at?: string;
|
||||||
|
merged_by?: User;
|
||||||
|
mergeable?: boolean;
|
||||||
|
allow_maintainer_edit?: boolean;
|
||||||
|
requested_reviewers?: User[];
|
||||||
|
requested_reviewers_teams?: Team[];
|
||||||
|
comments?: number;
|
||||||
|
review_comments?: number; // diff-level review comments only
|
||||||
|
additions?: number;
|
||||||
|
deletions?: number;
|
||||||
|
changed_files?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
closed_at?: string;
|
||||||
|
due_date?: string;
|
||||||
|
diff_url?: string;
|
||||||
|
patch_url?: string;
|
||||||
|
html_url?: string;
|
||||||
|
url?: string;
|
||||||
|
is_locked?: boolean;
|
||||||
|
pin_order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PRBranchInfo {
|
||||||
|
label?: string; // "owner:branch"
|
||||||
|
ref?: string; // branch name
|
||||||
|
sha?: string; // HEAD SHA
|
||||||
|
repo_id?: number;
|
||||||
|
repo?: Repository;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gitea-specific vs GitHub:**
|
||||||
|
- `draft` is present but **read-only** — cannot be set/changed via API.
|
||||||
|
- `pin_order` — no GitHub equivalent.
|
||||||
|
- `allow_maintainer_edit` — GitHub calls this `maintainer_can_modify`.
|
||||||
|
- GitHub has `review_decision` (computed field); Gitea does not — you must compute approval state from the reviews list.
|
||||||
|
- GitHub uses `head.repo` and `base.repo` as nested objects; Gitea uses the same pattern via `PRBranchInfo.repo`.
|
||||||
|
- `due_date` — Gitea-only deadline field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR Reviews
|
||||||
|
|
||||||
|
### List reviews
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/reviews
|
||||||
|
```
|
||||||
|
Query: `page`, `limit`
|
||||||
|
Returns: `PullReview[]`
|
||||||
|
|
||||||
|
### Create review (or start a pending review)
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/reviews
|
||||||
|
```
|
||||||
|
Body: `CreatePullReviewOptions`
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `event` | ReviewStateType | `APPROVED\|REQUEST_CHANGES\|COMMENT\|PENDING` |
|
||||||
|
| `body` | string | overall review comment |
|
||||||
|
| `commit_id` | string | commit to review at (defaults to head) |
|
||||||
|
| `comments` | CreatePullReviewComment[] | inline diff comments |
|
||||||
|
|
||||||
|
`CreatePullReviewComment`:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path?: string; // file path
|
||||||
|
body?: string; // comment text
|
||||||
|
new_position?: number; // line in new file (0 = not a line comment)
|
||||||
|
old_position?: number; // line in old file (0 = not a line comment)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To create a **pending** review (accumulate comments before submitting), pass `event: "PENDING"` or omit `event`.
|
||||||
|
Returns: `PullReview`
|
||||||
|
|
||||||
|
### Get a review
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}
|
||||||
|
```
|
||||||
|
Returns: `PullReview`
|
||||||
|
|
||||||
|
### Submit (publish) a pending review
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}
|
||||||
|
```
|
||||||
|
Body: `SubmitPullReviewOptions`
|
||||||
|
```typescript
|
||||||
|
{ body?: string, event?: ReviewStateType }
|
||||||
|
```
|
||||||
|
`event` values: `APPROVED`, `REQUEST_CHANGES`, `COMMENT`
|
||||||
|
Returns: `PullReview`
|
||||||
|
|
||||||
|
### Delete a review
|
||||||
|
```
|
||||||
|
DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id}
|
||||||
|
```
|
||||||
|
Returns: HTTP 204
|
||||||
|
|
||||||
|
### Get review inline comments
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments
|
||||||
|
```
|
||||||
|
Returns: `PullReviewComment[]`
|
||||||
|
|
||||||
|
### Dismiss a review
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals
|
||||||
|
```
|
||||||
|
Body: `DismissPullReviewOptions`
|
||||||
|
```typescript
|
||||||
|
{ message?: string, priors?: boolean }
|
||||||
|
```
|
||||||
|
`priors: true` dismisses all prior reviews from the same reviewer.
|
||||||
|
Returns: `PullReview`
|
||||||
|
|
||||||
|
### Un-dismiss a review
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals
|
||||||
|
```
|
||||||
|
Returns: `PullReview`
|
||||||
|
|
||||||
|
### Request / cancel review requests
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers → PullReview[]
|
||||||
|
DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers → 204
|
||||||
|
```
|
||||||
|
Body: `PullReviewRequestOptions { reviewers?: string[], team_reviewers?: string[] }`
|
||||||
|
|
||||||
|
### PullReview schema
|
||||||
|
```typescript
|
||||||
|
interface PullReview {
|
||||||
|
id?: number;
|
||||||
|
user?: User;
|
||||||
|
team?: Team; // for team review requests
|
||||||
|
body?: string; // overall comment text
|
||||||
|
state?: ReviewStateType; // "APPROVED" | "REQUEST_CHANGES" | "COMMENT" | "PENDING"
|
||||||
|
commit_id?: string; // commit SHA the review is on
|
||||||
|
submitted_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
stale?: boolean; // GITEA-SPECIFIC: true if head has moved since review
|
||||||
|
official?: boolean; // GITEA-SPECIFIC: counts toward required approvals
|
||||||
|
dismissed?: boolean;
|
||||||
|
comments_count?: number;
|
||||||
|
html_url?: string;
|
||||||
|
pull_request_url?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gitea-specific vs GitHub:**
|
||||||
|
- `official` — whether this review counts toward branch protection required approvals. No GitHub equivalent.
|
||||||
|
- `stale` — whether the PR head has been updated since this review was submitted, making it outdated. GitHub shows this in UI but doesn't expose it as a field.
|
||||||
|
- **No `reviewDecision` equivalent** — Gitea has no computed "overall review decision" field on the PR object. You must fetch all reviews and compute:
|
||||||
|
- Count `APPROVED` reviews where `official=true` and `stale=false` and `dismissed=false`.
|
||||||
|
- Check if any `REQUEST_CHANGES` review is `official=true` and not dismissed.
|
||||||
|
- `ReviewStateType` string values: `"APPROVED"`, `"REQUEST_CHANGES"`, `"COMMENT"`, `"PENDING"`
|
||||||
|
|
||||||
|
### PullReviewComment schema
|
||||||
|
```typescript
|
||||||
|
interface PullReviewComment {
|
||||||
|
id?: number;
|
||||||
|
body?: string;
|
||||||
|
path?: string;
|
||||||
|
position?: number; // line in new file
|
||||||
|
original_position?: number; // line in old file
|
||||||
|
diff_hunk?: string; // context diff around the comment
|
||||||
|
commit_id?: string;
|
||||||
|
original_commit_id?: string;
|
||||||
|
pull_request_review_id?: number;
|
||||||
|
pull_request_url?: string;
|
||||||
|
user?: User;
|
||||||
|
resolver?: User; // GITEA-SPECIFIC: user who resolved the thread
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
html_url?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Statuses (CI checks)
|
||||||
|
|
||||||
|
Gitea uses commit statuses (like GitHub's commit status API), **not** GitHub Check Runs.
|
||||||
|
There is no Gitea equivalent to `GET /check-runs` or `GET /check-suites`.
|
||||||
|
|
||||||
|
### List statuses by ref (branch/tag/commit)
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/commits/{ref}/statuses
|
||||||
|
```
|
||||||
|
| Param | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `sort` | `oldest\|recentupdate\|leastupdate\|leastindex\|highestindex` |
|
||||||
|
| `state` | `pending\|success\|error\|failure\|warning` |
|
||||||
|
| `page`, `limit` | |
|
||||||
|
|
||||||
|
Returns: `CommitStatus[]`
|
||||||
|
|
||||||
|
### Get combined status by ref
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/commits/{ref}/status
|
||||||
|
```
|
||||||
|
Returns: `CombinedStatus`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CombinedStatus {
|
||||||
|
sha?: string;
|
||||||
|
state?: CommitStatusState; // overall: worst of all statuses
|
||||||
|
statuses?: CommitStatus[];
|
||||||
|
total_count?: number;
|
||||||
|
repository?: Repository;
|
||||||
|
commit_url?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List statuses by SHA
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/statuses/{sha}
|
||||||
|
```
|
||||||
|
Same query params as above. Returns: `CommitStatus[]`
|
||||||
|
|
||||||
|
### Create a commit status
|
||||||
|
```
|
||||||
|
POST /repos/{owner}/{repo}/statuses/{sha}
|
||||||
|
```
|
||||||
|
Body: `CreateStatusOption`
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
context?: string; // e.g. "ci/test" — identifies the check
|
||||||
|
state?: CommitStatusState; // "pending" | "success" | "error" | "failure"
|
||||||
|
description?: string;
|
||||||
|
target_url?: string; // link to build/CI page
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Returns: `CommitStatus`
|
||||||
|
|
||||||
|
### CommitStatus schema
|
||||||
|
```typescript
|
||||||
|
interface CommitStatus {
|
||||||
|
id?: number;
|
||||||
|
context?: string; // identifier, e.g. "ci/build"
|
||||||
|
status?: CommitStatusState; // "pending" | "success" | "error" | "failure"
|
||||||
|
description?: string;
|
||||||
|
target_url?: string;
|
||||||
|
creator?: User;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CommitStatusState values:** `"pending"`, `"success"`, `"error"`, `"failure"`, `"warning"`, `"skipped"`
|
||||||
|
Note: `"warning"` is valid in list/filter but the spec comment says "pending, success, error and failure" for CreateStatusOption — `warning` may not be creatable.
|
||||||
|
Note: `"skipped"` exists in current Gitea server code but is absent from older OpenAPI specs; older instances never emit it.
|
||||||
|
Gitea's own combine logic treats `warning` as a failing state and `skipped` as compatible with success.
|
||||||
|
|
||||||
|
**For PR CI status:** Use `GET /repos/{owner}/{repo}/commits/{ref}/status` where `ref` is the PR head SHA (`pr.head.sha`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
Gitea has a projects feature (kanban boards) but **no REST API endpoints for projects** are exposed as of v1.23.
|
||||||
|
- `project_id` appears in `TimelineComment` (for "moved to project" events) but there's no CRUD API.
|
||||||
|
- Repository settings expose `has_projects` (bool) and `projects_mode` (`"repo"|"owner"|"all"`).
|
||||||
|
- Project management must be done through the web UI.
|
||||||
|
|
||||||
|
**GitHub comparison:** GitHub has a full Projects v2 GraphQL API and Projects REST API. Gitea has neither.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Draft Pull Requests
|
||||||
|
|
||||||
|
Gitea supports draft PRs in the web UI but has **significant API limitations:**
|
||||||
|
|
||||||
|
1. **Cannot create a draft PR via API** — `CreatePullRequestOption` has no `draft` field.
|
||||||
|
2. **Cannot convert draft to ready via API** — `EditPullRequestOption` has no `draft` or `ready_for_review` field.
|
||||||
|
3. **Can read draft status** — `PullRequest.draft` is a readable boolean field.
|
||||||
|
4. **Workaround:** Title prefix convention — some users prefix draft PR titles with `[WIP]` or `Draft:` and remove the prefix to signal readiness, but this is not enforced by the API.
|
||||||
|
|
||||||
|
The `op_type` enum in activity includes `pull_request_ready_for_review`, indicating the feature exists in the event log, but no API to trigger this transition is exposed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR Comments (Review Comments vs Issue Comments)
|
||||||
|
|
||||||
|
Gitea separates:
|
||||||
|
|
||||||
|
1. **Issue-style PR comments** (general comments, not tied to diff lines):
|
||||||
|
- `GET/POST /repos/{owner}/{repo}/issues/{index}/comments`
|
||||||
|
- Same `Comment` schema as issue comments.
|
||||||
|
|
||||||
|
2. **Review comments** (diff-level, tied to a review):
|
||||||
|
- `GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments`
|
||||||
|
- Created as part of `CreatePullReviewOptions.comments[]`.
|
||||||
|
- Schema: `PullReviewComment`.
|
||||||
|
|
||||||
|
There is no endpoint to create a standalone review comment outside of a review (unlike GitHub's `POST /pulls/{index}/comments`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Endpoint Index (Relevant to gitea-axi)
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues` | list; `type=issues` for issues only |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues` | create |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}` | get |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/issues/{index}` | edit; use `state` to close/reopen |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}` | delete (admin) |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues/{index}/pin` | pin |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}/pin` | unpin |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/issues/{index}/pin/{position}` | move pin |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}/blocks` | list blocked issues |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues/{index}/blocks` | add blocking |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}/blocks` | remove blocking |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}/dependencies` | list blockers |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues/{index}/dependencies` | add dependency |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}/dependencies` | remove dependency |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}/comments` | list comments |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues/{index}/comments` | add comment |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/comments/{id}` | get comment |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/issues/comments/{id}` | edit comment |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/comments/{id}` | delete comment |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}/timeline` | comments + events |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/{index}/labels` | get labels |
|
||||||
|
| POST | `/repos/{owner}/{repo}/issues/{index}/labels` | add labels |
|
||||||
|
| PUT | `/repos/{owner}/{repo}/issues/{index}/labels` | replace labels |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}/labels` | clear all labels |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/issues/{index}/labels/{id}` | remove one label |
|
||||||
|
| GET | `/repos/issues/search` | cross-repo search |
|
||||||
|
| GET | `/repos/{owner}/{repo}/issues/pinned` | list pinned |
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls` | list |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls` | create |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}` | get |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/pulls/{index}` | edit |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}.diff` | get diff |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}.patch` | get patch |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/commits` | get commits |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/files` | get changed files |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/merge` | check if merged |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/merge` | merge |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/pulls/{index}/merge` | cancel auto-merge |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/update` | sync base into head |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{base}/{head}` | get by branches |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/reviews` | list reviews |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/reviews` | create review |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}` | get review |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}` | submit pending review |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}` | delete review |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments` | get review comments |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals` | dismiss review |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals` | undismiss review |
|
||||||
|
| POST | `/repos/{owner}/{repo}/pulls/{index}/requested_reviewers` | request reviewers |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/pulls/{index}/requested_reviewers` | cancel review request |
|
||||||
|
| GET | `/repos/{owner}/{repo}/pulls/pinned` | list pinned PRs |
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/repos/{owner}/{repo}/labels` | list |
|
||||||
|
| POST | `/repos/{owner}/{repo}/labels` | create |
|
||||||
|
| GET | `/repos/{owner}/{repo}/labels/{id}` | get |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/labels/{id}` | update |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/labels/{id}` | delete |
|
||||||
|
|
||||||
|
### Milestones
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/repos/{owner}/{repo}/milestones` | list (`state`, `name`, `page`, `limit`) |
|
||||||
|
| POST | `/repos/{owner}/{repo}/milestones` | create |
|
||||||
|
| GET | `/repos/{owner}/{repo}/milestones/{id}` | get |
|
||||||
|
| PATCH | `/repos/{owner}/{repo}/milestones/{id}` | update |
|
||||||
|
| DELETE | `/repos/{owner}/{repo}/milestones/{id}` | delete |
|
||||||
|
|
||||||
|
### Commit Statuses
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/repos/{owner}/{repo}/commits/{ref}/status` | combined status |
|
||||||
|
| GET | `/repos/{owner}/{repo}/commits/{ref}/statuses` | list by ref |
|
||||||
|
| GET | `/repos/{owner}/{repo}/statuses/{sha}` | list by SHA |
|
||||||
|
| POST | `/repos/{owner}/{repo}/statuses/{sha}` | create status |
|
||||||
242
.claude/gitea-vs-github.md
Normal file
242
.claude/gitea-vs-github.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# Gitea vs GitHub: API Feature Comparison for CLI Agent Tooling
|
||||||
|
|
||||||
|
Focus: features relevant to `gitea-axi` — issues, pull requests, labels, reviews, comments, auth, and API mechanics.
|
||||||
|
Sources: Gitea OpenAPI spec (docs.gitea.com/api/1.21), GitHub REST docs, Gitea source structs (`modules/structs/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Style
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| REST API | Yes — `/api/v1` (actually `/repos/...` at api.github.com) | Yes — `/api/v1/repos/...` | Gitea mirrors GitHub's path structure closely |
|
||||||
|
| GraphQL API | Yes — `api.github.com/graphql` | **No** | Gitea is REST-only; no GraphQL endpoint exists |
|
||||||
|
| OpenAPI spec | Unofficial/community | Yes — `/api/swagger` on every instance | Gitea ships a first-class Swagger UI and JSON spec |
|
||||||
|
| TypeScript client | `@octokit/rest` (hand-written) | `gitea-js` (generated from OpenAPI) | `gitea-js` types are auto-generated and stay in sync with the spec |
|
||||||
|
| API versioning | `api-version` header (`2026-03-10`) | Path-prefixed (`/api/v1`) | GitHub uses a header; Gitea uses path versioning |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Personal access tokens (classic) | Yes — broad scope-based | Yes — scope-based (`read:issue`, `write:repository`, etc.) | Gitea scopes map to API route groups |
|
||||||
|
| Fine-grained PATs | Yes — per-repo permissions, expiry required | **No** — scopes are coarser (category-level, not per-repo) | GitHub fine-grained tokens are more restrictive by default |
|
||||||
|
| Token creation via API | No (web only for fine-grained) | **Yes** — `POST /user/tokens` | Gitea allows programmatic token creation with BasicAuth |
|
||||||
|
| Token listing/deletion via API | No | **Yes** — `GET /user/tokens`, `DELETE /user/tokens/{id}` | Gitea exposes full token lifecycle over the API |
|
||||||
|
| OAuth App device flow | **Yes** | **No** — only Authorization Code (+ PKCE) | GitHub device flow is essential for headless CLI auth; Gitea lacks it |
|
||||||
|
| OAuth App web flow | Yes | Yes | Both support standard Authorization Code grant |
|
||||||
|
| OAuth App management via API | No | **Yes** — `GET/POST/PATCH/DELETE /user/applications/oauth2` | Gitea lets users manage their own OAuth apps via API |
|
||||||
|
| GitHub Apps / installation tokens | Yes | **No** — no GitHub Apps concept | Gitea has no equivalent of GitHub Apps or installation tokens |
|
||||||
|
| `GITHUB_TOKEN` in Actions | Yes | No (`GITEA_TOKEN` in Gitea Actions is similar but not identical) | Gitea Actions has a built-in `GITEA_TOKEN` but it is instance-specific |
|
||||||
|
| HTTP Signatures (SSH key auth) | No | **Yes** | Gitea accepts signatures per draft-cavage-http-signatures |
|
||||||
|
| Basic auth | Deprecated/removed | Yes (for token creation only; not recommended for general use) | |
|
||||||
|
| Sudo (act-as) | No | **Yes** — `?sudo=username` for admins | Admin-only; useful for automation |
|
||||||
|
| SAML SSO token authorization | Yes (org-enforced) | No | Gitea has no SAML SSO concept |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Offset/page pagination | Yes — `?page=N&per_page=N` (max 100) | Yes — `?page=N&limit=N` | Same concept; parameter names differ (`per_page` vs `limit`) |
|
||||||
|
| Cursor-based pagination | Some endpoints (`before`/`after`) | **No** | Gitea is page-offset only |
|
||||||
|
| `Link` header (RFC 5988) | **Yes** — `next`, `prev`, `last`, `first` | **Yes** — same format | Both provide `Link` headers for navigation |
|
||||||
|
| `X-Total-Count` response header | **No** | **Yes** | Gitea returns total count in every list response; GitHub does not |
|
||||||
|
| `x-total-count` in `gitea-js` | n/a | Available as a parsed header | The `gitea-js` client exposes this alongside the response body |
|
||||||
|
| Maximum per-page results | 100 | Configurable (instance default ~50, max configurable) | Gitea's max is admin-controlled; no hard 100 cap in the API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Per-user rate limit | 5,000 req/hr (authenticated) | **No standard rate limiting** | Gitea uses QoS concurrency throttling, not per-user counters |
|
||||||
|
| `X-RateLimit-*` headers | Yes — `Limit`, `Remaining`, `Used`, `Reset` | **No** | Gitea does not emit rate-limit headers |
|
||||||
|
| Secondary limits | Yes (content creation, concurrency) | No | |
|
||||||
|
| QoS / overload protection | No | **Yes** — configurable `[qos]` section | Gitea drops or queues requests under load rather than rate-limiting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Management
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Create issue | Yes — `POST /repos/{owner}/{repo}/issues` | Yes — same path pattern | |
|
||||||
|
| List issues | Yes | Yes | |
|
||||||
|
| Get single issue | Yes | Yes | |
|
||||||
|
| Edit issue | Yes | Yes | |
|
||||||
|
| Delete issue | **No** (close only) | **Yes** — `DELETE /repos/{owner}/{repo}/issues/{index}` | Gitea allows hard deletion |
|
||||||
|
| Issue state | `open` / `closed` | `open` / `closed` | Same values |
|
||||||
|
| `state_reason` | **Yes** — `completed`, `not_planned`, `duplicate`, `reopened` | **No** | Gitea has no close-reason concept |
|
||||||
|
| Issue type | **Yes** — first-class `type` field on create/edit | **No** | GitHub supports issue type classification; Gitea does not |
|
||||||
|
| `duplicate_issue_id` | **Yes** | **No** | |
|
||||||
|
| Issue dependencies (blocking) | No (sub-issue relationships only) | **Yes** — `POST /repos/{owner}/{repo}/issues/{index}/dependencies` | Gitea has explicit blocking/blocked-by relationships between issues |
|
||||||
|
| Issue blocking list | No | **Yes** — `GET /repos/{owner}/{repo}/issues/{index}/blocked-by` | |
|
||||||
|
| Pin issue | Partial (read-only in API response) | **Yes** — `POST/DELETE /repos/{owner}/{repo}/issues/{index}/pin`, position reorder | Gitea has full pin CRUD including ordering |
|
||||||
|
| Issue timeline | Yes — 30+ event types (labels, assignments, reviews, etc.) | Yes — `GET /repos/{owner}/{repo}/issues/{index}/timeline` (comments + events) | GitHub's timeline is richer in event type variety |
|
||||||
|
| Reactions on issues | Yes — 8 types | Yes — same types | |
|
||||||
|
| Reactions on issue comments | Yes | Yes | |
|
||||||
|
| Lock/unlock conversation | Yes — with `lock_reason` (`off-topic`, `too heated`, `resolved`, `spam`) | **No** — issues have `IsLocked` field but no dedicated lock endpoint visible | |
|
||||||
|
| Subscriptions (watching) | Via notifications API | **Yes** — explicit `PUT/DELETE /repos/{owner}/{repo}/issues/{index}/subscriptions` | Gitea exposes subscribe/unsubscribe directly on issues |
|
||||||
|
| Issue search (cross-repo) | Yes — `GET /search/issues` with qualifiers | Yes — `GET /issues/search` | Both support cross-repo search |
|
||||||
|
| Custom field values | **Yes** (org repos) | **No** | GitHub-specific feature |
|
||||||
|
| Time tracking (estimate) | No | **Yes** — `TimeEstimate` field on Issue struct | Gitea has built-in time tracking |
|
||||||
|
| Content versioning (conflict detect) | No | **Yes** — `ContentVersion` field on create/edit | Gitea supports optimistic locking for concurrent edits |
|
||||||
|
| Original author tracking (migrations) | No | **Yes** — `OriginalAuthor` / `OriginalAuthorID` fields | Preserved when importing from other platforms |
|
||||||
|
| Issue `Ref` field | No | **Yes** — git ref associated with issue | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Milestone CRUD | Yes — `GET/POST/PATCH/DELETE /repos/{owner}/{repo}/milestones` | Yes — same path pattern | |
|
||||||
|
| Milestone on issues | Yes | Yes | |
|
||||||
|
| Milestone on PRs | Yes | Yes — `milestone` field in `CreatePullRequestOption` and `EditPullRequestOption` | |
|
||||||
|
| Milestone properties | `title`, `description`, `state`, `due_on` | Same | |
|
||||||
|
| Org-level milestones | No | No | Neither supports org-level milestones |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Label Management
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Repo label CRUD | Yes — `GET/POST/PATCH/DELETE /repos/{owner}/{repo}/labels` | Yes — same path pattern | |
|
||||||
|
| Org-level labels | No | **Yes** — `GET/POST/PATCH/DELETE /orgs/{org}/labels` | Gitea supports organization-scoped labels |
|
||||||
|
| Label properties | `name`, `color`, `description` | `name`, `color`, `description` | |
|
||||||
|
| Add/remove labels on issue | Yes | Yes | |
|
||||||
|
| Add/remove labels on PR | Yes | Yes | |
|
||||||
|
| Replace all labels | Yes | Yes | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Request Management
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Create PR | Yes | Yes | |
|
||||||
|
| List PRs | Yes | Yes | |
|
||||||
|
| Get PR | Yes | Yes | |
|
||||||
|
| Edit PR | Yes | Yes | |
|
||||||
|
| Delete PR | No | No | Neither allows hard deletion |
|
||||||
|
| Draft PRs | **Yes** — `draft: true` on create | **Yes** — `Draft bool` field | Both support drafts |
|
||||||
|
| Draft PR filter in list | Yes | Yes — `draft` query param available | |
|
||||||
|
| `maintainer_can_modify` | Yes | **Yes** — `AllowMaintainerEdit` field | Same concept, different name |
|
||||||
|
| `ContentVersion` conflict detection | No | **Yes** | Same optimistic-locking field as issues |
|
||||||
|
| Pin PR | No | **Yes** — same pin endpoints as issues, `PinOrder` field | |
|
||||||
|
| Get PR diff | Yes — `Accept: application/vnd.github.diff` | **Yes** — `GET /repos/{owner}/{repo}/pulls/{index}/diff` | Gitea returns raw diff text |
|
||||||
|
| Get PR patch | Yes — `Accept: application/vnd.github.patch` | Yes — `GET /repos/{owner}/{repo}/pulls/{index}/diff` with format param | |
|
||||||
|
| List PR files (structured) | Yes — `GET /pulls/{number}/files` | **Yes** — `GET /repos/{owner}/{repo}/pulls/{index}/files` | Returns file list with additions/deletions |
|
||||||
|
| List PR commits | Yes | **Yes** — `GET /repos/{owner}/{repo}/pulls/{index}/commits` | |
|
||||||
|
| Mergeable status | Yes — `mergeable` field | **Yes** — `Mergeable bool` field on PR | |
|
||||||
|
| Merge strategies | merge, squash, rebase | merge, squash, rebase, **rebase-merge** (rebase + no-ff), **fast-forward-only** | **Gitea has more merge strategies** |
|
||||||
|
| Squash merge | Yes | Yes | |
|
||||||
|
| Rebase merge | Yes | Yes | |
|
||||||
|
| Fast-forward-only | No | **Yes** | |
|
||||||
|
| Rebase + explicit merge commit | No | **Yes** — `rebase-merge` style | |
|
||||||
|
| Auto-merge (merge when checks pass) | Yes | **Yes** — `MergeWhenChecksSucceed` field | Both support scheduling a merge after CI passes |
|
||||||
|
| Cancel auto-merge | Yes | Yes — `DELETE /repos/{owner}/{repo}/pulls/{index}/merge` | |
|
||||||
|
| Delete branch after merge | Yes | **Yes** — `DeleteBranchAfterMerge` field on merge request | |
|
||||||
|
| Force merge | Limited (bypass protections with permissions) | **Yes** — `ForceMerge bool` field | Gitea has an explicit force-merge flag |
|
||||||
|
| Manually merged (mark as merged) | No | **Yes** — `Do: "manually-merged"` + `MergeCommitID` field | Gitea allows recording a manual merge after the fact |
|
||||||
|
| `HeadCommitSHA` validation on merge | No | **Yes** — `HeadCommitID` field for merge safety | |
|
||||||
|
| PR merge commit message/title | Yes — `commit_title`, `commit_message` | **Yes** — `MergeTitleField`, `MergeMessageField` | |
|
||||||
|
| Pinned PRs list | No | **Yes** — `GET /repos/{owner}/{repo}/pulls/pinned` | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review Management
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| List reviews | Yes | Yes | |
|
||||||
|
| Create review | Yes | Yes | |
|
||||||
|
| Submit pending review | Yes | Yes | |
|
||||||
|
| Delete pending review | Yes | Yes | |
|
||||||
|
| Dismiss review | Yes — `POST /pulls/{number}/reviews/{id}/dismissals` | **Yes** — `POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismiss` | |
|
||||||
|
| Un-dismiss review | No | **Yes** — `POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismiss` | Gitea allows undoing a dismissal |
|
||||||
|
| Review states | `APPROVED`, `REQUEST_CHANGES`, `COMMENT`, `PENDING`, `DISMISSED` | `APPROVED`, `REQUEST_CHANGES`, `COMMENT`, `PENDING`, (+ `REQUEST_REVIEW`) | States match; Gitea adds `REQUEST_REVIEW` as a state type |
|
||||||
|
| `DISMISSED` state | Yes — set by dismiss action | Yes — `Dismissed bool` + `Stale bool` fields | |
|
||||||
|
| Aggregated `reviewDecision` field | **Yes (GraphQL only)** — `APPROVED`, `CHANGES_REQUESTED`, `REVIEW_REQUIRED` | **No** | Gitea has no single aggregated review decision; must be computed from individual reviews |
|
||||||
|
| `reviewDecision` via REST | No (GitHub REST also lacks this) | No | Both REST APIs require client-side aggregation |
|
||||||
|
| Official/required review flag | No | **Yes** — `Official bool` field on review | Gitea marks reviews from required reviewers as official |
|
||||||
|
| `Stale` review flag | No | **Yes** — `Stale bool` field | Gitea marks reviews stale when new commits are pushed |
|
||||||
|
| Review request (users) | Yes — `POST /pulls/{number}/requested_reviewers` | **Yes** — `POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers` | |
|
||||||
|
| Review request (teams) | Yes — `team_reviewers` param | **Yes** — `TeamReviewers` param | |
|
||||||
|
| Remove review request | Yes | Yes | |
|
||||||
|
| List eligible reviewers | No (must list repo collaborators separately) | **Yes** — `GET /repos/{owner}/{repo}/pulls/{index}/requested_reviewers` returns all eligible users | Gitea returns who *can* be requested, not just who has been |
|
||||||
|
| Inline code comments on review | Yes — with line, side, path, start_line | **Yes** — `CreatePullReviewComment` struct with position/path | |
|
||||||
|
| Multi-line comment range | Yes — `start_line`/`start_side` + `line`/`side` | Partial — position-based rather than line-range | GitHub's multi-line range is more explicit |
|
||||||
|
| Reply to review comment | Yes — `POST /pulls/{number}/comments/{id}/replies` | Yes — reply via in_reply_to reference | |
|
||||||
|
| List review comments (inline) | Yes | Yes | |
|
||||||
|
| Edit/delete review comment | Yes | Yes | |
|
||||||
|
| Reactions on review comments | Yes | **No** — reactions are on issue comments only, not PR review comments | Gitea's reaction support does not extend to PR review comments |
|
||||||
|
| `commitId` on review | Yes | **Yes** — `CommitID` field | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue & PR Comments
|
||||||
|
|
||||||
|
| Feature | GitHub | Gitea | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| List issue comments | Yes | Yes | |
|
||||||
|
| Create issue comment | Yes | Yes | |
|
||||||
|
| Edit comment | Yes | Yes | |
|
||||||
|
| Delete comment | Yes | Yes | |
|
||||||
|
| Pin/unpin comment | **Yes** — `PUT/DELETE /issues/comments/{id}/pin` | No | GitHub supports pinned comments on issues |
|
||||||
|
| Reactions on issue comments | Yes — 8 types | **Yes** — same 8 types | |
|
||||||
|
| Reactions on PR review comments | Yes | **No** | Gap in Gitea: review comment reactions not supported |
|
||||||
|
| `body_html` / `body_text` fields | Yes | No — body is Markdown only | GitHub returns rendered HTML variants |
|
||||||
|
| `author_association` field | Yes | No | GitHub classifies commenter relationship to repo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: Key Gaps and Gitea Advantages
|
||||||
|
|
||||||
|
### Gitea lacks (compared to GitHub)
|
||||||
|
|
||||||
|
| Missing in Gitea | Impact on CLI tooling |
|
||||||
|
|---|---|
|
||||||
|
| No GraphQL API | Cannot get aggregated `reviewDecision`; must compute from review list |
|
||||||
|
| No `reviewDecision` enum (REST or GraphQL) | CLI must fold `APPROVED`/`REQUEST_CHANGES` reviews client-side |
|
||||||
|
| No device flow OAuth | Headless login requires PAT or BasicAuth; cannot do browser-less OAuth |
|
||||||
|
| No `state_reason` on issues | Cannot distinguish "completed" vs "not planned" close reasons |
|
||||||
|
| No issue type (bug/feature classification) | Labels must substitute for issue type |
|
||||||
|
| No fine-grained per-repo PATs | Tokens are category-scoped, not repo-scoped |
|
||||||
|
| No `X-RateLimit-*` headers | Cannot back off gracefully on rate limits (no limit signal) |
|
||||||
|
| No cursor-based pagination | Must use offset pagination; large result sets may drift |
|
||||||
|
| No `body_html` on comments | Must render Markdown client-side if HTML is needed |
|
||||||
|
| No `author_association` | Cannot determine contributor relationship without separate lookup |
|
||||||
|
| No reactions on PR review comments | Review comment reactions not possible |
|
||||||
|
| No lock/unlock issue endpoint | Issue locking is not exposed as an API operation |
|
||||||
|
| No GitHub Apps / installation tokens | Cannot use app-level auth scoping |
|
||||||
|
|
||||||
|
### Gitea advantages (compared to GitHub REST)
|
||||||
|
|
||||||
|
| Gitea-only capability | Impact on CLI tooling |
|
||||||
|
|---|---|
|
||||||
|
| `X-Total-Count` response header | Accurate totals on every list response; no extra count query needed |
|
||||||
|
| Token CRUD via API | Programmatic token management without web UI |
|
||||||
|
| OAuth app management via API | Automation-friendly app registration |
|
||||||
|
| Hard delete issues | Useful for cleanup automation |
|
||||||
|
| Issue dependencies (blocking/blocked-by) | Richer workflow modelling |
|
||||||
|
| Issue pinning with position reorder | Full pin management via API |
|
||||||
|
| Issue subscriptions endpoint | Subscribe/unsubscribe without using notifications API |
|
||||||
|
| Org-level labels | Labels shared across all org repos |
|
||||||
|
| `Official` + `Stale` flags on reviews | Richer review state without client-side inference |
|
||||||
|
| `Undismiss` review endpoint | Reversible review dismissals |
|
||||||
|
| Eligible reviewer list on PR | Know who *can* review before requesting |
|
||||||
|
| `ForceMerge` flag | Explicit force-merge without separate branch protection bypass |
|
||||||
|
| `manually-merged` merge style | Record out-of-band merges |
|
||||||
|
| `rebase-merge` + `fast-forward-only` styles | More granular merge strategy control |
|
||||||
|
| `ContentVersion` on issues and PRs | Optimistic locking for concurrent edits |
|
||||||
|
| Time tracking (`TimeEstimate`) | Built-in estimation without third-party integrations |
|
||||||
|
| `Sudo` param (admin) | Act-as for admin automation |
|
||||||
|
| HTTP Signature auth | SSH key based API auth |
|
||||||
|
| `PinOrder` on PRs | PRs can also be pinned, not just issues |
|
||||||
586
.claude/spec/gitea-axi.md
Normal file
586
.claude/spec/gitea-axi.md
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Coding agents that need to drive a Gitea-hosted workflow (issues, pull requests, labels) today have two poor options.
|
||||||
|
The official `tea` CLI is human-oriented: it has no token-efficiency, no contextual guidance, and no agent-facing error conventions.
|
||||||
|
Gitea's MCP servers expose the full API surface (dozens of tools) rather than being tuned for token or turn efficiency.
|
||||||
|
There is no Gitea-focused tool built to the same "agent ergonomics" standard that `gh-axi` established for GitHub.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Build `gitea-axi`: a thin TypeScript CLI that calls the Gitea REST API directly via `gitea-js`, reshaping its output according to all 10 AXI (Agent eXperience Interface) principles.
|
||||||
|
The canonical principle text at https://axi.md/ is the design authority.
|
||||||
|
The `gh-axi` reference implementation is a non-binding shape reference for concrete interface details (block names, flag names, field extractors), departed from freely — with each deliberate departure documented in place.
|
||||||
|
It gives coding agents an ergonomic, low-token way to drive issues and pull requests on any Gitea instance.
|
||||||
|
It ships as both an installable npm CLI and a bundled Agent Skill, so any agent session can adopt it with one install step.
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
1. As a coding agent, I want to create a Gitea issue with a title, body, and labels, so that I can record work items for later retrieval.
|
||||||
|
2. As a coding agent, I want to find issues by label and state, so that I can locate relevant work without already knowing its issue number.
|
||||||
|
3. As a coding agent, I want to read an issue's full body, labels, and comments, so that I can load its context into a session.
|
||||||
|
4. As a coding agent, I want to add and remove labels on an existing issue, so that I can reflect state transitions as work progresses.
|
||||||
|
5. As a coding agent, I want to create a pull request from the current branch, so that completed work becomes reviewable.
|
||||||
|
6. As a coding agent, I want to fetch a pull request's metadata and diff, so that review tooling can operate on it without re-deriving it from git.
|
||||||
|
7. As a coding agent, I want to post a comment on a pull request, so that findings or notes are visible as a permanent reference on the PR itself.
|
||||||
|
8. As a coding agent, I want command output in TOON format with minimal default fields and truncated large fields, so that repeated calls across a long-running session don't consume excessive context.
|
||||||
|
9. As a coding agent, I want pre-computed aggregates in list and read output, so that I don't need follow-up calls just to derive obvious derived fields.
|
||||||
|
10. As a coding agent, I want explicit empty-state output when a query returns nothing, so that "no results" is never ambiguous with an error or a hang.
|
||||||
|
11. As a coding agent, I want structured errors with actionable suggestions and meaningful exit codes instead of prose failures, so that I can self-correct without the operator's help.
|
||||||
|
12. As a coding agent, I want mutations to be idempotent and to never prompt interactively, so that unattended, scripted use never stalls or double-applies.
|
||||||
|
13. As a coding agent, I want contextual next-step suggestions appended after output, so that I know what to call next without being taught the tool from scratch every session.
|
||||||
|
14. As a coding agent, I want a consistent per-subcommand `--help`, so that I can discover the interface on demand rather than needing it pre-loaded in context.
|
||||||
|
15. As an operator, I want `gitea-axi` run with no arguments to show live, actionable repository state instead of a help screen, so that I get immediate value without memorizing flags.
|
||||||
|
16. As an operator, I want `gitea-axi` to reuse my existing `tea` login configuration, so that I don't manage a second set of credentials.
|
||||||
|
17. As an operator, I want `gitea-axi`'s command surface to stay generic, with no workflow-specific behavior baked in, so that it's useful across different projects without code changes.
|
||||||
|
18. As an operator, I want `gitea-axi` published to npm and as an installable Agent Skill, so that I (and others) can adopt it with a single install command plus an explicit one-time `gitea-axi setup`.
|
||||||
|
19. As an operator, I want an optional `gitea-axi setup hooks` command that injects the dashboard into my agent sessions at session start, so that my agent begins each session already aware of the repository's live state.
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Language and Runtime
|
||||||
|
|
||||||
|
TypeScript on Node 20+, matching the `gh-axi` reference implementation.
|
||||||
|
ESM module format.
|
||||||
|
|
||||||
|
### Implementation Strategy
|
||||||
|
|
||||||
|
Call the Gitea REST API directly via `gitea-js` (the official TypeScript client generated from Gitea's OpenAPI spec).
|
||||||
|
This is not a subprocess wrapper — it is a direct HTTP API client.
|
||||||
|
|
||||||
|
Tea was evaluated as a subprocess target and rejected during design (see ADR 0002).
|
||||||
|
The short version: tea's create commands have no `--output json` flag, its PR list has no head-branch filter, it exposes no review or total counts in JSON, and diff content requires a direct HTTP GET regardless — making a tea wrapper a patchwork of subprocess calls and text parsing rather than a clean pipeline.
|
||||||
|
|
||||||
|
`gitea-js` gives clean typed responses, `X-Total-Count` headers for pagination, review counts, and immediate JSON from create operations.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
gitea-axi reads credentials from tea's login store via `tea login list --output json`.
|
||||||
|
It requires `tea` to be installed and at least one login configured via `tea login add`.
|
||||||
|
At startup, it detects the current repository's Gitea hostname (see Repository Context Detection), finds the matching login entry, and extracts the token for all subsequent API calls.
|
||||||
|
Tea is used only for credential discovery — no commands are dispatched through the tea subprocess.
|
||||||
|
|
||||||
|
### Command Surface
|
||||||
|
|
||||||
|
#### Dashboard
|
||||||
|
|
||||||
|
`gitea-axi` (no args): a two-tier home view showing live repository state, preceded by the `bin:` + `description:` header from `axi-sdk-js`, followed by next-step suggestions (see ADR 0012).
|
||||||
|
|
||||||
|
The short tier (no flags — also 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`), fetched in parallel with `limit=3`.
|
||||||
|
The `review` field is computed client-side via the same parallel review fetch as `pr list` (see ADR 0006) — at most 3 extra HTTP calls.
|
||||||
|
The short tier's `help:` block always includes a hint pointing at `--full`.
|
||||||
|
|
||||||
|
The full tier (`gitea-axi --full`): open PRs as a TOON table and open issue counts grouped by label (a label→count record).
|
||||||
|
Full-tier PR table default fields: `number`, `title`, `author` (plucked from `user.login`), `labels` (joined label names), `review` (same client-side computation).
|
||||||
|
The full-tier PR table is capped at 20 rows with a standard count line (`count: 20 of T total`).
|
||||||
|
Full-tier issue aggregation fetches all pages of open issues up to a hard cap of 1000 (page size 50, 20 pages max); if the cap is hit, label counts are suffixed with `+`.
|
||||||
|
`--full` here selects the full tier — an intentional overload of the flag that elsewhere suppresses truncation.
|
||||||
|
|
||||||
|
Output blocks (both tiers): a `repo: owner/name` line, then `prs:` and `issues:`.
|
||||||
|
Empty states are explicit and match gh-axi's home: `prs: 0 open` / `issues: 0 open` (raw strings; list commands keep their `<noun>[0]: (none)` convention).
|
||||||
|
Issue fetching passes `type=issues` so PRs never pollute the issue block (see Issue/PR Type Guard).
|
||||||
|
Outside a recognizable Gitea repo the dashboard errors with `REPO_NOT_FOUND` (help: use `-R` + `--login`) — login selection requires a hostname.
|
||||||
|
This holds even when invoked by the SessionStart hook; error noise in non-Gitea sessions is an accepted consequence (see ADR 0009).
|
||||||
|
|
||||||
|
#### Issue Commands
|
||||||
|
|
||||||
|
**`issue list [flags]`**
|
||||||
|
`--state <open|closed|all>` (default open);
|
||||||
|
`--label <name>` (API-supported — Gitea issue list accepts comma-separated label names);
|
||||||
|
`--assignee <login>` (API-supported — maps to `assigned_by` param);
|
||||||
|
`--author <login>` (API-supported — maps to `created_by` param);
|
||||||
|
`--milestone <name>` (API-supported — maps to `milestones` param);
|
||||||
|
`--sort <created|updated|comments>` (client-side — Gitea issue list has no sort param; always descending, matching gh-axi);
|
||||||
|
`--limit <n>` (default 30);
|
||||||
|
`--fields <a,b,c>`.
|
||||||
|
`--search` is explicitly forbidden (VALIDATION_ERROR).
|
||||||
|
Always passes `type=issues` — Gitea's issue endpoints also serve PRs, which must never appear in issue lists (see Issue/PR Type Guard).
|
||||||
|
Client-side `--sort` reorders without changing membership, so the standard `count: N of T total` line is kept (see ADR 0005); full pagination still precedes sorting.
|
||||||
|
Default output fields (matching gh-axi): `number`, `title`, `state` (lowercased), `author` (plucked from `user.login`), `created` (relative time).
|
||||||
|
Extra fields via `--fields`: `body` (raw), `closedAt` (relative time, as `closed_at`), `labels` (joined names), `milestone` (title), `updatedAt` (relative time, as `updated_at`), `url`.
|
||||||
|
No `type` field in output — Gitea has no issue types.
|
||||||
|
|
||||||
|
**`issue view <n> [flags]`**
|
||||||
|
`--comments` (expand full comments — renders all comments with no count cap, each body truncated at 800 chars with cleanBody applied);
|
||||||
|
`--full` (suppress all truncation in the output — issue body and comment bodies alike).
|
||||||
|
Default output fields (matching gh-axi, minus `type`): `number`, `title`, `state`, `author`, `created`, `body` (truncated at 500), plus `comment_count`.
|
||||||
|
Type guard: if `<n>` is a pull request, fails with `VALIDATION_ERROR` ("issue #N is a pull request") and a `pr view <n>` help line (see Issue/PR Type Guard).
|
||||||
|
No `type` field in output (Gitea has no issue types).
|
||||||
|
No sub-issue augmentation (Gitea does not model issue hierarchies; use `issue blocks` and `issue blocked-by` for dependency relationships).
|
||||||
|
|
||||||
|
**`issue create [flags]`**
|
||||||
|
`--title <text>` (required);
|
||||||
|
`--body <text>` or `--body-file <path>`;
|
||||||
|
`--assignee <login>`;
|
||||||
|
`--label <name>` (repeatable; resolved to label ID via `GET /labels`, case-insensitive — `VALIDATION_ERROR` if not found);
|
||||||
|
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found).
|
||||||
|
`--project` is excluded (Gitea has no projects REST API).
|
||||||
|
`--type` is excluded (Gitea has no issue types).
|
||||||
|
Output schema: `issue: { number, title, state, url }` (where `url` = `html_url`).
|
||||||
|
Extra fields available via `--fields`: `labels`, `assignees`, `milestone`, `body`.
|
||||||
|
|
||||||
|
**`issue edit <n> [flags]`**
|
||||||
|
`--title`;
|
||||||
|
`--body <text>` or `--body-file <path>`;
|
||||||
|
`--add-label <name>`;
|
||||||
|
`--remove-label <name>`;
|
||||||
|
`--add-assignee <login>`;
|
||||||
|
`--remove-assignee <login>`;
|
||||||
|
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found).
|
||||||
|
Label mutations use Gitea's dedicated additive/removal label endpoints (idempotent).
|
||||||
|
`--add-label` passes the name directly via `POST /issues/{index}/labels` (Gitea accepts names here — no lookup needed).
|
||||||
|
`--remove-label` requires an ID: resolved via case-insensitive label lookup; `VALIDATION_ERROR` if the label name does not exist in the repo; if the label exists but is not applied to this issue, Gitea's 404 on `DELETE /labels/{id}` is treated as silent success.
|
||||||
|
Assignee mutations use fetch-then-patch: the current assignee list is read first, the addition or removal applied in-process, then the full resulting list is sent in a single PATCH (see ADR 0007).
|
||||||
|
|
||||||
|
**`issue close <n> [flags]`**
|
||||||
|
`--comment <text>`.
|
||||||
|
Closing sets `state: "closed"` via PATCH on the issue.
|
||||||
|
`--reason` is excluded (Gitea has no `state_reason` concept).
|
||||||
|
When `--comment` is provided, two API calls are made: PATCH to close, then POST to create the comment.
|
||||||
|
If the PATCH succeeds but the POST fails, the error is surfaced — the issue remains closed but the failure is reported rather than silently swallowed.
|
||||||
|
Idempotent: returns early with `message: "Already closed"` if already closed.
|
||||||
|
|
||||||
|
**`issue reopen <n>`**
|
||||||
|
Sets `state: "open"` via PATCH.
|
||||||
|
Idempotent: returns early with `message: "Already open"` if already open.
|
||||||
|
|
||||||
|
**`issue comment <n> [flags]`**
|
||||||
|
`--body <text>` or `--body-file <path>` (required).
|
||||||
|
Gitea's `POST /issues/{index}/comments` returns the created `Comment` object directly.
|
||||||
|
Output block: `comment: { number, author, created, body }` (body truncated at 800 chars).
|
||||||
|
`number` is the issue number the comment was posted to; the comment's own id is not output (nothing in the command surface consumes comment ids).
|
||||||
|
|
||||||
|
**`issue delete <n>`**
|
||||||
|
Hard-deletes the issue via `DELETE /issues/{index}` (requires admin or owner permissions).
|
||||||
|
Not idempotent: a nonexistent issue errors with `ISSUE_NOT_FOUND` rather than reporting success (see ADR 0010).
|
||||||
|
Output: `issue: { number, status: "deleted" }`.
|
||||||
|
|
||||||
|
**`issue pin <n>`**
|
||||||
|
`POST /issues/{index}/pin`.
|
||||||
|
Idempotent: returns early with `message: "Already pinned"` if already pinned.
|
||||||
|
Output: `issue: { number, state, pinned }`.
|
||||||
|
|
||||||
|
**`issue unpin <n>`**
|
||||||
|
`DELETE /issues/{index}/pin`.
|
||||||
|
Idempotent: returns early with `message: "Already unpinned"` if already unpinned.
|
||||||
|
Output: `issue: { number, state, pinned }`.
|
||||||
|
|
||||||
|
**`issue blocks <list|add|remove>` (Gitea-specific)**
|
||||||
|
Manages the set of issues that this issue blocks (downstream dependents that cannot proceed until this issue is resolved).
|
||||||
|
`issue blocks list <n>` — lists issues blocked by `<n>`; output block `blocked_issues`.
|
||||||
|
`issue blocks add <n> <target>` — makes `<n>` block `<target>`; output `blocks: { issue: n, blocks: target }`.
|
||||||
|
`issue blocks remove <n> <target>` — removes the blocking relationship.
|
||||||
|
Idempotent: `add` of an existing relationship returns `already: true` (fetch-first check against the current list); `remove` of a nonexistent relationship is silent success; self-reference and cycle errors still surface as `VALIDATION_ERROR` via the 422 mapping.
|
||||||
|
Gitea API: `GET/POST/DELETE /repos/{owner}/{repo}/issues/{index}/blocks`.
|
||||||
|
No gh-axi equivalent.
|
||||||
|
|
||||||
|
**`issue blocked-by <list|add|remove>` (Gitea-specific)**
|
||||||
|
Manages the set of issues that block this issue (upstream blockers that must be resolved before this issue can proceed).
|
||||||
|
`issue blocked-by list <n>` — lists issues that block `<n>`; output block `blocking_issues`.
|
||||||
|
`issue blocked-by add <n> <blocker>` — makes `<n>` depend on `<blocker>`; output `blocked_by: { issue: n, blocked_by: blocker }`.
|
||||||
|
`issue blocked-by remove <n> <blocker>` — removes the dependency.
|
||||||
|
Same idempotency rules as `issue blocks`.
|
||||||
|
Gitea API: `GET/POST/DELETE /repos/{owner}/{repo}/issues/{index}/dependencies`.
|
||||||
|
No gh-axi equivalent.
|
||||||
|
|
||||||
|
#### Excluded Issue Commands
|
||||||
|
|
||||||
|
`issue lock` / `issue unlock` — excluded: Gitea exposes `is_locked` as a readable field but has no lock/unlock API endpoint.
|
||||||
|
`issue transfer` — excluded: no Gitea equivalent.
|
||||||
|
`issue subissue` — excluded: GitHub-specific hierarchy model; Gitea uses blocking/dependency relationships instead (see `issue blocks` and `issue blocked-by`).
|
||||||
|
|
||||||
|
#### PR Commands
|
||||||
|
|
||||||
|
**`pr list [flags]`**
|
||||||
|
`--state <open|closed|all>` (default open);
|
||||||
|
`--label <name>` (requires name→ID lookup — Gitea PR list takes `labels: number[]`; see label name lookup in CONTEXT.md);
|
||||||
|
`--label-id <id>` (Gitea-specific shortcut — bypasses the name→ID lookup and passes the ID directly);
|
||||||
|
`--assignee <login>` (client-side filter — Gitea PR list has no assignee param);
|
||||||
|
`--author <login>` (API-supported — maps to `poster` param);
|
||||||
|
`--base <branch>` (client-side filter — no API param);
|
||||||
|
`--head <branch>` (client-side filter — no API param);
|
||||||
|
`--draft` (client-side filter — no API param);
|
||||||
|
`--sort <oldest|recentupdate|leastupdate|mostcomment|leastcomment|priority>` (Gitea-specific extension — maps directly to the API `sort` param);
|
||||||
|
`--limit <n>` (default 30);
|
||||||
|
`--fields <a,b,c>`.
|
||||||
|
`--search` is explicitly forbidden (VALIDATION_ERROR).
|
||||||
|
Default output fields (matching gh-axi): `number`, `title`, `state` (lowercased), `author` (plucked from `user.login`), `draft` (bool→yes/no), `review` (`reviewDecision` mapped: APPROVED→approved, CHANGES_REQUESTED→changes_requested, REVIEW_REQUIRED→required).
|
||||||
|
Extra fields via `--fields`: `body` (raw), `createdAt` (relative time, as `created`), `labels` (joined names), `milestone` (title), `mergedAt` (relative time, as `merged_at`), `url`.
|
||||||
|
`reviewDecision` is computed client-side by fetching reviews for each PR in parallel (one extra HTTP call per PR; see ADR 0006).
|
||||||
|
When any client-side filter is active, the count line shows `count: N of T total` with `T` computed from the in-memory filtered result set (see ADR 0005).
|
||||||
|
|
||||||
|
**`pr view <n> [flags]`**
|
||||||
|
`--comments` (renders all comments with no count cap, each body truncated at 800 chars with cleanBody applied);
|
||||||
|
`--reviews`;
|
||||||
|
`--full` (suppress all truncation in the output — PR body and comment bodies alike).
|
||||||
|
Default output fields (matching gh-axi): `number`, `title`, `state`, `author`, `draft`, `merged`, `checks`, `body` (truncated at 500), plus `comment_count` and `review_count`.
|
||||||
|
The `checks` field is populated from Gitea commit statuses via `GET /commits/{sha}/status` using the PR head SHA.
|
||||||
|
It renders as `"N passed, N failed[, N skipped][, N pending], N total"`, or `"0 passed, 0 failed — this PR has no CI checks configured"` when no statuses exist.
|
||||||
|
Commit status states map to gh-axi's four-value classification: `success`→`pass`; `failure`/`error`/`warning`→`fail` (matching Gitea's own combine logic, which treats `warning` as failure); `skipped`→`skip`; `pending`→`pending`.
|
||||||
|
Older Gitea instances never emit `skipped`, so the `skip` bucket is simply absent there.
|
||||||
|
`pr view` always makes three API calls: the PR fetch and `GET /pulls/{index}/reviews` are issued in parallel, then the combined-status fetch runs once the head SHA is known — so `review_count` and `checks` are always in the default output without requiring `--reviews`.
|
||||||
|
When `--reviews` is passed, additionally fetches per-review inline comments (`GET /pulls/{index}/reviews/{id}/comments` for each review).
|
||||||
|
Gitea-specific fields exposed on review objects when `--reviews` is passed: `official` (whether the review counts toward required approvals) and `stale` (whether the PR head has moved since review submission).
|
||||||
|
|
||||||
|
**`pr create [flags]`**
|
||||||
|
`--title <text>` (required);
|
||||||
|
`--body <text>` or `--body-file <path>`;
|
||||||
|
`--base <branch>`;
|
||||||
|
`--head <branch>`;
|
||||||
|
`--assignee <login>`;
|
||||||
|
`--reviewer <login>`;
|
||||||
|
`--label <name>` (repeatable; resolved to label ID via `GET /labels`, case-insensitive — `VALIDATION_ERROR` if not found);
|
||||||
|
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found).
|
||||||
|
`--draft` is excluded (Gitea cannot create draft PRs via API).
|
||||||
|
`--project` is excluded (Gitea has no projects REST API).
|
||||||
|
When `--head` is not specified, defaults to the current local branch (via `git rev-parse --abbrev-ref HEAD`).
|
||||||
|
When `--base` is not specified, the repository's default branch is used (fetched via `GET /repos/{owner}/{repo}`).
|
||||||
|
Idempotent: before creating, checks `GET /pulls/{base}/{head}` for an existing open PR for the same branch pair.
|
||||||
|
If found, returns `pull_request: { number, url, already: true }` without creating a duplicate.
|
||||||
|
Output on success: `created: { number, url }` — completing gh-axi's action-block/entity-block pattern (action-named block when the mutation ran, entity-named block when it was a no-op).
|
||||||
|
|
||||||
|
**`pr edit <n> [flags]`**
|
||||||
|
`--title`;
|
||||||
|
`--body <text>` or `--body-file <path>`;
|
||||||
|
`--add-label <name>`;
|
||||||
|
`--remove-label <name>`;
|
||||||
|
`--add-assignee <login>`;
|
||||||
|
`--remove-assignee <login>`;
|
||||||
|
`--add-reviewer <login>`;
|
||||||
|
`--remove-reviewer <login>`;
|
||||||
|
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found);
|
||||||
|
`--base <branch>`.
|
||||||
|
Assignee and reviewer mutations use fetch-then-patch (see ADR 0007).
|
||||||
|
Output: `edited: { number, status: "ok" }`.
|
||||||
|
|
||||||
|
**`pr close <n> [flags]`**
|
||||||
|
`--comment <text>`.
|
||||||
|
Idempotent: returns `pull_request: { number, state, already: true }` if already closed or merged.
|
||||||
|
Output on success: `closed: { number, status: "ok" }`.
|
||||||
|
|
||||||
|
**`pr merge <n> [flags]`**
|
||||||
|
`--method <merge|squash|rebase|rebase-merge|fast-forward-only|manually-merged>`;
|
||||||
|
`--merge`, `--squash`, `--rebase` (shorthands for the three common methods);
|
||||||
|
`--auto`;
|
||||||
|
`--delete-branch`;
|
||||||
|
`--body <text>` or `--body-file <path>`;
|
||||||
|
`--subject <text>`;
|
||||||
|
`--merge-commit-id <sha>` (required when `--method manually-merged`; VALIDATION_ERROR if omitted; VALIDATION_ERROR if provided with any other method).
|
||||||
|
Gitea-specific methods not in gh-axi: `rebase-merge` (rebase + explicit merge commit), `fast-forward-only`, `manually-merged` (records an out-of-band merge without actually merging).
|
||||||
|
Idempotent: if already merged, returns `pull_request: { number, state: "merged", merged_by, merged_at }` without calling the API.
|
||||||
|
Output on success: `merged: { number, status: "ok", method }`.
|
||||||
|
|
||||||
|
**`pr review <n> [flags]`**
|
||||||
|
`--approve`;
|
||||||
|
`--request-changes`;
|
||||||
|
`--comment`;
|
||||||
|
`--body <text>` or `--body-file <path>`.
|
||||||
|
Exactly one of the three action flags is required; zero or multiple → `VALIDATION_ERROR` before any API call (mirroring the `pr merge` shorthand-conflict rule).
|
||||||
|
Body requirements are not pre-validated locally: if Gitea rejects a body-less review event, its 422 surfaces as `VALIDATION_ERROR` with the server's message.
|
||||||
|
Output: `review: { number, action }`.
|
||||||
|
|
||||||
|
**`pr checks <n>`**
|
||||||
|
Fetches combined commit status for the PR head SHA via `GET /commits/{sha}/status`.
|
||||||
|
Output matches gh-axi: a `summary` line (`N passed, N failed[, N skipped][, N pending], N total`) followed by a `checks` list of `{ name, conclusion }`.
|
||||||
|
Conclusions: `pass`, `fail`, `skip`, or `pending`, using the same state mapping as `pr view` (`skipped`→`skip`; `warning`→`fail`).
|
||||||
|
When no statuses are configured: `checks: "0 passed, 0 failed — this PR has no CI checks configured"`.
|
||||||
|
|
||||||
|
**`pr diff <n> [flags]`**
|
||||||
|
`--full`.
|
||||||
|
Fetches raw diff from `GET /pulls/{index}.diff`.
|
||||||
|
Truncation limit: 4000 chars.
|
||||||
|
Output: `pr_diff: { number, diff[, truncated, original_length] }`.
|
||||||
|
|
||||||
|
**`pr checkout <n>`**
|
||||||
|
Fetches the PR head branch name from `GET /pulls/{index}` (`head.ref` field), then runs in the current working directory:
|
||||||
|
1. `git fetch origin pull/<n>/head:<branch>`
|
||||||
|
2. `git checkout <branch>`
|
||||||
|
Fetching `refs/pull/{index}/head` from the base repo works uniformly for same-repo and fork PRs — the head branch itself may live in a fork that is not a configured remote (see ADR 0011).
|
||||||
|
Git subprocess failures (dirty worktree, network) map to `GIT_ERROR`, carrying git's first stderr line and a remediation help line.
|
||||||
|
Output: `checkout: { number, branch, status: "ok" }`.
|
||||||
|
|
||||||
|
**`pr reopen <n>`**
|
||||||
|
Idempotent: returns `pull_request: { number, state: "open", already: true }` if already open.
|
||||||
|
Output on success: `reopened: { number, status: "ok" }`.
|
||||||
|
|
||||||
|
**`pr comment <n> [flags]`**
|
||||||
|
`--body <text>` or `--body-file <path>` (required).
|
||||||
|
PRs share the issue comment endpoint in Gitea (`POST /issues/{index}/comments`), which returns the created `Comment` object directly.
|
||||||
|
Output block: `comment: { number, author, created, body }` (body truncated at 800 chars).
|
||||||
|
`number` is the PR number the comment was posted to; the comment's own id is not output.
|
||||||
|
This diverges from gh-axi's `commented: { number, status: "ok" }` — returning the created comment eliminates the need for a follow-up view call (AXI Principle 4; see ADR 0008).
|
||||||
|
|
||||||
|
**`pr update-branch <n> [flags]`**
|
||||||
|
`--style <merge|rebase>` (Gitea-specific; default `merge`).
|
||||||
|
Merges the base branch into the PR head branch via `POST /pulls/{index}/update?style=<style>`.
|
||||||
|
Output: `updated: { number, status: "ok" }`.
|
||||||
|
|
||||||
|
#### Excluded PR Commands
|
||||||
|
|
||||||
|
`pr ready` — excluded: Gitea has no API to convert a draft PR to ready for review.
|
||||||
|
`pr revert` — excluded: Gitea has no revert PR endpoint.
|
||||||
|
|
||||||
|
#### Label Commands
|
||||||
|
|
||||||
|
**`label list [flags]`**
|
||||||
|
`--limit <n>` (default 500).
|
||||||
|
Output: count line followed by `labels: [ { name } ]`.
|
||||||
|
|
||||||
|
**`label create [flags]`**
|
||||||
|
`--name <text>` (required);
|
||||||
|
`--color <hex>` (required, without `#`).
|
||||||
|
The `#` prefix is automatically prepended before calling the Gitea API (which requires it in `CreateLabelOption.color`).
|
||||||
|
`--description <text>`.
|
||||||
|
Idempotent: checks for an existing label with the same name (case-insensitive) before creating.
|
||||||
|
If found: `create: already_exists`, `label: <existing-name>`.
|
||||||
|
Output on success: `created: ok`, `label: <name>`.
|
||||||
|
|
||||||
|
**`label edit <name> [flags]`**
|
||||||
|
`--name <new-name>`;
|
||||||
|
`--color <hex>`;
|
||||||
|
`--description <text>`.
|
||||||
|
`<name>` is resolved via the standard case-insensitive label lookup; `VALIDATION_ERROR` if not found.
|
||||||
|
Output: `edit: ok`, `label: <new-name-or-original-name>`.
|
||||||
|
|
||||||
|
**`label delete <name>`**
|
||||||
|
`<name>` is resolved via the standard case-insensitive label lookup.
|
||||||
|
Not idempotent: a nonexistent label errors with `VALIDATION_ERROR` rather than reporting success (see ADR 0010).
|
||||||
|
Output: `delete: ok`, `label: <name>`.
|
||||||
|
|
||||||
|
#### Setup Command
|
||||||
|
|
||||||
|
**`setup`**
|
||||||
|
Installs the bundled Agent Skill markdown into `~/.claude/skills/` (see ADR 0009).
|
||||||
|
This is gitea-axi's primary fulfillment of AXI Principle 7 (Ambient context): an explicit setup command, matching gh-axi's `setup`.
|
||||||
|
Idempotent: re-running reports already-installed/updated rather than failing.
|
||||||
|
Output: `setup: { skill, path, status: <installed|updated|unchanged> }`.
|
||||||
|
|
||||||
|
**`setup hooks`**
|
||||||
|
Opt-in: installs a SessionStart hook via axi-sdk-js's `installSessionStartHooks()` into Claude Code (`~/.claude/settings.json`), Codex (`~/.codex/hooks.json` plus `config.toml`), and OpenCode (ambient plugin) — see ADR 0009.
|
||||||
|
The hook runs the bare `gitea-axi` binary (the short dashboard tier) in the session's working directory at session start and injects its output as ambient context.
|
||||||
|
Idempotent: managed entries are updated in place by the SDK.
|
||||||
|
Output mirrors gh-axi: `hooks: { status: installed, integrations: Claude Code, Codex, OpenCode }`, with a help line to restart the agent session.
|
||||||
|
|
||||||
|
#### Shadowed Built-in Commands
|
||||||
|
|
||||||
|
`update` — axi-sdk-js ships a built-in self-update command (checks npmjs.org and updates the install) with its own `UPDATE_ERROR` code; gitea-axi shadows it (see ADR 0013).
|
||||||
|
`gitea-axi update` fails with `VALIDATION_ERROR` and a help line: `` Run `npm install -g gitea-axi@latest` to update ``.
|
||||||
|
This keeps the command surface and the ten-code error list exactly as specified here.
|
||||||
|
|
||||||
|
### Name-to-ID Resolution
|
||||||
|
|
||||||
|
Some Gitea API endpoints require integer IDs where gitea-axi accepts human-readable names.
|
||||||
|
|
||||||
|
**Milestone names** (`--milestone <name>` on `issue create`, `issue edit`, `pr create`, `pr edit`):
|
||||||
|
Resolved via `GET /repos/{owner}/{repo}/milestones?name=<name>`.
|
||||||
|
`VALIDATION_ERROR` if no milestone with that name exists.
|
||||||
|
|
||||||
|
**Label names for `pr list --label`**:
|
||||||
|
Resolved via `GET /repos/{owner}/{repo}/labels`, matched case-insensitively.
|
||||||
|
`VALIDATION_ERROR` if not found.
|
||||||
|
`--label-id <id>` bypasses this lookup.
|
||||||
|
|
||||||
|
**Label names for `issue create` and `pr create` (`--label <name>`)**:
|
||||||
|
Same case-insensitive label lookup.
|
||||||
|
`VALIDATION_ERROR` if not found.
|
||||||
|
|
||||||
|
**Label names for `issue edit` / `pr edit` `--add-label`**:
|
||||||
|
Not resolved — the label name is passed directly in the POST body; Gitea's label endpoint accepts names.
|
||||||
|
|
||||||
|
**Label names for `issue edit` / `pr edit` `--remove-label`**:
|
||||||
|
Resolved via case-insensitive label lookup.
|
||||||
|
`VALIDATION_ERROR` if the label name does not exist in the repo.
|
||||||
|
If the label exists but is not applied to the issue/PR, Gitea's 404 on `DELETE /labels/{id}` is treated as silent success.
|
||||||
|
|
||||||
|
**Label names for `label edit <name>` / `label delete <name>`**:
|
||||||
|
Resolved via the same case-insensitive label lookup.
|
||||||
|
`VALIDATION_ERROR` if not found.
|
||||||
|
|
||||||
|
### Client-Side Filtering Policy
|
||||||
|
|
||||||
|
When a filter flag has no corresponding Gitea API query parameter, gitea-axi paginates all results (`limit=50` per page until exhausted) and filters in-process (see ADR 0005).
|
||||||
|
Known client-side filters for the current surface: `pr list --assignee`, `pr list --base`, `pr list --head`, `pr list --draft`.
|
||||||
|
When any client-side filter is active, the count line shows `count: N of T total`, where `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 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; full pagination still precedes sorting.
|
||||||
|
|
||||||
|
### Issue/PR Type Guard
|
||||||
|
|
||||||
|
Gitea's issue endpoints also serve pull requests (`Issue.pull_request` is non-null for PRs).
|
||||||
|
Every issues-list call passes `type=issues` — `issue list`, the dashboard's issue aggregation, and any client-side-filter pagination.
|
||||||
|
Issue commands invoked with a PR number fail with `VALIDATION_ERROR` ("issue #N is a pull request") and a `pr view <n>` help line, detected via the fetched object's `pull_request` field.
|
||||||
|
Exception: `issue comment` stays permissive — PRs genuinely share the comment endpoint.
|
||||||
|
|
||||||
|
### reviewDecision Computation
|
||||||
|
|
||||||
|
Gitea has no aggregated `reviewDecision` field on the PR object.
|
||||||
|
gitea-axi computes it client-side from the reviews list (see ADR 0006).
|
||||||
|
Logic: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false` and no non-dismissed `REQUEST_CHANGES` exists; `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES` exists; `REVIEW_REQUIRED` otherwise.
|
||||||
|
On `pr list`, reviews for each PR are fetched in parallel (one extra HTTP call per PR in the list).
|
||||||
|
|
||||||
|
### Context Override Flags
|
||||||
|
|
||||||
|
gitea-axi accepts two top-level context override flags and two matching environment variables, mirroring gh-axi's design (`GH_REPO`):
|
||||||
|
|
||||||
|
- `-R` / `--repo <OWNER/NAME>` — overrides the repository detected from the git remote; env equivalent `GITEA_AXI_REPO`.
|
||||||
|
- `--login <name>` — selects a specific tea login profile, overriding the one matched from the git remote's hostname; env equivalent `GITEA_AXI_LOGIN`.
|
||||||
|
|
||||||
|
Resolution priority: flag > environment variable > auto-detection (git remote / hostname match).
|
||||||
|
Both flags are accepted anywhere on the command line, before or after the command — more permissive than gh-axi, which rejects them before the command; next-step suggestions always render them after the command.
|
||||||
|
These overrides are injected into next-step suggestions only when the context came from a flag or environment variable, not when it was auto-detected from the git remote (because the agent's next call will be in the same working directory and will auto-detect the same context).
|
||||||
|
|
||||||
|
### Output — The 10 AXI Principles
|
||||||
|
|
||||||
|
**Principle 1 — TOON output.**
|
||||||
|
All structured output uses the `@toon-format/toon` `encode()` function, wrapped in `renderList()` and `renderDetail()` helpers that handle the list/detail shape distinction.
|
||||||
|
|
||||||
|
**Principle 2 — Minimal default schemas.**
|
||||||
|
Each command exposes a small default field set (5–6 fields per list row), enumerated per command in the Command Surface section.
|
||||||
|
This is a deliberate, documented departure from the canonical 3–4-field guidance: each extra field (`state`, `created`, `draft`, `review`) answers a routine triage question that would otherwise cost a follow-up call.
|
||||||
|
Additional fields are opt-in via `--fields`.
|
||||||
|
Field extraction uses a `FieldDef` type system with typed extractors: nested pluck, array join, enum map, bool-to-text, and relative time formatting — matching gh-axi's internal architecture.
|
||||||
|
|
||||||
|
**Principle 3 — Content truncation.**
|
||||||
|
Body text is truncated at **500 characters** in all contexts (list and detail alike), matching gh-axi.
|
||||||
|
Comment bodies truncate at 800 characters wherever they appear (comment-post output and `--comments` view blocks), with cleanBody applied.
|
||||||
|
Diff content is truncated at 4000 characters.
|
||||||
|
When body truncation occurs, a hint is appended inline: `"... (truncated, N chars total - use --full to see complete body)"`.
|
||||||
|
When diff truncation occurs, `truncated: true` and `original_length: N` are added as separate fields, and a next-step suggestion to use `--full` is prepended.
|
||||||
|
`--full` on `issue view` and `pr view` suppresses all truncation in the command's output (entity body and comment bodies alike); `--full` on `pr diff` suppresses diff truncation.
|
||||||
|
Before truncation, a `cleanBody` step is applied **only when the raw body exceeds the truncation limit**.
|
||||||
|
`cleanBody` normalizes Gitea issue/PR URLs using the detected hostname (`https://<host>/<owner>/<repo>/issues/N` → `Issue#N`; `.../pulls/N` → `PR#N`), strips markdown image embeds, removes long URLs in markdown links and standalone text, and collapses email-style quoted blocks — matching gh-axi's transforms plus Gitea-specific URL normalization.
|
||||||
|
If cleaning brings the body within the limit, the cleaned body is returned with an appended note; if it still exceeds the limit, the cleaned body is truncated.
|
||||||
|
|
||||||
|
**Principle 4 — Pre-computed aggregates.**
|
||||||
|
List output leads with a `formatCountLine()`: `"count: N of T total"`, with `T` from the `X-Total-Count` response header, or computed from the in-memory filtered set when a client-side filter is active; `"count: N (showing first N)"` when at the request limit and no total is available.
|
||||||
|
The bare `count: N` form does not exist — the total is always reported (canonical Principle 4).
|
||||||
|
Detail output for issues includes `comment_count`; for PRs includes `review_count` and `comment_count`.
|
||||||
|
The review decision (`review` field) is a default on `pr list` and the dashboard PR table, computed client-side from parallel review fetches.
|
||||||
|
The full-tier dashboard's issue-by-label counts are computed by fetching all pages of open issues and aggregating in-process.
|
||||||
|
Both `issue comment` and `pr comment` return the created comment object directly from the POST response, eliminating the need for a follow-up view call.
|
||||||
|
|
||||||
|
**Principle 5 — Definitive empty states.**
|
||||||
|
When a list command returns no results it emits `<noun>[0]: (none)` followed by a relevant next-step suggestion.
|
||||||
|
The dashboard's empty states are `prs: 0 open` / `issues: 0 open` (raw strings, matching gh-axi's home view).
|
||||||
|
Empty output is never silent.
|
||||||
|
|
||||||
|
**Principle 6 — Structured errors, exit codes, idempotent mutations, no prompts.**
|
||||||
|
Errors are represented as a typed `AxiError` with one of ten named codes: `REPO_NOT_FOUND`, `ISSUE_NOT_FOUND`, `PR_NOT_FOUND`, `AUTH_REQUIRED`, `FORBIDDEN`, `RATE_LIMITED`, `TEA_NOT_INSTALLED`, `VALIDATION_ERROR`, `GIT_ERROR`, `UNKNOWN`.
|
||||||
|
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.
|
||||||
|
API error responses are classified by HTTP status code and calling context:
|
||||||
|
|
||||||
|
| HTTP status | Context | Error code |
|
||||||
|
|---|---|---|
|
||||||
|
| 401 | any | `AUTH_REQUIRED` |
|
||||||
|
| 403 | any | `FORBIDDEN` |
|
||||||
|
| 404 | called on `/repos/{owner}/{repo}` itself | `REPO_NOT_FOUND` |
|
||||||
|
| 404 | called on `/repos/.../issues/{index}` | `ISSUE_NOT_FOUND` |
|
||||||
|
| 404 | called on `/repos/.../pulls/{index}` | `PR_NOT_FOUND` |
|
||||||
|
| 404 | other paths | `UNKNOWN` |
|
||||||
|
| 422 | any | `VALIDATION_ERROR` (body message surfaced) |
|
||||||
|
| 429 | any | `RATE_LIMITED` (help: wait and retry, or reduce `--limit`) |
|
||||||
|
| other | any | `UNKNOWN` |
|
||||||
|
|
||||||
|
`TEA_NOT_INSTALLED` is emitted if the tea binary is not found during credential discovery.
|
||||||
|
`AUTH_REQUIRED` is also emitted when tea is installed but no login matches the detected hostname (help: `` Run `tea login add --url <host>` ``, or pass `--login <name>`).
|
||||||
|
A `--login` value naming a nonexistent profile is `VALIDATION_ERROR`, listing the available profile names.
|
||||||
|
`GIT_ERROR` classifies non-zero git subprocess exits (currently only `pr checkout`), carrying git's first stderr line.
|
||||||
|
Error output is TOON-encoded to stdout (not stderr): `error: <message>`, `code: <CODE>`, and optionally `help[N]:` with suggestion lines.
|
||||||
|
The suggestions field is named `help`, not `hint`.
|
||||||
|
Exit codes: 0 success, 1 error, 2 for `VALIDATION_ERROR` — covering unknown flags, missing required inputs, and server-side 422 rejections alike (the `axi-sdk-js` `exitCodeForError` mapping; see ADR 0004).
|
||||||
|
This deliberately broadens the canonical "exit 2 for unknown flags" wording: exit 2 uniformly means "the input was invalid — fix the call and retry".
|
||||||
|
|
||||||
|
Mutations are idempotent, and no command ever prompts:
|
||||||
|
`pr create` checks for an existing open PR before creating; if one exists, returns its details with `already: true` rather than creating a duplicate.
|
||||||
|
`issue edit --add-label` / `--remove-label` uses Gitea's dedicated additive label endpoints, which are idempotent.
|
||||||
|
`issue close`, `issue reopen`, `pr close`, `pr reopen`, `pr merge`, `issue pin`, `issue unpin`, `issue blocks add/remove`, `issue blocked-by add/remove`, and `setup` all check current state before mutating and return early if already in the target state.
|
||||||
|
Hard deletes (`issue delete`, `label delete`) deliberately refuse missing targets instead of reporting idempotent success (see ADR 0010).
|
||||||
|
All required inputs are flags — missing ones cause an immediate `error:` exit.
|
||||||
|
|
||||||
|
**Principle 7 — Ambient context.**
|
||||||
|
Fulfilled primarily by the `setup` command, which installs the bundled Agent Skill into `~/.claude/skills/` (see ADR 0009).
|
||||||
|
The skill surfaces gitea-axi to the agent at session start whenever it is relevant, without a per-session hook cost.
|
||||||
|
The canonical principle's primary mechanism — SessionStart hooks that inject the dashboard as initial context — is offered as the opt-in `setup hooks`, not the default.
|
||||||
|
There is no postinstall script — skill and hook installation are always explicit user actions, matching the canonical principle wording ("from an explicit setup command") and gh-axi's own `setup` command.
|
||||||
|
|
||||||
|
**Principle 8 — Content first.**
|
||||||
|
Running `gitea-axi` with no arguments shows live repository state, not a help screen, preceded by the SDK's `bin:` + `description:` header (executable path and one-sentence description, per the canonical principle).
|
||||||
|
The short tier makes two parallel API calls — 3 open issues, 3 open PRs — plus up to 3 parallel review fetches.
|
||||||
|
The full tier (`--full`) additionally aggregates open issue counts by label: open issues are paginated with `limit=50` and `type=issues`, up to a hard cap of 1000 issues (20 pages max); if the cap is hit, label counts are suffixed with `+`.
|
||||||
|
Each issue contributes to all of its labels; unlabeled issues appear as a separate `unlabeled` row only when non-zero.
|
||||||
|
|
||||||
|
**Principle 9 — Contextual next-step suggestions.**
|
||||||
|
Every command appends semi-dynamic suggestions to its output, rendered as a `help[N]:` block — the same block name used for error suggestions, matching gh-axi and the canonical principle text.
|
||||||
|
Runtime values are hybrid: list output keeps placeholders (`` `gitea-axi issue view <number>` ``) since the agent must choose which result it cares about; single-entity output fills the actual id (`` `gitea-axi issue view 42` ``) since it is unambiguous — matching the canonical "leave runtime values parameterized" guidance while carrying forward known ids.
|
||||||
|
Every command emits at least one suggestion; there are no empty `help:` blocks (a departure from gh-axi, which omits suggestions on `pr view` and emits empty blocks on `pr checkout`).
|
||||||
|
Every suggestion auto-includes `-R`/`--repo` and `--login` flags when the context was not auto-detected from the git remote — matching gh-axi's suggestion normalization approach.
|
||||||
|
|
||||||
|
**Principle 10 — Consistent `--help`.**
|
||||||
|
Every subcommand responds to `--help` with a concise flag reference.
|
||||||
|
Unknown flags exit with code 2.
|
||||||
|
No subcommand ever prompts interactively.
|
||||||
|
|
||||||
|
### Repository Context Detection
|
||||||
|
|
||||||
|
Repo owner, name, and hostname are detected from the git `origin` remote URL of the current directory.
|
||||||
|
Both SSH (`git@host:owner/repo.git`) and HTTPS (`https://host/owner/repo.git`) remote formats are supported.
|
||||||
|
If no recognizable Gitea remote URL is found on `origin`, gitea-axi exits with `REPO_NOT_FOUND` and a hint to configure the remote.
|
||||||
|
The detected hostname is used to select the matching tea login profile for auth.
|
||||||
|
|
||||||
|
### Distribution
|
||||||
|
|
||||||
|
Published to npm as `gitea-axi` (unscoped).
|
||||||
|
Binary name: `gitea-axi`.
|
||||||
|
The Agent Skill markdown file is bundled inside the npm package.
|
||||||
|
There is no postinstall script: `npm install -g gitea-axi` delivers the CLI binary, and a one-time explicit `gitea-axi setup` installs the skill into `~/.claude/skills/` (see ADR 0009).
|
||||||
|
The dashboard suggestion table hints at `setup` so the skill install is discoverable.
|
||||||
|
|
||||||
|
## Testing Decisions
|
||||||
|
|
||||||
|
Good tests exercise the actual command-line interface (argv in, stdout/exit-code/stderr out) — the one seam every caller depends on.
|
||||||
|
They do not test internal functions in isolation, and they do not mock individual API calls in a way that only proves gitea-axi issued the right HTTP request.
|
||||||
|
Instead, tests verify that gitea-axi correctly reshapes real API responses into correct TOON, correct error lines, and correct exit codes.
|
||||||
|
|
||||||
|
**Test seam:** Three environment variables together activate test mode:
|
||||||
|
- `GITEA_AXI_API_URL` — overrides the API base URL to point at the fixture server; also signals test mode, suppressing both the git remote subprocess and the tea credential subprocess.
|
||||||
|
- `GITEA_AXI_TOKEN` — supplies the auth token directly, bypassing `tea login list`.
|
||||||
|
- `GITEA_AXI_REPO` — supplies the repository context as `OWNER/NAME`, equivalent to `-R`; required in test mode since git remote detection is suppressed.
|
||||||
|
|
||||||
|
`GITEA_AXI_REPO` and `GITEA_AXI_LOGIN` are general context overrides (see Context Override Flags), not test-mode-specific; test mode merely relies on them.
|
||||||
|
|
||||||
|
In tests, `GITEA_AXI_API_URL` points to a local HTTP fixture server that maps incoming request paths and methods to pre-recorded Gitea API JSON response files stored in `fixtures/`.
|
||||||
|
This means tests exercise the full reshaping pipeline — JSON parse, field extraction, TOON encoding, truncation, suggestion generation, error classification — without a live Gitea instance.
|
||||||
|
|
||||||
|
**Two-tier test strategy:**
|
||||||
|
- Local / unit tier: the fixture server runs fast with no external dependencies.
|
||||||
|
Used for all command-level assertions.
|
||||||
|
- CI integration tier: a live disposable Gitea instance serves real API responses end-to-end, verifying that fixture recordings remain accurate and that the full HTTP pipeline works correctly.
|
||||||
|
|
||||||
|
**Test runner:** Vitest.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Any workflow-specific commands or hardcoded label/state semantics.
|
||||||
|
- Inline per-line PR review comments (a possible future addition; the primitive here is a plain PR comment).
|
||||||
|
- Multi-instance orchestration beyond what `tea`'s own login profiles already provide.
|
||||||
|
- A repo-level config file for dashboard customization (deferred post-MVP).
|
||||||
|
- A `dot` or any other host CLI's subcommand wrapping this tool — it is a standalone, independently distributed tool.
|
||||||
|
- Gitea Projects (kanban boards) — no REST API exists as of gitea-js v1.23.
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
- AXI ("Agent eXperience Interface"): https://axi.md/ and https://github.com/kunchenguid/axi.
|
||||||
|
Its reference implementation, `gh-axi` (https://github.com/kunchenguid/gh-axi), wraps GitHub's `gh` CLI.
|
||||||
|
The canonical principle text is authoritative for gitea-axi; gh-axi is a non-binding shape reference.
|
||||||
|
gitea-axi adopts gh-axi's `FieldDef` type system, `renderList()`/`renderDetail()` output helpers, typed `AxiError` classification, and suggestion normalization, and documents each deliberate departure in place.
|
||||||
|
- TOON format spec: https://toonformat.dev/.
|
||||||
|
The official TypeScript library is `@toon-format/toon`; no Go library exists, which was a decisive factor in the TypeScript language choice.
|
||||||
|
- `gitea-js` is the official TypeScript client for the Gitea API, generated from Gitea's OpenAPI spec.
|
||||||
|
It is the sole HTTP layer; no raw `fetch` calls are made outside of it.
|
||||||
|
- Tea's login store (`~/.config/tea/config.yml`) is read indirectly via `tea login list --output json`.
|
||||||
|
gitea-axi does not parse the YAML config file directly, to avoid coupling to tea's internal storage format.
|
||||||
|
- Tea was evaluated as the primary implementation strategy (subprocess wrapping with `--output json`) and rejected — see ADR 0002.
|
||||||
|
Tea improvements relevant to the gaps found (JSON output on create commands, non-interactive comment expansion) may be contributed upstream as separate PRs.
|
||||||
|
- The Gitea Go SDK (`gitea.dev/sdk`) was evaluated as an alternative to `gitea-js`.
|
||||||
|
It was rejected because it requires Go compilation, adding cross-compilation complexity for npm distribution.
|
||||||
|
- `gitea-axi` is unclaimed on npm and GitHub as of 2026-07-09.
|
||||||
|
- Developed against the operator's personal Gitea instance at `git.alexion.dev`; push-mirrored to GitHub for npm publishing and public discoverability.
|
||||||
Reference in New Issue
Block a user