docs: Requirements for the project.

This commit is contained in:
2026-07-10 22:37:38 -04:00
commit 21a075f8cd
19 changed files with 5378 additions and 0 deletions

View 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.

View 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.

View 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.

View 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.

View 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.

View 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`.

View 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.

View 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.

View 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).

View 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.

View 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.

View 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.

View 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.