From d03a0798543f178db17e403a8cdf2b9f3969f67c Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 11 Jul 2026 07:14:39 -0400 Subject: [PATCH] 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 --- .claude/CONTEXT.md | 17 +++++- .../adr/0006-reviewdecision-parallel-fetch.md | 9 +++ ...07-fetch-then-patch-assignees-reviewers.md | 9 +++ .../0011-pr-checkout-via-refs-pull-head.md | 11 ++++ .claude/spec/gitea-axi.md | 46 ++++++++++++--- .../0002-issue-list-filters-and-fields.md | 21 +++++++ .../tasks/0003-issue-view-and-truncation.md | 26 +++++++++ .../tasks/0004-issue-create-and-comment.md | 23 ++++++++ .claude/tasks/0005-issue-edit-close-reopen.md | 24 ++++++++ .claude/tasks/0006-issue-delete-pin-unpin.md | 19 +++++++ .claude/tasks/0007-issue-blocks-blocked-by.md | 20 +++++++ .claude/tasks/0008-pr-list.md | 25 ++++++++ .claude/tasks/0009-pr-view-and-checks.md | 23 ++++++++ .claude/tasks/0010-pr-create-and-comment.md | 23 ++++++++ .claude/tasks/0011-pr-edit-close-reopen.md | 22 +++++++ .../tasks/0012-pr-merge-and-update-branch.md | 23 ++++++++ .claude/tasks/0013-pr-review.md | 19 +++++++ .claude/tasks/0014-pr-diff-and-checkout.md | 23 ++++++++ .claude/tasks/0015-label-commands.md | 22 +++++++ .claude/tasks/0016-search-commands.md | 21 +++++++ .claude/tasks/0017-dashboard.md | 22 +++++++ .claude/tasks/0018-setup-skill-and-hooks.md | 21 +++++++ .claude/tasks/0019-ci-integration-tier.md | 26 +++++++++ .claude/tasks/0020-npm-distribution.md | 18 ++++++ CLAUDE.md | 5 ++ CONVENTIONAL-COMMITS.md | 57 +++++++++++++++++++ 26 files changed, 564 insertions(+), 11 deletions(-) create mode 100644 .claude/tasks/0002-issue-list-filters-and-fields.md create mode 100644 .claude/tasks/0003-issue-view-and-truncation.md create mode 100644 .claude/tasks/0004-issue-create-and-comment.md create mode 100644 .claude/tasks/0005-issue-edit-close-reopen.md create mode 100644 .claude/tasks/0006-issue-delete-pin-unpin.md create mode 100644 .claude/tasks/0007-issue-blocks-blocked-by.md create mode 100644 .claude/tasks/0008-pr-list.md create mode 100644 .claude/tasks/0009-pr-view-and-checks.md create mode 100644 .claude/tasks/0010-pr-create-and-comment.md create mode 100644 .claude/tasks/0011-pr-edit-close-reopen.md create mode 100644 .claude/tasks/0012-pr-merge-and-update-branch.md create mode 100644 .claude/tasks/0013-pr-review.md create mode 100644 .claude/tasks/0014-pr-diff-and-checkout.md create mode 100644 .claude/tasks/0015-label-commands.md create mode 100644 .claude/tasks/0016-search-commands.md create mode 100644 .claude/tasks/0017-dashboard.md create mode 100644 .claude/tasks/0018-setup-skill-and-hooks.md create mode 100644 .claude/tasks/0019-ci-integration-tier.md create mode 100644 .claude/tasks/0020-npm-distribution.md create mode 100644 CLAUDE.md create mode 100644 CONVENTIONAL-COMMITS.md diff --git a/.claude/CONTEXT.md b/.claude/CONTEXT.md index 40b52b2..c2a00ef 100644 --- a/.claude/CONTEXT.md +++ b/.claude/CONTEXT.md @@ -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 `, `search prs `), 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 ` 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 ` 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 ` is a Gitea-specific shortcut flag that bypasses the lookup and passes the ID directly. _Avoid_: label resolution, name-to-ID mapping diff --git a/.claude/adr/0006-reviewdecision-parallel-fetch.md b/.claude/adr/0006-reviewdecision-parallel-fetch.md index 6d557ba..fe193e3 100644 --- a/.claude/adr/0006-reviewdecision-parallel-fetch.md +++ b/.claude/adr/0006-reviewdecision-parallel-fetch.md @@ -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. diff --git a/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md b/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md index fba51fb..5aebe35 100644 --- a/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md +++ b/.claude/adr/0007-fetch-then-patch-assignees-reviewers.md @@ -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. diff --git a/.claude/adr/0011-pr-checkout-via-refs-pull-head.md b/.claude/adr/0011-pr-checkout-via-refs-pull-head.md index 97c6726..3d1abe1 100644 --- a/.claude/adr/0011-pr-checkout-via-refs-pull-head.md +++ b/.claude/adr/0011-pr-checkout-via-refs-pull-head.md @@ -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//head:`) then checkout; the local branch is defined as a mirror of the PR head. +3. Branch currently checked out — fetch `pull//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. diff --git a/.claude/spec/gitea-axi.md b/.claude/spec/gitea-axi.md index 15c9869..6030f05 100644 --- a/.claude/spec/gitea-axi.md +++ b/.claude/spec/gitea-axi.md @@ -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 ` — 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 ` (client-side — Gitea issue list has no sort param; always descending, matching gh-axi); `--limit ` (default 30); `--fields `. -`--search` is explicitly forbidden (VALIDATION_ERROR). +`--search` is explicitly forbidden (VALIDATION_ERROR; help: `` Use `gitea-axi search issues ""` 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 `; `--remove-assignee `; `--milestone ` (resolved to milestone ID via `GET /milestones?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 `** 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 [flags]`** @@ -203,7 +207,7 @@ No gh-axi equivalent. `--sort ` (Gitea-specific extension — maps directly to the API `sort` param); `--limit ` (default 30); `--fields `. -`--search` is explicitly forbidden (VALIDATION_ERROR). +`--search` is explicitly forbidden (VALIDATION_ERROR; help: `` Use `gitea-axi search prs ""` 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 `; `--milestone ` (resolved to milestone ID via `GET /milestones?name=` — `VALIDATION_ERROR` if not found); `--base `. -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 [flags]`** `--comment `. +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 `** -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//head:` -2. `git checkout ` +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//head:`, then `git checkout `. +2. Branch exists, not checked out: `git fetch origin +pull//head:` (force — the branch mirrors the PR head), then `git checkout `. +3. Branch currently checked out: `git fetch origin pull//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: `. Not idempotent: a nonexistent label errors with `VALIDATION_ERROR` rather than reporting success (see ADR 0010). Output: `delete: ok`, `label: `. +#### Search Commands + +**`search issues [flags]`** / **`search prs [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 `` is required (`VALIDATION_ERROR` if missing). +Endpoint: `GET /repos/issues/search` with `q=`, `type=issues` or `type=pulls`, and `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 ` (default open); `--label ` (API-supported — comma-separated names); `--limit ` (default 30); `--fields `. +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 `[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: }`. @@ -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 ` or `pr checkout ` 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 ` ``, or pass `--login `). +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 ` `` 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: `, `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. diff --git a/.claude/tasks/0002-issue-list-filters-and-fields.md b/.claude/tasks/0002-issue-list-filters-and-fields.md new file mode 100644 index 0000000..38c133a --- /dev/null +++ b/.claude/tasks/0002-issue-list-filters-and-fields.md @@ -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 `, 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 ` 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 ` 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 ""` +- [ ] Fixture-server tests cover each filter, client-side sort with pagination, `--fields` extraction, and the forbidden `--search` diff --git a/.claude/tasks/0003-issue-view-and-truncation.md b/.claude/tasks/0003-issue-view-and-truncation.md new file mode 100644 index 0000000..6226856 --- /dev/null +++ b/.claude/tasks/0003-issue-view-and-truncation.md @@ -0,0 +1,26 @@ +--- +spec: gitea-axi +blocked-by: 0001-scaffold-and-issue-list-core +--- + +## What to build + +`issue view ` 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 ` help line. +No `type` field and no sub-issue augmentation. + +## Acceptance criteria + +- [ ] `issue view ` 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 ` 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 diff --git a/.claude/tasks/0004-issue-create-and-comment.md b/.claude/tasks/0004-issue-create-and-comment.md new file mode 100644 index 0000000..6d8aa12 --- /dev/null +++ b/.claude/tasks/0004-issue-create-and-comment.md @@ -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 ` 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 ` 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 --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 diff --git a/.claude/tasks/0005-issue-edit-close-reopen.md b/.claude/tasks/0005-issue-edit-close-reopen.md new file mode 100644 index 0000000..3927db1 --- /dev/null +++ b/.claude/tasks/0005-issue-edit-close-reopen.md @@ -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 ` 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 ` PATCHes `state: "closed"`, with optional `--comment` as a second API call whose failure is surfaced rather than swallowed; `--reason` is excluded. +`issue reopen ` 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 ` 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 ` 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 diff --git a/.claude/tasks/0006-issue-delete-pin-unpin.md b/.claude/tasks/0006-issue-delete-pin-unpin.md new file mode 100644 index 0000000..13d95d5 --- /dev/null +++ b/.claude/tasks/0006-issue-delete-pin-unpin.md @@ -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 ` 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 ` and `issue unpin ` 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 ` outputs `issue: { number, status: "deleted" }` on success +- [ ] Deleting a nonexistent issue yields `ISSUE_NOT_FOUND` (exit 1), not idempotent success +- [ ] `issue pin ` outputs `issue: { number, state, pinned }`; pinning an already-pinned issue returns early with `message: "Already pinned"` and exit 0 +- [ ] `issue unpin ` 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 diff --git a/.claude/tasks/0007-issue-blocks-blocked-by.md b/.claude/tasks/0007-issue-blocks-blocked-by.md new file mode 100644 index 0000000..58ae6d2 --- /dev/null +++ b/.claude/tasks/0007-issue-blocks-blocked-by.md @@ -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 ` and `issue blocked-by ` — 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 ` and `issue blocked-by list ` render their respective output blocks with count lines and explicit empty states +- [ ] `issue blocks add ` outputs `blocks: { issue: n, blocks: target }`; `issue blocked-by add ` 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 diff --git a/.claude/tasks/0008-pr-list.md b/.claude/tasks/0008-pr-list.md new file mode 100644 index 0000000..9b26a16 --- /dev/null +++ b/.claude/tasks/0008-pr-list.md @@ -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 ""` +- [ ] 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 diff --git a/.claude/tasks/0009-pr-view-and-checks.md b/.claude/tasks/0009-pr-view-and-checks.md new file mode 100644 index 0000000..1e081c3 --- /dev/null +++ b/.claude/tasks/0009-pr-view-and-checks.md @@ -0,0 +1,23 @@ +--- +spec: gitea-axi +blocked-by: [0003-issue-view-and-truncation, 0008-pr-list] +--- + +## What to build + +`pr view ` and `pr checks `, 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 ` outputs the same summary line followed by a `checks` list of `{ name, conclusion }`. + +## Acceptance criteria + +- [ ] `pr view ` 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 ` 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 diff --git a/.claude/tasks/0010-pr-create-and-comment.md b/.claude/tasks/0010-pr-create-and-comment.md new file mode 100644 index 0000000..bcf0617 --- /dev/null +++ b/.claude/tasks/0010-pr-create-and-comment.md @@ -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 ` 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 --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 diff --git a/.claude/tasks/0011-pr-edit-close-reopen.md b/.claude/tasks/0011-pr-edit-close-reopen.md new file mode 100644 index 0000000..f34eec1 --- /dev/null +++ b/.claude/tasks/0011-pr-edit-close-reopen.md @@ -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 ` 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 ` 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 ` 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 diff --git a/.claude/tasks/0012-pr-merge-and-update-branch.md b/.claude/tasks/0012-pr-merge-and-update-branch.md new file mode 100644 index 0000000..f355906 --- /dev/null +++ b/.claude/tasks/0012-pr-merge-and-update-branch.md @@ -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 ` 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 ` merges the base branch into the PR head via the update endpoint with `--style ` (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 ` or `pr checkout ` +- [ ] `pr update-branch --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 diff --git a/.claude/tasks/0013-pr-review.md b/.claude/tasks/0013-pr-review.md new file mode 100644 index 0000000..eaf1a2c --- /dev/null +++ b/.claude/tasks/0013-pr-review.md @@ -0,0 +1,19 @@ +--- +spec: gitea-axi +blocked-by: 0001-scaffold-and-issue-list-core +--- + +## What to build + +`pr review ` 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 diff --git a/.claude/tasks/0014-pr-diff-and-checkout.md b/.claude/tasks/0014-pr-diff-and-checkout.md new file mode 100644 index 0000000..d890b3c --- /dev/null +++ b/.claude/tasks/0014-pr-diff-and-checkout.md @@ -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 ` 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 ` 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 ` 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 ` 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) diff --git a/.claude/tasks/0015-label-commands.md b/.claude/tasks/0015-label-commands.md new file mode 100644 index 0000000..6bd15b8 --- /dev/null +++ b/.claude/tasks/0015-label-commands.md @@ -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 ` and `label delete ` 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: ` +- [ ] Creating an existing label (case-insensitive) outputs `create: already_exists` + the existing name, exit 0 +- [ ] `label edit ` 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 ` outputs `delete: ok` + `label: ` +- [ ] Fixture-server tests cover create, idempotent re-create, edit, delete, and the unknown-name refusals diff --git a/.claude/tasks/0016-search-commands.md b/.claude/tasks/0016-search-commands.md new file mode 100644 index 0000000..f127124 --- /dev/null +++ b/.claude/tasks/0016-search-commands.md @@ -0,0 +1,21 @@ +--- +spec: gitea-axi +blocked-by: 0008-pr-list +--- + +## What to build + +The full-text escape hatch: `search issues ` and `search prs `, 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 ""` and `search prs ""` 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 `[0]: (none)` empty state +- [ ] Fixture-server tests cover both types, cross-repo results being filtered out, and the missing-query validation diff --git a/.claude/tasks/0017-dashboard.md b/.claude/tasks/0017-dashboard.md new file mode 100644 index 0000000..0af6bad --- /dev/null +++ b/.claude/tasks/0017-dashboard.md @@ -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 diff --git a/.claude/tasks/0018-setup-skill-and-hooks.md b/.claude/tasks/0018-setup-skill-and-hooks.md new file mode 100644 index 0000000..faac0ae --- /dev/null +++ b/.claude/tasks/0018-setup-skill-and-hooks.md @@ -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) diff --git a/.claude/tasks/0019-ci-integration-tier.md b/.claude/tasks/0019-ci-integration-tier.md new file mode 100644 index 0000000..7feea83 --- /dev/null +++ b/.claude/tasks/0019-ci-integration-tier.md @@ -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 diff --git a/.claude/tasks/0020-npm-distribution.md b/.claude/tasks/0020-npm-distribution.md new file mode 100644 index 0000000..e4c7488 --- /dev/null +++ b/.claude/tasks/0020-npm-distribution.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3646d07 --- /dev/null +++ b/CLAUDE.md @@ -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). diff --git a/CONVENTIONAL-COMMITS.md b/CONVENTIONAL-COMMITS.md new file mode 100644 index 0000000..42cd666 --- /dev/null +++ b/CONVENTIONAL-COMMITS.md @@ -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: + +``` +[optional scope]: + +[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: ` 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 `:` or `#` 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.