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.
_Avoid_: detail formatter, record renderer
**action-block/entity-block pattern**: The uniform convention for mutation output — an action-named block (`created:`, `edited:`, `closed:`, `reopened:`, `merged:`) when the mutation actually ran, an entity-named block (`issue:`, `pull_request:`) when it was an idempotent no-op.
Applies across issue and PR mutations alike; a deliberate departure from gh-axi, which returns entity blocks for issue-side mutation successes.
_Avoid_: status block, result block
**count line**: The leading line in list output that states how many results were returned and their relationship to the total, e.g. `count: N of T total`.
When a client-side filter is active, `T` is the true filtered total computed from the in-memory result set (the `X-Total-Count` header, which reflects the unfiltered total, is ignored); the bare `count: N` form does not exist.
_Avoid_: summary line, header
@@ -93,6 +97,11 @@ Same idempotency rules as [[issue blocks]].
No gh-axi equivalent — Gitea-specific API (`/issues/{index}/dependencies`).
_Avoid_: depends, depends-on, dependencies
**search**: The full-text query commands (`search issues <query>`, `search prs <query>`), repo-scoped via `owner` param plus [[client-side filtering]] by repository (Gitea's `/repos/issues/search` has no repo-name filter).
Results use a locator schema (`number`, `title`, `state`, `author`, `created`) — search finds the number; `issue view` / `pr view` load the detail.
The forbidden `--search` flag on the list commands redirects here.
_Avoid_: query command, find
### Gitea API patterns
**type guard**: The defense against Gitea's unified issue/PR model, where issue endpoints also serve PRs.
@@ -102,7 +111,8 @@ Exception: `issue comment` stays permissive — PRs genuinely share the comment
_Avoid_: PR filtering, issue-only mode
**reviewDecision**: A computed field (not returned by Gitea) that summarizes the overall review state of a PR.
Derived client-side from the reviews list: `APPROVED` if at least one review has `official=true`, `stale=false`, `dismissed=false`, and no non-dismissed `REQUEST_CHANGES` review exists; `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES` exists; otherwise `REVIEW_REQUIRED`.
Derived client-side from the reviews list with an official-first fallback: if any review is `official=true`, only official reviews are considered; otherwise all reviews are (unprotected repos never produce official reviews).
Within the considered set: `CHANGES_REQUESTED` if any non-dismissed `REQUEST_CHANGES`; `APPROVED` if any non-dismissed, non-stale review with state `APPROVED`; otherwise `REVIEW_REQUIRED` (rendered `required` — covers zero-review and comment-only PRs; there is no `none` value).
On `pr list`, this requires one extra parallel HTTP call per PR to fetch reviews.
_Avoid_: review status, review aggregate
@@ -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`.
_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.
Reviewers do *not* use this pattern: `EditPullRequestOption` has no reviewers field; reviewer mutations go through the dedicated `POST`/`DELETE /pulls/{index}/requested_reviewers` endpoints (see ADR 0007 amendment).
_Avoid_: read-modify-write, merge-then-patch
**client-side filtering**: The policy applied when Gitea's API does not support a given filter parameter.
@@ -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.
_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.
`--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

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).
- `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`.
## 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.
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.
## 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.
- 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.
## 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`.
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.
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.
### 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);
`--limit <n>` (default 30);
`--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).
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).
@@ -127,6 +128,7 @@ Extra fields available via `--fields`: `labels`, `assignees`, `milestone`, `body
`--add-assignee <login>`;
`--remove-assignee <login>`;
`--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).
`--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.
@@ -138,10 +140,12 @@ Closing sets `state: "closed"` via PATCH on the issue.
`--reason` is excluded (Gitea has no `state_reason` concept).
When `--comment` is provided, two API calls are made: PATCH to close, then POST to create the comment.
If the PATCH succeeds but the POST fails, the error is surfaced — the issue remains closed but the failure is reported rather than silently swallowed.
Output on success: `closed: { number, status: "ok" }` (action-block/entity-block pattern).
Idempotent: returns early with `message: "Already closed"` if already closed.
**`issue reopen <n>`**
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.
**`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);
`--limit <n>` (default 30);
`--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).
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).
@@ -250,11 +254,13 @@ Output on success: `created: { number, url }` — completing gh-axi's action-blo
`--remove-reviewer <login>`;
`--milestone <name>` (resolved to milestone ID via `GET /milestones?name=<name>` — `VALIDATION_ERROR` if not found);
`--base <branch>`.
Assignee and reviewer mutations use fetch-then-patch (see ADR 0007).
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" }`.
**`pr close <n> [flags]`**
`--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.
Output on success: `closed: { number, status: "ok" }`.
@@ -292,9 +298,10 @@ Truncation limit: 4000 chars.
Output: `pr_diff: { number, diff[, truncated, original_length] }`.
**`pr checkout <n>`**
Fetches the PR head branch name from `GET /pulls/{index}` (`head.ref` field), then runs in the current working directory:
1. `git fetch origin pull/<n>/head:<branch>`
2. `git checkout <branch>`
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. Branch absent: `git fetch origin pull/<n>/head:<branch>`, then `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).
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" }`.
@@ -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).
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`**
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`.
Idempotent: re-running reports already-installed/updated rather than failing.
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-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).
### 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/.../pulls/{index}` | `PR_NOT_FOUND` |
| 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) |
| 429 | any | `RATE_LIMITED` (help: wait and retry, or reduce `--limit`) |
| other | any | `UNKNOWN` |
`TEA_NOT_INSTALLED` is emitted if the tea binary is not found during credential discovery.
`AUTH_REQUIRED` is also emitted when tea is installed but no login matches the detected hostname (help: `` Run `tea login add --url <host>` ``, or pass `--login <name>`).
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.
`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.
@@ -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.
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 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.

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