docs: adopt Conventional Commits and land planning docs

- CONVENTIONAL-COMMITS.md: vendored spec (v1.0.0); CLAUDE.md requires
  agent-written commits to follow it
- tasks 0002-0020: remaining task breakdown of the gitea-axi spec
  (0019 reframed around the three-tier test taxonomy: unit,
  integration, end-to-end)
- spec, CONTEXT.md, and ADRs 0006/0007/0011: pending design
  refinements from the planning sessions
This commit is contained in:
2026-07-11 07:14:39 -04:00
parent 38026f963d
commit d03a079854
26 changed files with 564 additions and 11 deletions

View File

@@ -44,6 +44,10 @@ _Avoid_: home view, status view
**renderDetail**: The output helper that formats a single entity's full detail as a TOON record. **renderDetail**: The output helper that formats a single entity's full detail as a TOON record.
_Avoid_: detail formatter, record renderer _Avoid_: detail formatter, record renderer
**action-block/entity-block pattern**: The uniform convention for mutation output — an action-named block (`created:`, `edited:`, `closed:`, `reopened:`, `merged:`) when the mutation actually ran, an entity-named block (`issue:`, `pull_request:`) when it was an idempotent no-op.
Applies across issue and PR mutations alike; a deliberate departure from gh-axi, which returns entity blocks for issue-side mutation successes.
_Avoid_: status block, result block
**count line**: The leading line in list output that states how many results were returned and their relationship to the total, e.g. `count: N of T total`. **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. 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 _Avoid_: summary line, header
@@ -93,6 +97,11 @@ Same idempotency rules as [[issue blocks]].
No gh-axi equivalent — Gitea-specific API (`/issues/{index}/dependencies`). No gh-axi equivalent — Gitea-specific API (`/issues/{index}/dependencies`).
_Avoid_: depends, depends-on, dependencies _Avoid_: depends, depends-on, dependencies
**search**: The full-text query commands (`search issues <query>`, `search prs <query>`), repo-scoped via `owner` param plus [[client-side filtering]] by repository (Gitea's `/repos/issues/search` has no repo-name filter).
Results use a locator schema (`number`, `title`, `state`, `author`, `created`) — search finds the number; `issue view` / `pr view` load the detail.
The forbidden `--search` flag on the list commands redirects here.
_Avoid_: query command, find
### Gitea API patterns ### Gitea API patterns
**type guard**: The defense against Gitea's unified issue/PR model, where issue endpoints also serve PRs. **type guard**: The defense against Gitea's unified issue/PR model, where issue endpoints also serve PRs.
@@ -102,7 +111,8 @@ Exception: `issue comment` stays permissive — PRs genuinely share the comment
_Avoid_: PR filtering, issue-only mode _Avoid_: PR filtering, issue-only mode
**reviewDecision**: A computed field (not returned by Gitea) that summarizes the overall review state of a PR. **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`. Derived client-side from the reviews list with an official-first fallback: if any review is `official=true`, only official reviews are considered; otherwise all reviews are (unprotected repos never produce official reviews).
Within the considered set: `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES`; `APPROVED` if any non-dismissed, non-stale review with state `APPROVED`; otherwise `REVIEW_REQUIRED` (rendered `required` — covers zero-review and comment-only PRs; there is no `none` value).
On `pr list`, this requires one extra parallel HTTP call per PR to fetch reviews. On `pr list`, this requires one extra parallel HTTP call per PR to fetch reviews.
_Avoid_: review status, review aggregate _Avoid_: review status, review aggregate
@@ -112,8 +122,9 @@ gitea-axi uses this as the equivalent of GitHub Check Runs for `pr checks` and t
Conclusion mapping: `success``pass`; `failure`/`error`/`warning``fail` (matching Gitea's own `Combine()` logic, which treats `warning` as failure); `skipped``skip`; `pending``pending`. 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 _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. **fetch-then-patch**: The pattern used for additive or subtractive mutations on list fields where Gitea's PATCH replaces the entire list rather than adding/removing individual entries — applies to assignees only.
gitea-axi reads the current list first, computes the desired list, then sends a single PATCH with the full resulting list. gitea-axi reads the current list first, computes the desired list, then sends a single PATCH with the full resulting list.
Reviewers do *not* use this pattern: `EditPullRequestOption` has no reviewers field; reviewer mutations go through the dedicated `POST`/`DELETE /pulls/{index}/requested_reviewers` endpoints (see ADR 0007 amendment).
_Avoid_: read-modify-write, merge-then-patch _Avoid_: read-modify-write, merge-then-patch
**client-side filtering**: The policy applied when Gitea's API does not support a given filter parameter. **client-side filtering**: The policy applied when Gitea's API does not support a given filter parameter.
@@ -122,7 +133,7 @@ When any client-side filter is active, the count line emits `count: N of T total
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. 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 _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`). **label name lookup**: The process of resolving a `--label <name>` string to a Gitea label ID before calling endpoints that require numeric IDs (e.g. `pr list --label`; note `issue list --label` does *not* need it — the issue-list endpoint accepts label names directly).
Implemented via `GET /repos/{owner}/{repo}/labels`; matched case-insensitively. 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. `--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 _Avoid_: label resolution, name-to-ID mapping

View File

@@ -23,3 +23,12 @@ Accepted explicitly: API call cost does not factor into design decisions for thi
- `pr list` with N results makes N+1 HTTP calls (list + N review fetches). - `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. - `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`. - The `reviewDecision` field appears in the default schema for both `pr list` and `pr view`.
## Amendment (2026-07-10): official-first fallback
The original logic required `official=true` for `APPROVED`, but Gitea only marks reviews official under branch protection with required approvals.
On unprotected repos (the norm on personal instances) every review is `official=false`, making `APPROVED` unreachable — an approved PR would show `required` forever.
Amended logic: if any review on the PR carries `official=true`, consider only official reviews (branch-protection semantics preserved); otherwise consider all reviews.
Within the considered set the derivation is unchanged: `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES`; `APPROVED` if any non-dismissed, non-stale review with state `APPROVED`; `REVIEW_REQUIRED` otherwise.
The otherwise-bucket (zero reviews, comment-only) renders `required`; gitea-axi deliberately has no `none` value, since detecting "review not formally required" would need admin-only branch-protection API access.

View File

@@ -27,3 +27,12 @@ Accepted cost: same policy as client-side filtering — extra HTTP calls do not
- The operation is not atomic: a concurrent edit between the GET and the PATCH could cause a lost update. - 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. 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. - `issue label --add` / `--remove` does NOT use fetch-then-patch — Gitea has dedicated additive label endpoints that are already idempotent.
## Amendment (2026-07-10): reviewers use dedicated endpoints, not fetch-then-patch
The original decision was wrong about reviewers on two counts:
`EditPullRequestOption` has no reviewers field at all (fetch-then-patch is impossible, not merely chosen against), and Gitea does have dedicated add/remove endpoints — `POST`/`DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers` with `{ reviewers: string[] }`.
`pr edit --add-reviewer` / `--remove-reviewer` therefore use the dedicated review-request endpoints, mirroring the label-mutation pattern.
Fetch-then-patch remains the pattern for assignees only (issues and PRs), where the PATCH body does replace the whole list and no dedicated endpoints exist.
`pr create --reviewer` is unaffected: `CreatePullRequestOption` accepts `reviewers` directly.

View File

@@ -17,3 +17,14 @@ One uniform code path, no remote mutation, no fork credentials.
- Same-repo and fork PRs check out identically. - Same-repo and fork PRs check out identically.
- Git subprocess failures (dirty worktree, network) classify as `GIT_ERROR`, carrying git's first stderr line. - 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. - The created local branch does not track the contributor's fork; pushing back to a fork branch is out of scope.
## Amendment (2026-07-10): three-case handling for existing local branches
The original two-step pipeline fails on re-checkout: git refuses to fetch into the currently checked-out branch, and a moved PR head makes the plain fetch non-fast-forward — so the second run of the same command errored, violating Principle 6.
Amended behavior:
1. Branch absent — original pipeline unchanged.
2. Branch exists, not checked out — force-fetch (`+pull/<n>/head:<branch>`) then checkout; the local branch is defined as a mirror of the PR head.
3. Branch currently checked out — fetch `pull/<n>/head` then `git merge --ff-only FETCH_HEAD`; divergence (local commits not on the PR head) surfaces as `GIT_ERROR` with explanatory help rather than being silently discarded.
A force-reset-always variant was rejected: fully idempotent but silently destroys local commits, which is unacceptable for unattended agents.

View File

@@ -57,6 +57,7 @@ The short version: tea's create commands have no `--output json` flag, its PR li
gitea-axi reads credentials from tea's login store via `tea login list --output json`. 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`. 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. 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.
When multiple logins match the hostname: if tea's default login is among them it is used; otherwise `VALIDATION_ERROR` listing the matching profile names, with help to pass `--login <name>` — an arbitrary identity is never picked silently.
Tea is used only for credential discovery — no commands are dispatched through the tea subprocess. Tea is used only for credential discovery — no commands are dispatched through the tea subprocess.
### Command Surface ### Command Surface
@@ -93,7 +94,7 @@ This holds even when invoked by the SessionStart hook; error noise in non-Gitea
`--sort <created|updated|comments>` (client-side — Gitea issue list has no sort param; always descending, matching gh-axi); `--sort <created|updated|comments>` (client-side — Gitea issue list has no sort param; always descending, matching gh-axi);
`--limit <n>` (default 30); `--limit <n>` (default 30);
`--fields <a,b,c>`. `--fields <a,b,c>`.
`--search` is explicitly forbidden (VALIDATION_ERROR). `--search` is explicitly forbidden (VALIDATION_ERROR; help: `` Use `gitea-axi search issues "<query>"` for full-text search ``).
Always passes `type=issues` — Gitea's issue endpoints also serve PRs, which must never appear in issue lists (see Issue/PR Type Guard). 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. 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). Default output fields (matching gh-axi): `number`, `title`, `state` (lowercased), `author` (plucked from `user.login`), `created` (relative time).
@@ -127,6 +128,7 @@ Extra fields available via `--fields`: `labels`, `assignees`, `milestone`, `body
`--add-assignee <login>`; `--add-assignee <login>`;
`--remove-assignee <login>`; `--remove-assignee <login>`;
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found). `--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found).
Output on success: `edited: { number, status: "ok" }` — the action-block/entity-block pattern (see `pr create`) applied uniformly across issue and PR mutations; a deliberate departure from gh-axi, whose `issue edit` returns the updated `issue:` entity block.
Label mutations use Gitea's dedicated additive/removal label endpoints (idempotent). 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). `--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. `--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.
@@ -138,10 +140,12 @@ Closing sets `state: "closed"` via PATCH on the issue.
`--reason` is excluded (Gitea has no `state_reason` concept). `--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. 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. 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.
Output on success: `closed: { number, status: "ok" }` (action-block/entity-block pattern).
Idempotent: returns early with `message: "Already closed"` if already closed. Idempotent: returns early with `message: "Already closed"` if already closed.
**`issue reopen <n>`** **`issue reopen <n>`**
Sets `state: "open"` via PATCH. Sets `state: "open"` via PATCH.
Output on success: `reopened: { number, status: "ok" }` (action-block/entity-block pattern).
Idempotent: returns early with `message: "Already open"` if already open. Idempotent: returns early with `message: "Already open"` if already open.
**`issue comment <n> [flags]`** **`issue comment <n> [flags]`**
@@ -203,7 +207,7 @@ No gh-axi equivalent.
`--sort <oldest|recentupdate|leastupdate|mostcomment|leastcomment|priority>` (Gitea-specific extension — maps directly to the API `sort` param); `--sort <oldest|recentupdate|leastupdate|mostcomment|leastcomment|priority>` (Gitea-specific extension — maps directly to the API `sort` param);
`--limit <n>` (default 30); `--limit <n>` (default 30);
`--fields <a,b,c>`. `--fields <a,b,c>`.
`--search` is explicitly forbidden (VALIDATION_ERROR). `--search` is explicitly forbidden (VALIDATION_ERROR; help: `` Use `gitea-axi search prs "<query>"` for full-text search ``).
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). 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`. 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). `reviewDecision` is computed client-side by fetching reviews for each PR in parallel (one extra HTTP call per PR; see ADR 0006).
@@ -250,11 +254,13 @@ Output on success: `created: { number, url }` — completing gh-axi's action-blo
`--remove-reviewer <login>`; `--remove-reviewer <login>`;
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found); `--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found);
`--base <branch>`. `--base <branch>`.
Assignee and reviewer mutations use fetch-then-patch (see ADR 0007). Assignee mutations use fetch-then-patch (see ADR 0007).
Reviewer mutations use Gitea's dedicated review-request endpoints (`POST`/`DELETE /pulls/{index}/requested_reviewers` with `{ reviewers: [login] }`) — `EditPullRequestOption` has no reviewers field, so fetch-then-patch is structurally impossible for reviewers (see ADR 0007 amendment).
Output: `edited: { number, status: "ok" }`. Output: `edited: { number, status: "ok" }`.
**`pr close <n> [flags]`** **`pr close <n> [flags]`**
`--comment <text>`. `--comment <text>`.
When `--comment` is provided, two API calls are made (PATCH to close, then POST the comment); if the PATCH succeeds but the POST fails, the error is surfaced — same partial-failure policy as `issue close`.
Idempotent: returns `pull_request: { number, state, already: true }` if already closed or merged. Idempotent: returns `pull_request: { number, state, already: true }` if already closed or merged.
Output on success: `closed: { number, status: "ok" }`. Output on success: `closed: { number, status: "ok" }`.
@@ -292,9 +298,10 @@ Truncation limit: 4000 chars.
Output: `pr_diff: { number, diff[, truncated, original_length] }`. Output: `pr_diff: { number, diff[, truncated, original_length] }`.
**`pr checkout <n>`** **`pr checkout <n>`**
Fetches the PR head branch name from `GET /pulls/{index}` (`head.ref` field), then runs in the current working directory: Fetches the PR head branch name from `GET /pulls/{index}` (`head.ref` field), then runs in the current working directory, three-cased on the local branch state so a re-checkout is idempotent (git refuses to fetch into the checked-out branch, and a moved PR head makes the plain fetch non-fast-forward):
1. `git fetch origin pull/<n>/head:<branch>` 1. Branch absent: `git fetch origin pull/<n>/head:<branch>`, then `git checkout <branch>`.
2. `git checkout <branch>` 2. Branch exists, not checked out: `git fetch origin +pull/<n>/head:<branch>` (force — the branch mirrors the PR head), then `git checkout <branch>`.
3. Branch currently checked out: `git fetch origin pull/<n>/head`, then `git merge --ff-only FETCH_HEAD`; if not fast-forwardable (local commits diverge from the PR head), `GIT_ERROR` with a help line explaining the divergence — local commits are never discarded silently.
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). 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. 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" }`. Output: `checkout: { number, branch, status: "ok" }`.
@@ -347,10 +354,23 @@ Output: `edit: ok`, `label: <new-name-or-original-name>`.
Not idempotent: a nonexistent label errors with `VALIDATION_ERROR` rather than reporting success (see ADR 0010). Not idempotent: a nonexistent label errors with `VALIDATION_ERROR` rather than reporting success (see ADR 0010).
Output: `delete: ok`, `label: <name>`. Output: `delete: ok`, `label: <name>`.
#### Search Commands
**`search issues <query> [flags]`** / **`search prs <query> [flags]`**
Full-text search within the current repository, added because `--search` on the list commands is forbidden and agents need a text-query escape hatch (the forbidden-flag error redirects here, mirroring gh-axi).
The positional `<query>` is required (`VALIDATION_ERROR` if missing).
Endpoint: `GET /repos/issues/search` with `q=<query>`, `type=issues` or `type=pulls`, and `owner=<owner>`.
The endpoint has no repo-name filter, so results are additionally filtered client-side to the current repository via each result's `repository` field — the standard client-side filtering policy applies, including its `count: N of T total` rule with `T` from the filtered set.
Flags: `--state <open|closed|all>` (default open); `--label <name>` (API-supported — comma-separated names); `--limit <n>` (default 30); `--fields <a,b,c>`.
Default output fields (both commands): `number`, `title`, `state`, `author`, `created` — a locator schema; search results are Issue-shaped for both types, and `draft`/`review` parity with `pr list` would require two extra fetches per result for a command whose job is finding the number to feed into `issue view` / `pr view`.
Output blocks: `issues:` / `pull_requests:`, matching the list commands.
Empty state: standard `<noun>[0]: (none)`.
#### Setup Command #### Setup Command
**`setup`** **`setup`**
Installs the bundled Agent Skill markdown into `~/.claude/skills/` (see ADR 0009). Installs the bundled Agent Skill markdown into `~/.claude/skills/` (see ADR 0009).
The skill is a minimal pointer, not a command reference: its frontmatter description triggers on Gitea issue/PR/label work; its body says when to use gitea-axi (over tea, raw API calls, or git), lists the command groups with one-line summaries, and points at the bare `gitea-axi` dashboard and per-command `--help` for discovery — the CLI remains the single source of interface truth.
This is gitea-axi's primary fulfillment of AXI Principle 7 (Ambient context): an explicit setup command, matching gh-axi's `setup`. 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. Idempotent: re-running reports already-installed/updated rather than failing.
Output: `setup: { skill, path, status: <installed|updated|unchanged> }`. Output: `setup: { skill, path, status: <installed|updated|unchanged> }`.
@@ -414,7 +434,10 @@ Exception: `issue comment` stays permissive — PRs genuinely share the comment
Gitea has no aggregated `reviewDecision` field on the PR object. Gitea has no aggregated `reviewDecision` field on the PR object.
gitea-axi computes it client-side from the reviews list (see ADR 0006). 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. Scope (official-first fallback): if any review on the PR carries `official=true`, only official reviews are considered (branch-protection semantics preserved); otherwise all reviews are considered — unprotected repos never produce official reviews, so without the fallback `APPROVED` would be unreachable there.
Logic within the considered set: `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES` exists; `APPROVED` if at least one review with state `APPROVED` has `stale=false` and `dismissed=false`; `REVIEW_REQUIRED` otherwise.
The otherwise-bucket includes zero-review PRs and comment-only reviews: it renders as `required`, meaning "no conclusive review yet".
There is no `none` value — a deliberate three-value departure from gh-axi's four-value mapping, since Gitea offers no non-admin way to detect whether branch protection formally requires review.
On `pr list`, reviews for each PR are fetched in parallel (one extra HTTP call per PR in the list). On `pr list`, reviews for each PR are fetched in parallel (one extra HTTP call per PR in the list).
### Context Override Flags ### Context Override Flags
@@ -476,12 +499,17 @@ API error responses are classified by HTTP status code and calling context:
| 404 | called on `/repos/.../issues/{index}` | `ISSUE_NOT_FOUND` | | 404 | called on `/repos/.../issues/{index}` | `ISSUE_NOT_FOUND` |
| 404 | called on `/repos/.../pulls/{index}` | `PR_NOT_FOUND` | | 404 | called on `/repos/.../pulls/{index}` | `PR_NOT_FOUND` |
| 404 | other paths | `UNKNOWN` | | 404 | other paths | `UNKNOWN` |
| 405 | any | `VALIDATION_ERROR` (body message surfaced — e.g. PR not mergeable due to conflicts or unmet checks; help: `pr update-branch <n>` or `pr checkout <n>` to resolve) |
| 409 | any | `VALIDATION_ERROR` (body message surfaced — e.g. head changed since merge was requested, or auto-merge already scheduled) |
| 422 | any | `VALIDATION_ERROR` (body message surfaced) | | 422 | any | `VALIDATION_ERROR` (body message surfaced) |
| 429 | any | `RATE_LIMITED` (help: wait and retry, or reduce `--limit`) | | 429 | any | `RATE_LIMITED` (help: wait and retry, or reduce `--limit`) |
| other | any | `UNKNOWN` | | other | any | `UNKNOWN` |
`TEA_NOT_INSTALLED` is emitted if the tea binary is not found during credential discovery. `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>`). Login matching against the detected hostname is a three-way split:
tea installed with zero logins configured → `AUTH_REQUIRED` (the tool was never set up; help: `` Run `tea login add` ``);
tea has logins but none match the detected hostname → `REPO_NOT_FOUND` (the repo is not recognized as belonging to a known Gitea instance — a remote URL's shape cannot reveal Gitea-ness, so an unmatched host most likely means a non-Gitea repo such as a GitHub clone; help: `` Run `tea login add --url <host>` `` if this is a Gitea instance, or pass `-R` + `--login`);
HTTP 401 from the API → `AUTH_REQUIRED` (token invalid or revoked), per the status table.
A `--login` value naming a nonexistent profile is `VALIDATION_ERROR`, listing the available profile names. 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. `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. Error output is TOON-encoded to stdout (not stderr): `error: <message>`, `code: <CODE>`, and optionally `help[N]:` with suggestion lines.
@@ -554,6 +582,8 @@ This means tests exercise the full reshaping pipeline — JSON parse, field extr
- Local / unit tier: the fixture server runs fast with no external dependencies. - Local / unit tier: the fixture server runs fast with no external dependencies.
Used for all command-level assertions. 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. - 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.
CI runs on Gitea Actions on `git.alexion.dev` (where the PRs live), with the disposable Gitea as a docker service container pinned to the latest stable image tag, bumped deliberately.
The workflow file stays GitHub-Actions-compatible so the GitHub mirror can adopt it nearly verbatim later.
**Test runner:** Vitest. **Test runner:** Vitest.

View File

@@ -0,0 +1,21 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
Complete the `issue list` flag surface on top of the minimal version from the tracer slice.
API-supported filters: `--label` (comma-separated names, passed through), `--assignee` (maps to `assigned_by`), `--author` (maps to `created_by`), `--milestone` (maps to `milestones`).
Client-side sort: `--sort <created|updated|comments>`, always descending, paginating fully before sorting while keeping the count line's `T` from the `X-Total-Count` header (sort reorders without changing membership).
Field selection: `--fields <a,b,c>` exposing the extra fields `body` (raw), `closedAt`, `labels` (joined names), `milestone` (title), `updatedAt`, `url`, built on the FieldDef extractor system.
`--search` is explicitly forbidden with a `VALIDATION_ERROR` redirecting to `search issues`.
## Acceptance criteria
- [ ] `--label`, `--assignee`, `--author`, and `--milestone` map to their Gitea API query params and filter server-side
- [ ] `--sort <created|updated|comments>` reorders descending client-side after full pagination; the count line still reports `T` from `X-Total-Count`
- [ ] `--fields` selects among the documented extra fields, each rendered via its FieldDef extractor (relative times, joined label names, milestone title)
- [ ] Output contains no `type` field
- [ ] `--search` fails with `VALIDATION_ERROR` (exit 2) and a help line pointing at `gitea-axi search issues "<query>"`
- [ ] Fixture-server tests cover each filter, client-side sort with pagination, `--fields` extraction, and the forbidden `--search`

View File

@@ -0,0 +1,26 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
`issue view <n>` as a detail command, introducing the content-truncation and cleanBody machinery that later slices reuse.
Default output: `number`, `title`, `state`, `author`, `created`, `body` (truncated at 500 chars), plus `comment_count`.
`--comments` renders all comments with no count cap, each body truncated at 800 chars with cleanBody applied.
`--full` suppresses all truncation in the output — issue body and comment bodies alike.
cleanBody runs only when the raw body exceeds the truncation limit: it normalizes Gitea issue/PR URLs on the detected hostname to `Issue#N`/`PR#N`, strips markdown image embeds, removes long URLs, and collapses email-style quoted blocks; if cleaning brings the body under the limit the cleaned body is returned with a note, otherwise it is truncated with the inline hint.
Type guard: viewing a PR number fails with `VALIDATION_ERROR` ("issue #N is a pull request") and a `pr view <n>` help line.
No `type` field and no sub-issue augmentation.
## Acceptance criteria
- [ ] `issue view <n>` renders the default detail fields plus `comment_count` via renderDetail
- [ ] Bodies over 500 chars are cleaned then truncated with the inline hint `"... (truncated, N chars total - use --full to see complete body)"`; bodies at or under the limit pass through untouched
- [ ] cleanBody normalizes issue/PR URLs using the detected hostname, strips image embeds and long URLs, and collapses quoted blocks
- [ ] `--comments` renders every comment (no cap), each body cleaned and truncated at 800 chars
- [ ] `--full` returns raw, untruncated body and comment bodies
- [ ] A PR number yields `VALIDATION_ERROR` (exit 2) with the "is a pull request" message and a `pr view <n>` help line, detected via the fetched object's `pull_request` field
- [ ] A nonexistent issue yields `ISSUE_NOT_FOUND` (exit 1)
- [ ] Single-entity next-step suggestions fill the actual issue number rather than a placeholder
- [ ] Fixture-server tests cover truncation boundaries, cleanBody transforms, `--comments`, `--full`, and the type guard

View File

@@ -0,0 +1,23 @@
---
spec: gitea-axi
blocked-by: 0003-issue-view-and-truncation
---
## What to build
The first mutations: `issue create` and `issue comment`, introducing the shared machinery for `--body-file`, name→ID resolution, and mutation output blocks.
`issue create` takes `--title` (required), `--body`/`--body-file`, `--assignee`, repeatable `--label` (resolved to label ID via the case-insensitive label lookup), and `--milestone` (resolved via the milestone name query); `--project` and `--type` are excluded.
Create output is the entity block `issue: { number, title, state, url }` with extra fields (`labels`, `assignees`, `milestone`, `body`) available via `--fields`.
`issue comment <n>` requires `--body`/`--body-file` and returns the created comment directly from the POST response as `comment: { number, author, created, body }` with the body truncated at 800 chars — no follow-up view call needed; the comment's own id is not output.
`issue comment` stays permissive toward PR numbers (PRs genuinely share the comment endpoint).
## Acceptance criteria
- [ ] `issue create --title` creates an issue and outputs `issue: { number, title, state, url }` where `url` is `html_url`
- [ ] Missing `--title` fails immediately with `VALIDATION_ERROR` (exit 2) before any API call
- [ ] `--body-file <path>` reads the body from a file; `--body` and `--body-file` together are rejected
- [ ] `--label` resolves each name to an ID via case-insensitive lookup against the repo's labels; an unknown name yields `VALIDATION_ERROR`
- [ ] `--milestone` resolves the name via the milestone query; an unknown name yields `VALIDATION_ERROR`
- [ ] `issue comment <n> --body` posts and outputs `comment: { number, author, created, body }` built from the POST response, body cleaned and truncated at 800 chars, where `number` is the issue number
- [ ] `issue comment` accepts a PR number without a type-guard error
- [ ] Fixture-server tests cover create with labels/milestone, both body sources, comment output shape, and each validation failure

View File

@@ -0,0 +1,24 @@
---
spec: gitea-axi
blocked-by: 0004-issue-create-and-comment
---
## What to build
The issue state-transition mutations: `issue edit`, `issue close`, `issue reopen`.
`issue edit <n>` supports `--title`, `--body`/`--body-file`, `--add-label`, `--remove-label`, `--add-assignee`, `--remove-assignee`, and `--milestone` (name-resolved).
Label mutations use Gitea's dedicated additive/removal label endpoints: `--add-label` passes the name directly (no lookup); `--remove-label` resolves the name to an ID case-insensitively, erroring if the label does not exist in the repo, and treating Gitea's 404 for a label not applied to the issue as silent success.
Assignee mutations introduce fetch-then-patch: read the current assignee list, apply the change in-process, send the full resulting list in one PATCH (see ADR 0007).
`issue close <n>` PATCHes `state: "closed"`, with optional `--comment` as a second API call whose failure is surfaced rather than swallowed; `--reason` is excluded.
`issue reopen <n>` PATCHes `state: "open"`.
All three use the action-block pattern on success (`edited:`/`closed:`/`reopened:` with `{ number, status: "ok" }`) — a deliberate departure from gh-axi's entity block on `issue edit` — and close/reopen return early with an `Already closed`/`Already open` message when a no-op.
## Acceptance criteria
- [ ] `issue edit` applies title, body, and milestone changes and outputs `edited: { number, status: "ok" }`
- [ ] `--add-label` posts the name directly to the additive label endpoint; `--remove-label` resolves the ID first, yields `VALIDATION_ERROR` for a name not in the repo, and treats a 404 for an unapplied label as silent success
- [ ] `--add-assignee`/`--remove-assignee` use fetch-then-patch, sending the full resulting assignee list in a single PATCH
- [ ] `issue close <n>` outputs `closed: { number, status: "ok" }`; with `--comment` the comment is posted after the close, and a comment-post failure surfaces as an error even though the issue is closed
- [ ] `issue close` on an already-closed issue and `issue reopen` on an already-open issue return early with `message: "Already closed"` / `message: "Already open"` and exit 0
- [ ] `issue reopen <n>` outputs `reopened: { number, status: "ok" }`
- [ ] Fixture-server tests cover each mutation path, both idempotent no-ops, the unapplied-label silent success, and the close-comment partial failure

View File

@@ -0,0 +1,19 @@
---
spec: gitea-axi
blocked-by: 0004-issue-create-and-comment
---
## What to build
The remaining simple issue mutations: `issue delete`, `issue pin`, `issue unpin`.
`issue delete <n>` hard-deletes via the DELETE endpoint (requires admin or owner permissions) and is deliberately not idempotent: a nonexistent issue errors with `ISSUE_NOT_FOUND` rather than reporting success (see ADR 0010).
`issue pin <n>` and `issue unpin <n>` call the pin endpoints and are idempotent, returning early with `Already pinned`/`Already unpinned` messages.
`issue lock`/`unlock`, `issue transfer`, and `issue subissue` remain excluded per the spec.
## Acceptance criteria
- [ ] `issue delete <n>` outputs `issue: { number, status: "deleted" }` on success
- [ ] Deleting a nonexistent issue yields `ISSUE_NOT_FOUND` (exit 1), not idempotent success
- [ ] `issue pin <n>` outputs `issue: { number, state, pinned }`; pinning an already-pinned issue returns early with `message: "Already pinned"` and exit 0
- [ ] `issue unpin <n>` mirrors pin with `message: "Already unpinned"` on the no-op
- [ ] Fixture-server tests cover delete success, delete-missing refusal, and both pin/unpin no-ops

View File

@@ -0,0 +1,20 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
The two Gitea-specific dependency subcommand groups — `issue blocks <list|add|remove>` and `issue blocked-by <list|add|remove>` — over Gitea's blocks and dependencies endpoints.
`blocks` manages downstream dependents (issues that cannot proceed until this one is resolved); `blocked-by` manages upstream blockers.
List output blocks are `blocked_issues` and `blocking_issues`; add outputs `blocks: { issue, blocks }` / `blocked_by: { issue, blocked_by }`.
Idempotency: `add` of an existing relationship does a fetch-first check and returns `already: true`; `remove` of a nonexistent relationship is silent success; genuine validation failures (self-reference, cycles) surface as `VALIDATION_ERROR` via the 422 mapping.
No gh-axi equivalent exists — the interface shape follows this spec alone.
## Acceptance criteria
- [ ] `issue blocks list <n>` and `issue blocked-by list <n>` render their respective output blocks with count lines and explicit empty states
- [ ] `issue blocks add <n> <target>` outputs `blocks: { issue: n, blocks: target }`; `issue blocked-by add <n> <blocker>` outputs `blocked_by: { issue: n, blocked_by: blocker }`
- [ ] Adding an existing relationship returns `already: true` (fetch-first check, no duplicate POST); removing a nonexistent relationship exits 0 silently-successfully
- [ ] Self-reference and cycle rejections from Gitea surface as `VALIDATION_ERROR` (exit 2) with the server's message
- [ ] Fixture-server tests cover list, add, idempotent re-add, remove, idempotent re-remove, and a 422 cycle rejection for both groups

View File

@@ -0,0 +1,25 @@
---
spec: gitea-axi
blocked-by: 0004-issue-create-and-comment
---
## What to build
`pr list`, introducing two policies that later slices reuse: client-side filtering with its count-line rule (see ADR 0005) and the reviewDecision computation via parallel review fetches (see ADR 0006).
API-supported flags: `--state`, `--author` (maps to `poster`), `--label` (name→ID via the case-insensitive label lookup, since the PR list endpoint takes label IDs), `--label-id` (Gitea-specific bypass), `--sort` (Gitea-specific values passed straight to the API), `--limit`, `--fields`.
Client-side filters (no API param exists): `--assignee`, `--base`, `--head`, `--draft` — each paginates fully at 50 per page and filters in-process, with the count line's `T` computed from the filtered set instead of the misleading `X-Total-Count`.
Default fields: `number`, `title`, `state`, `author`, `draft` (bool→yes/no), `review` — the reviewDecision mapped to `approved`/`changes_requested`/`required`.
reviewDecision uses the official-first fallback: only official reviews count when any exist, otherwise all reviews; `CHANGES_REQUESTED` beats `APPROVED`, non-stale non-dismissed approval wins, everything else is `required`; there is no `none` value.
`--search` is forbidden with a redirect to `search prs`.
## Acceptance criteria
- [ ] `pr list` renders the default fields with `review` computed from one parallel review fetch per PR
- [ ] reviewDecision honors the official-first fallback and maps to the three lowercase values, with zero-review and comment-only PRs rendering `required`
- [ ] `--label` resolves the name case-insensitively to an ID (`VALIDATION_ERROR` if unknown); `--label-id` bypasses the lookup
- [ ] `--author` and `--sort` map to their API params; `--sort` accepts the six Gitea values
- [ ] `--assignee`, `--base`, `--head`, and `--draft` filter client-side after full pagination, and the count line reports `count: N of T total` with `T` from the in-memory filtered set
- [ ] `--fields` exposes `body`, `createdAt`, `labels`, `milestone`, `mergedAt`, `url`
- [ ] `--search` fails with `VALIDATION_ERROR` (exit 2) pointing at `gitea-axi search prs "<query>"`
- [ ] Empty result emits `pull_requests[0]: (none)` plus a suggestion
- [ ] Fixture-server tests cover the review computation variants (official/unofficial, stale, dismissed), each client-side filter with its count line, the label lookup, and the forbidden flag

View File

@@ -0,0 +1,23 @@
---
spec: gitea-axi
blocked-by: [0003-issue-view-and-truncation, 0008-pr-list]
---
## What to build
`pr view <n>` and `pr checks <n>`, built on the truncation machinery and the reviewDecision computation from earlier slices.
`pr view` always makes three API calls — the PR fetch and the reviews fetch in parallel, then the combined commit status once the head SHA is known — so default output includes `number`, `title`, `state`, `author`, `draft`, `merged`, `checks`, `body` (truncated at 500), `comment_count`, and `review_count` without extra flags.
The `checks` field renders as the summary string (`N passed, N failed[, N skipped][, N pending], N total`) or the explicit no-CI message; commit-status states map `success``pass`, `failure`/`error`/`warning``fail`, `skipped``skip`, `pending``pending`.
`--reviews` additionally fetches per-review inline comments and exposes Gitea-specific `official` and `stale` fields; `--comments` renders all comments at the 800-char truncation; `--full` suppresses all truncation.
`pr checks <n>` outputs the same summary line followed by a `checks` list of `{ name, conclusion }`.
## Acceptance criteria
- [ ] `pr view <n>` renders the default fields including `checks`, `comment_count`, and `review_count` from the three-call fetch pattern
- [ ] Commit-status states map to the four conclusions per the spec, `warning` counting as failure
- [ ] A PR with no statuses renders the `"0 passed, 0 failed — this PR has no CI checks configured"` message in both commands
- [ ] `--reviews` lists reviews with `official` and `stale` fields plus their inline comments
- [ ] `--comments` and `--full` behave as on `issue view` (800-char comment truncation with cleanBody; `--full` suppresses everything)
- [ ] `pr checks <n>` outputs the summary line followed by `{ name, conclusion }` rows
- [ ] A nonexistent PR yields `PR_NOT_FOUND` (exit 1)
- [ ] Fixture-server tests cover the status mapping including `skipped` and `warning`, the no-CI case, `--reviews`, and truncation behavior

View File

@@ -0,0 +1,23 @@
---
spec: gitea-axi
blocked-by: 0004-issue-create-and-comment
---
## What to build
`pr create` and `pr comment`.
`pr create` takes `--title` (required), `--body`/`--body-file`, `--base`, `--head`, `--assignee`, `--reviewer`, repeatable `--label` (name-resolved), and `--milestone` (name-resolved); `--draft` and `--project` are excluded (no Gitea API support).
When `--head` is omitted it defaults to the current local branch from git; when `--base` is omitted it defaults to the repository's default branch fetched from the repo endpoint.
Idempotent: before creating, check for an existing open PR for the same base/head pair; if found, return `pull_request: { number, url, already: true }` instead of creating a duplicate.
Success output is the action block `created: { number, url }` — action block when the mutation ran, entity block when it was a no-op.
`pr comment <n>` posts through the shared issue-comment endpoint and returns the created comment as `comment: { number, author, created, body }` (800-char truncation), diverging from gh-axi's status-only block to save a follow-up view call (see ADR 0008).
## Acceptance criteria
- [ ] `pr create --title` creates a PR and outputs `created: { number, url }`
- [ ] Omitted `--head` resolves to the current local branch; omitted `--base` resolves to the repo's default branch
- [ ] An existing open PR for the same branch pair short-circuits to `pull_request: { number, url, already: true }` with no duplicate created
- [ ] `--label` and `--milestone` resolve names case-insensitively with `VALIDATION_ERROR` on unknown names; `--assignee` and `--reviewer` pass through
- [ ] `pr comment <n> --body` outputs `comment: { number, author, created, body }` from the POST response, body truncated at 800 chars
- [ ] Missing required inputs (`--title` on create, body on comment) fail with `VALIDATION_ERROR` (exit 2) before any API call
- [ ] Fixture-server tests cover creation with defaults, the idempotent short-circuit, name resolution failures, and the comment output shape

View File

@@ -0,0 +1,22 @@
---
spec: gitea-axi
blocked-by: 0005-issue-edit-close-reopen
---
## What to build
The PR-side state mutations: `pr edit`, `pr close`, `pr reopen`, mirroring the issue-side slice with two PR-specific differences.
`pr edit <n>` supports `--title`, `--body`/`--body-file`, `--add-label`/`--remove-label` (same additive-endpoint and lookup rules as issues), `--add-assignee`/`--remove-assignee` (fetch-then-patch), `--add-reviewer`/`--remove-reviewer`, `--milestone` (name-resolved), and `--base`.
Reviewer mutations cannot use fetch-then-patch — `EditPullRequestOption` has no reviewers field — so they go through Gitea's dedicated requested-reviewers POST/DELETE endpoints (see ADR 0007 amendment).
`pr close <n>` supports `--comment` with the same two-call partial-failure policy as `issue close`, and returns `pull_request: { number, state, already: true }` when already closed or merged.
`pr reopen <n>` returns `pull_request: { number, state: "open", already: true }` when already open.
Success outputs follow the action-block pattern: `edited:`/`closed:`/`reopened:` with `{ number, status: "ok" }`.
## Acceptance criteria
- [ ] `pr edit` applies title, body, milestone, and base changes and outputs `edited: { number, status: "ok" }`
- [ ] Label and assignee mutations follow the same rules as `issue edit` (additive endpoints, fetch-then-patch, unapplied-label silent success)
- [ ] `--add-reviewer`/`--remove-reviewer` call the requested-reviewers endpoints with `{ reviewers: [login] }`
- [ ] `pr close --comment` posts the comment after the PATCH and surfaces a comment failure; closing an already-closed-or-merged PR returns the entity block with `already: true`
- [ ] `pr reopen` on an open PR returns the entity block with `already: true`; otherwise outputs `reopened: { number, status: "ok" }`
- [ ] Fixture-server tests cover reviewer add/remove, the merged-PR close no-op, and the reopen paths

View File

@@ -0,0 +1,23 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
`pr merge` and `pr update-branch`.
`pr merge <n>` supports `--method` with all six Gitea methods (`merge`, `squash`, `rebase`, `rebase-merge`, `fast-forward-only`, `manually-merged`), the three common-method shorthands (`--merge`, `--squash`, `--rebase`), `--auto`, `--delete-branch`, `--body`/`--body-file`, `--subject`, and `--merge-commit-id`.
`--merge-commit-id` is required with `--method manually-merged` and rejected with any other method — both violations are `VALIDATION_ERROR` before any API call, as are conflicting shorthands.
Idempotent: an already-merged PR returns `pull_request: { number, state: "merged", merged_by, merged_at }` without calling the merge API.
Merge-blocked conditions surface through the standard 405/409 → `VALIDATION_ERROR` mapping with the server's message and remediation help lines.
`pr update-branch <n>` merges the base branch into the PR head via the update endpoint with `--style <merge|rebase>` (default merge).
## Acceptance criteria
- [ ] `--method` accepts all six methods and the shorthands map to their methods; conflicting or duplicate action flags yield `VALIDATION_ERROR` (exit 2) before any API call
- [ ] `--merge-commit-id` without `manually-merged`, or `manually-merged` without `--merge-commit-id`, both yield `VALIDATION_ERROR` locally
- [ ] Successful merge outputs `merged: { number, status: "ok", method }`
- [ ] An already-merged PR short-circuits to the entity block with `merged_by` and `merged_at`, exit 0, no merge API call
- [ ] A 405 not-mergeable response surfaces as `VALIDATION_ERROR` with help suggesting `pr update-branch <n>` or `pr checkout <n>`
- [ ] `pr update-branch <n> --style rebase` calls the update endpoint with the style param and outputs `updated: { number, status: "ok" }`
- [ ] Fixture-server tests cover each method, the local validations, the idempotent no-op, and the 405/409 mappings

View File

@@ -0,0 +1,19 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
`pr review <n>` with the three action flags `--approve`, `--request-changes`, `--comment`, plus `--body`/`--body-file`.
Exactly one action flag is required: zero or multiple yield `VALIDATION_ERROR` before any API call, mirroring the 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 }`.
## Acceptance criteria
- [ ] Each action flag submits the corresponding review event and outputs `review: { number, action }`
- [ ] Zero action flags, or more than one, yield `VALIDATION_ERROR` (exit 2) with no API call
- [ ] A server-side 422 for a missing body surfaces as `VALIDATION_ERROR` carrying Gitea's message
- [ ] `--body-file` works as everywhere else
- [ ] Fixture-server tests cover all three actions, the flag-count validations, and the 422 passthrough

View File

@@ -0,0 +1,23 @@
---
spec: gitea-axi
blocked-by: 0003-issue-view-and-truncation
---
## What to build
The two PR commands that touch content and the local worktree: `pr diff` and `pr checkout`.
`pr diff <n>` fetches the raw diff from the `.diff` endpoint and truncates at 4000 chars, signaling truncation with separate `truncated: true` and `original_length: N` fields (not an inline hint) plus a prepended `--full` suggestion; `--full` suppresses diff truncation.
Output: `pr_diff: { number, diff[, truncated, original_length] }`.
`pr checkout <n>` reads the PR head branch name from the PR fetch, then fetches `refs/pull/{n}/head` from origin — which works uniformly for same-repo and fork PRs (see ADR 0011) — three-cased on local branch state so re-checkout is idempotent:
absent branch → fetch into it and check out; existing unchecked-out branch → force-fetch (the branch mirrors the PR head) and check out; currently checked-out branch → plain fetch then `--ff-only` merge, failing with `GIT_ERROR` and a divergence explanation if local commits diverge — never discarding them silently.
This slice introduces `GIT_ERROR`: non-zero git subprocess exits map to it, carrying git's first stderr line and a remediation help line.
Output: `checkout: { number, branch, status: "ok" }`.
## Acceptance criteria
- [ ] `pr diff <n>` outputs the diff, adding `truncated: true` and `original_length` when over 4000 chars plus a `--full` next-step suggestion; `--full` returns the raw diff
- [ ] `pr checkout <n>` handles all three local-branch cases and re-running it is idempotent
- [ ] A checked-out branch that has diverged from the PR head fails with `GIT_ERROR` and an explanatory help line, leaving local commits intact
- [ ] Other git failures (dirty worktree, network) map to `GIT_ERROR` with git's first stderr line
- [ ] Checkout works for a fork PR whose head repo is not a configured remote (via `refs/pull/{n}/head`)
- [ ] Tests cover diff truncation boundaries and the three checkout cases (git behavior exercised against a scratch repository, API responses from the fixture server)

View File

@@ -0,0 +1,22 @@
---
spec: gitea-axi
blocked-by: 0004-issue-create-and-comment
---
## What to build
The label command group: `label list`, `label create`, `label edit`, `label delete`.
`label list` takes `--limit` (default 500) and outputs a count line plus `labels: [ { name } ]`.
`label create` requires `--name` and `--color` (hex without `#`; the `#` is prepended before the API call) with optional `--description`; it is idempotent via a case-insensitive existence check, reporting `create: already_exists` instead of failing.
`label edit <name>` and `label delete <name>` resolve the positional name via the standard case-insensitive label lookup with `VALIDATION_ERROR` when not found.
`label delete` is deliberately not idempotent — a nonexistent label is refused rather than reported as success (see ADR 0010).
## Acceptance criteria
- [ ] `label list` renders the count line and `labels:` block; empty repos get the explicit empty state
- [ ] `label create --name --color` creates the label, prepending `#` to the color, and outputs `created: ok` + `label: <name>`
- [ ] Creating an existing label (case-insensitive) outputs `create: already_exists` + the existing name, exit 0
- [ ] `label edit <name>` applies `--name`/`--color`/`--description` and outputs `edit: ok` + the resulting name
- [ ] `label edit`/`label delete` on an unknown name yield `VALIDATION_ERROR` (exit 2)
- [ ] `label delete <name>` outputs `delete: ok` + `label: <name>`
- [ ] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals

View File

@@ -0,0 +1,21 @@
---
spec: gitea-axi
blocked-by: 0008-pr-list
---
## What to build
The full-text escape hatch: `search issues <query>` and `search prs <query>`, the destination of the forbidden `--search` redirects.
Both hit Gitea's repo-issues search endpoint with the query, a `type` of issues or pulls, and the owner param; since the endpoint has no repo-name filter, results are filtered client-side to the current repository via each result's repository field, following the client-side filtering policy including its count-line rule.
The positional query is required (`VALIDATION_ERROR` if missing).
Flags: `--state` (default open), `--label` (comma-separated names, API-supported), `--limit` (default 30), `--fields`.
Both commands use the locator schema (`number`, `title`, `state`, `author`, `created`) — search finds the number, `issue view`/`pr view` load the detail — and output blocks `issues:`/`pull_requests:` matching the list commands.
## Acceptance criteria
- [ ] `search issues "<query>"` and `search prs "<query>"` query the search endpoint with the right `type` and owner, then filter to the current repo client-side
- [ ] The count line reports `count: N of T total` with `T` from the client-side-filtered set
- [ ] A missing query yields `VALIDATION_ERROR` (exit 2)
- [ ] `--state`, `--label`, `--limit`, and `--fields` work; default fields are the locator schema
- [ ] Empty results emit the standard `<noun>[0]: (none)` empty state
- [ ] Fixture-server tests cover both types, cross-repo results being filtered out, and the missing-query validation

View File

@@ -0,0 +1,22 @@
---
spec: gitea-axi
blocked-by: 0008-pr-list
---
## What to build
The two-tier dashboard: `gitea-axi` with no arguments, preceded by the `bin:` + `description:` header from axi-sdk-js and followed by next-step suggestions (see ADR 0012).
The short tier fetches up to 3 open issues (`number`, `title`, `state`, `author`) and up to 3 open PRs (`number`, `title`, `author`, `review`) in parallel with `limit=3`, computing `review` via the same parallel review fetch as `pr list`; its `help:` block always hints at `--full`.
The full tier (`--full`) shows open PRs as a TOON table (default fields `number`, `title`, `author`, `labels`, `review`; capped at 20 rows with a standard count line) and open issue counts grouped by label — aggregating all pages of open issues at page size 50 up to a hard cap of 1000, suffixing counts with `+` if capped; each issue contributes to all its labels, with a nonzero-only `unlabeled` row.
Output blocks in both tiers: a `repo:` line, then `prs:` and `issues:`; empty states are the raw strings `prs: 0 open` / `issues: 0 open`.
Issue fetching passes `type=issues`; outside a recognizable Gitea repo the dashboard errors with `REPO_NOT_FOUND` and `-R` + `--login` help — even when invoked by the SessionStart hook (see ADR 0009).
## Acceptance criteria
- [ ] Bare `gitea-axi` renders the header, `repo:` line, up to 3 issues and 3 PRs with the specified fields (including the computed `review`), and a `help:` block hinting at `--full`
- [ ] `gitea-axi --full` renders the PR table capped at 20 rows with `count: 20 of T total` and issue counts grouped by label
- [ ] Label aggregation paginates to the 1000-issue cap, suffixes counts with `+` when capped, counts each issue under all its labels, and shows `unlabeled` only when nonzero
- [ ] Empty states render `prs: 0 open` / `issues: 0 open` as raw strings
- [ ] Issue fetches pass `type=issues` so PRs never appear in the issue block
- [ ] Outside a Gitea repo the dashboard exits with `REPO_NOT_FOUND` and help mentioning `-R` and `--login`
- [ ] Fixture-server tests cover both tiers, the cap-and-suffix behavior, empty states, and the no-repo error

View File

@@ -0,0 +1,21 @@
---
spec: gitea-axi
blocked-by: 0017-dashboard
---
## What to build
Ambient-context distribution: the bundled Agent Skill, the `setup` command, the opt-in `setup hooks`, and the shadowed `update` command (see ADRs 0009 and 0013).
Author the Agent Skill markdown as a minimal pointer, not a command reference: frontmatter description triggering on Gitea issue/PR/label work; a body saying when to use gitea-axi over tea, raw API calls, or git; one-line command-group summaries; and pointers at the bare dashboard and per-command `--help` — the CLI stays the single source of interface truth.
`setup` installs the skill into the user-level skills directory, idempotently reporting installed/updated/unchanged.
`setup hooks` installs a SessionStart hook via axi-sdk-js's `installSessionStartHooks()` for Claude Code, Codex, and OpenCode; the hook runs the bare binary (short dashboard tier) at session start.
`update` shadows the SDK's built-in self-update command, failing with `VALIDATION_ERROR` and a help line pointing at the npm update command, keeping the ten-code error list intact.
There is no postinstall script — skill and hook installation are always explicit user actions.
## Acceptance criteria
- [ ] The Agent Skill markdown is bundled in the package and follows the minimal-pointer shape (trigger description, when-to-use, command-group one-liners, discovery pointers)
- [ ] `gitea-axi setup` installs the skill and outputs `setup: { skill, path, status }`; re-running reports `updated` or `unchanged` rather than failing
- [ ] `gitea-axi setup hooks` registers the SessionStart hook for all three integrations via the SDK and outputs the `hooks:` block with a restart help line; managed entries are updated in place on re-run
- [ ] `gitea-axi update` fails with `VALIDATION_ERROR` (exit 2) and the npm update help line; the SDK's `UPDATE_ERROR` never surfaces
- [ ] Tests cover the setup idempotency states and the update shadow (hook installation verified against a temp home directory)

View File

@@ -0,0 +1,26 @@
---
spec: gitea-axi
blocked-by: 0001-scaffold-and-issue-list-core
---
## What to build
The end-to-end tier of the test suite: a CI workflow that runs the local vitest suite plus end-to-end tests against a live disposable Gitea instance, verifying that fixture recordings remain accurate and the full HTTP pipeline works.
This project's test taxonomy has three tiers (recorded here because the committed spec's two-tier wording is frozen):
- **Unit tests** — functions within a single file, no I/O (e.g. `parseRemoteUrl`, `relativeTime`).
- **Integration tests** — functionality across files, driven at the CLI seam (argv in, stdout/exit-code out) against the fixture server; what the spec's "Two-tier test strategy" calls the "local / unit tier".
- **End-to-end tests** — the same CLI seam against a live disposable Gitea instance; what the spec calls the "CI integration tier". This task builds this tier.
CI runs on Gitea Actions on the operator's instance, with the disposable Gitea as a docker service container pinned to the latest stable image tag (bumped deliberately, not floating).
The end-to-end tests provision what they need on the disposable instance (repo, token, seed issues/PRs) and then exercise the real CLI seam against it.
The workflow file stays GitHub-Actions-compatible so the GitHub mirror can adopt it nearly verbatim later.
## Acceptance criteria
- [ ] A workflow runs on push/PR on Gitea Actions, executing the unit and integration tiers (the local vitest suite) and the end-to-end tier
- [ ] The disposable Gitea runs as a service container pinned to a specific stable image tag
- [ ] End-to-end tests provision their own repo, token, and seed data on the disposable instance, then assert real CLI output and exit codes for at least the tracer command set
- [ ] The workflow uses only syntax that works verbatim (or near-verbatim) on GitHub Actions
- [ ] A fixture-vs-live divergence in a covered response shape fails the end-to-end tier

View File

@@ -0,0 +1,18 @@
---
spec: gitea-axi
blocked-by: 0018-setup-skill-and-hooks
---
## What to build
Publish readiness for the unscoped `gitea-axi` npm package.
Package metadata (name, description, repository, license, engines for Node 20+, ESM), the `gitea-axi` bin entry, and the bundled Agent Skill file included in the published artifact.
No postinstall script — the install delivers the CLI binary only, and skill installation stays behind the explicit `setup` command.
Verify the packed artifact: a global install from the packed tarball yields a working binary whose `setup` finds the bundled skill.
## Acceptance criteria
- [ ] The packed tarball contains the built CLI, the bin entry, and the Agent Skill markdown, and nothing declares a postinstall script
- [ ] A global install from the tarball puts a working `gitea-axi` on the PATH (dashboard header, `--help`, and `setup` all function)
- [ ] Package metadata is complete: unscoped name, description, repository URL, license, Node 20+ engines, ESM module type
- [ ] The publish flow (registry target, access, prepack build) is documented or scripted so publishing is a single command

5
CLAUDE.md Normal file
View File

@@ -0,0 +1,5 @@
# gitea-axi — Agent Instructions
## Commits
Any commit message you write must follow the Conventional Commits specification as documented in [CONVENTIONAL-COMMITS.md](CONVENTIONAL-COMMITS.md).

57
CONVENTIONAL-COMMITS.md Normal file
View File

@@ -0,0 +1,57 @@
# Conventional Commits
> Sourced from [conventionalcommits.org/en/v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)
> by the Conventional Commits authors, licensed under
> [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).
---
## Summary
The Conventional Commits specification is a lightweight convention on top of commit messages.
It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of.
This convention dovetails with [SemVer](http://semver.org), by describing the features, fixes, and breaking changes made in commit messages.
The commit message should be structured as follows:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
The commit contains the following structural elements, to communicate intent to the consumers of your library:
1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with `PATCH` in Semantic Versioning).
2. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with `MINOR` in Semantic Versioning).
3. **BREAKING CHANGE:** a commit that has a footer with a token `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with `MAJOR` in Semantic Versioning). A BREAKING CHANGE can be part of commits of any _type_.
4. _types_ other than `fix:` and `feat:` are allowed, for example `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others.
5. _footers_ other than `BREAKING CHANGE: <description>` may be provided and follow a convention similar to [git trailer format](https://git-scm.com/docs/git-interpret-trailers).
Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE).
A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`.
---
## Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space.
2. The type `feat` MUST be used when a commit adds a new feature to your application or library.
3. The type `fix` MUST be used when a commit represents a bug fix for your application.
4. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):`.
5. A description MUST immediately follow the colon and space after the type/scope prefix. The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_.
6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description.
7. A commit body is free-form and MAY consist of any number of newline separated paragraphs.
8. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a `:<space>` or `<space>#` separator, followed by a string value (this is inspired by the [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)).
9. A footer's token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate the footer section from a multi-paragraph body). An exception is made for `BREAKING CHANGE`, which MAY also be used as a token.
10. A footer's value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed.
11. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer section.
12. If included as a footer, a breaking change MUST consist of the uppercase text `BREAKING CHANGE`, followed by a colon, space, and description, e.g., _BREAKING CHANGE: environment variables now take precedence over config files_.
13. If included in the type/scope prefix, breaking changes MUST be indicated by a `!` immediately before the `:`. If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change.
14. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: correct spelling of CHANGELOG_.
15. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.
16. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer.