docs: Requirements for the project.
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user