[Bugfix #1137] Fix gitea forge preset against the real tea CLI - #1146
[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146pseudoseed wants to merge 6 commits into
Conversation
waleedkadous
left a comment
There was a problem hiding this comment.
Excellent work — thank you for the disciplined re-do, and apologies for the review latency. This is what a model bugfix PR looks like: every tea-CLI deficiency documented in-script with reasoning, the REST passthrough returning exactly the shape forge-contracts.ts expects, and a genuinely well-built regression suite (fake tea on PATH serving captured REST fixtures, the real scripts executed, contract-shape assertions, the comments-as-int crash and null-login team reviewers both covered). We verified every output mapping field-by-field against the contracts — all conform — and the switch from string-interpolated jq to --arg in pr-exists is a quiet security improvement worth crediting.
One substantive question before merge, and two optional polish items:
1. Pagination cap (the one we'd like addressed or answered). Gitea servers cap page size at max_response_items (default 50), so ?limit=200 likely returns 50 items with no client-side pagination in the raw passthrough. That means pr-exists?state=all can false-negative for a branch whose PR isn't in the most recent ~50 (which would block a porch pr_exists gate), and recently-merged (previously --limit 1000) can miss on a busy repo. A pagination loop (page=1..N until a short page) would settle it — or at minimum a comment documenting the server-side cap and the false-negative window, so the next debugger isn't blind. Happy with either; we'd just like the behavior to be chosen rather than inherited.
2. (Polish, optional) With no origin remote or an unusual URL, REPO silently becomes empty/garbage and tea api "repos//…" fails with a confusing 404. An explicit [ -n "$REPO" ] || { echo "…set CODEV_REPO" >&2; exit 1; } naming the remedy would fit this repo's fail-fast convention — ideally factored once since the derivation appears in five scripts.
3. (Polish, optional) A failed comments fetch silently yields comments: [] — indistinguishable from "no comments" for consumers reading issue discussion. A stderr warning on the degraded path would keep the graceful behavior while leaving a trace.
Verdict: approve once item 1 is addressed (fix or documented caveat — your choice). Items 2–3 are welcome in this PR or a follow-up, contributor's choice.
…ast, warn on degraded comments Addresses PR cluesmith#1146 review feedback: 1. Pagination (blocking). Gitea caps list responses at max_response_items (default 50), so the raw `&limit=200` passthrough silently truncated — pr-exists could false-negative a PR beyond the first ~50 (blocking a porch pr_exists gate) and recently-merged could miss on a busy repo. New shared helper `_lib.sh#tea_api_paged` walks page=1..N at limit=50, concatenates the arrays, and stops on a short/empty page with a hard 100-page ceiling. Chosen behavior: paginates, ceiling 100 pages. Wired into pr-exists, pr-list, recently-merged; output shape unchanged (same jq normalizers). 2. REPO derivation, fail-fast + factored. The CODEV_REPO/origin-derivation was duplicated in five scripts. Factored into `_lib.sh#gitea_repo`, sourced by issue-view, pr-exists, pr-list, pr-view, recently-merged. It now validates the result is a clean owner/repo and, if not, prints a stderr message naming CODEV_REPO as the remedy and exits non-zero (was a confusing `repos//…` 404). POSIX sh, $0-relative source; not a forge concept (KNOWN_CONCEPTS allowlist). 3. Degraded comments warn. issue-view still degrades a failed comments fetch to [], but now writes a stderr warning so it's distinguishable from a genuinely uncommented issue. stdout stays pure JSON (parsed by forge.ts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the thorough review — all three items addressed in 1. Pagination — fixed (real page loop, not a comment). You're right that 2. REPO fail-fast, factored once — done. The 3. Degraded comments warn — done. Full suite green in the worktree: 3449 passed | 48 skipped, 0 failures ( |
|
@waleedkadous let me know if there's anything else that needs to be addressed with this one :) |
Verified against a real Forgejo +
|
| concept | result |
|---|---|
user-identity |
FAIL Incorrect Usage: flag provided but not defined: -output |
issue-view |
FAIL jq: Cannot index array with string "html_url" |
pr-list |
FAIL, exit 0 — prints Error: invalid field 'description' and still returns success |
recently-merged |
FAIL, exit 0 — same |
pr-view |
returns a list, not the requested PR |
issue-list, issue-search, pr-exists, recently-closed, auth-status |
OK |
The two exit-0 cases are the nastiest: a caller checking the exit status sees success and gets an error string where JSON should be.
After — this branch, same environment
| concept | result |
|---|---|
user-identity |
OK — user |
issue-view |
OK — object with title, body, state, url, comments[] |
pr-list |
OK — normalized PrListItem[] |
pr-view |
OK — single PR object, correct one |
pr-exists |
OK — true |
recently-merged |
OK — merged-only, correct merged_at ordering |
All six previously-broken concepts now work. No regressions in the five that already worked.
Two notes
The comments-as-int catch is real and would have bitten immediately. Gitea returns comments as an integer count on the issue object; our issue-view on the released version failed exactly there. The second call for the comments array is necessary, not defensive.
One nearly-false report from me, worth stating so nobody repeats it. My first run of this branch's issue-view failed with Cannot index array with string "title". That was my harness, not your code — I had exported CODEV_ISSUE_NUMBER where the contract is CODEV_ISSUE_ID, so the path resolved to the issue list endpoint. With the correct variable it works. Flagging it because the failure mode is plausible-looking and someone else testing this could draw the wrong conclusion.
Unrelated gap this surfaced
pr-create is not a forge concept at all, so gh pr create stays hardcoded in the skeleton prompts (porch/prompts/pr.md, protocols/{air,spir,pir,bugfix,maintain}/…). That means a Gitea/Forgejo user still needs a gh shim on PATH no matter how complete this preset becomes. Not this PR's problem — filing separately — but relevant if anyone assumes a working gitea preset makes gh unnecessary.
Happy to re-run against any further revisions.
The gitea preset invoked `tea <entity> list/view/whoami/comment`, whose
flattened `--fields` output (or missing flags/subcommands) doesn't match the
Gitea REST shape that forge-contracts.ts and the jq normalizers assume. Route
the read concepts through `tea api`, the raw REST passthrough that returns
exactly that shape:
- user-identity: `tea api user | jq .login` (`tea whoami` has no --output json)
- pr-view: `tea api repos/<repo>/pulls/N` → PrViewResult
- pr-list: `tea api repos/<repo>/pulls?state=open` → PrListItem[]
(now also populates real reviewRequests/isDraft/body)
- pr-exists: `tea api repos/<repo>/pulls?state=all` with nested .head.ref/.merged
- issue-view: `tea api repos/<repo>/issues/N` + a second call for the comments
ARRAY (Gitea's issue object reports `comments` as an int count,
which would crash consumers' `.comments.filter(...)`)
- recently-merged: `tea api repos/<repo>/pulls?state=closed`, filter .merged,
using the real .merged_at
- issue-comment: `tea comments add` (`tea issues` has no `comment` subcommand)
`tea api` needs an explicit owner/repo path segment (unlike `tea <entity>`,
which auto-detects it from the local git remote), and most concepts are invoked
without CODEV_REPO set, so each api-based script derives owner/repo from the
origin remote, honoring CODEV_REPO when present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stubs a fake `tea` on PATH answering `api <endpoint>` with captured Gitea REST fixtures (tea isn't in CI, per cluesmith#920), points the scripts at a throwaway repo with a gitea remote, runs each real script, and asserts the normalized output conforms to forge-contracts.ts — incl. comments-as-array, merged-only filtering, open/merged/closed pr-exists cases, and CODEV_REPO override. Also updates the cluesmith#568 pr-exists assertion for gitea to match the new `state=all` query param (was `--state all` flag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ast, warn on degraded comments Addresses PR cluesmith#1146 review feedback: 1. Pagination (blocking). Gitea caps list responses at max_response_items (default 50), so the raw `&limit=200` passthrough silently truncated — pr-exists could false-negative a PR beyond the first ~50 (blocking a porch pr_exists gate) and recently-merged could miss on a busy repo. New shared helper `_lib.sh#tea_api_paged` walks page=1..N at limit=50, concatenates the arrays, and stops on a short/empty page with a hard 100-page ceiling. Chosen behavior: paginates, ceiling 100 pages. Wired into pr-exists, pr-list, recently-merged; output shape unchanged (same jq normalizers). 2. REPO derivation, fail-fast + factored. The CODEV_REPO/origin-derivation was duplicated in five scripts. Factored into `_lib.sh#gitea_repo`, sourced by issue-view, pr-exists, pr-list, pr-view, recently-merged. It now validates the result is a clean owner/repo and, if not, prints a stderr message naming CODEV_REPO as the remedy and exits non-zero (was a confusing `repos//…` 404). POSIX sh, $0-relative source; not a forge concept (KNOWN_CONCEPTS allowlist). 3. Degraded comments warn. issue-view still degrades a failed comments fetch to [], but now writes a stderr warning so it's distinguishable from a genuinely uncommented issue. stdout stays pure JSON (parsed by forge.ts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3c2e3c2 to
86b82ac
Compare
…luesmith#1458 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rop the lookup The gitea `pr-create` ran `tea pulls create` (whose output is a rendered, ANSI-decorated view, not parseable) and then searched for the PR it had just made with `tea pulls list --limit 200`. That search was built on a disproven assumption. cluesmith#1146 established, and this change re-confirmed against live Forgejo 15.0.2, that Gitea caps every list response at the server's `max_response_items` — default 50. `settings/api` reports 50, and a `?limit=200` request returns exactly 50 items where paging at 50 returns 53. So `--limit 200` silently truncates: on a busy repo the just-created PR falls off the first page, and pr-create reported created the PR but could not find an open pull for head '<branch>' and exited 1 for a PR that exists — inviting a duplicate retry at the single most important write in the protocol. Rather than paginate the lookup, remove it. `tea api -X POST repos/{owner}/{repo}/pulls` RETURNS the created PR — `number` and `html_url` — in its response body, so there is nothing to search, nothing to race, and nothing to truncate. It also drops the `<user>:<branch>` head-matching heuristic: the API resolves an owner-qualified head itself. Live verification against tea 0.14.2 + Forgejo 15.0.2 turned up three defects in the obvious version of that change. Each is the same bug class as cluesmith#1455 itself — an operation accepted and then silently not performed — so each is handled in code, not left as a caveat. 1. `tea api` EXITS 0 on HTTP errors, printing the error body. Since the whole change replaces a lookup with a single call, trusting that exit code would reintroduce cluesmith#1455's silent success inside the fix for it: a 404 or 422 would be reported as a created PR. The response is therefore asserted to BE a PR object — an object carrying a numeric `number` AND a non-empty browser URL — and anything else fails loudly with the response body. Pinned by tests that feed an error object, an array, a string-typed `number`, a numberless object, `null` and an empty body, all at exit 0. The one case where `number` is present but the URL is not gets its own message: the PR WAS created, so it names the number and says not to retry. Reading that as "nothing happened" is how duplicates get opened. 2. `base` is REQUIRED by the API — it answers `[Base]: Required` — where `tea pulls create` defaulted it client-side. Silently posting against the wrong base would be worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly, and fails with a clear message if that cannot be resolved. 3. `draft: true` in the payload is SILENTLY IGNORED (the response comes back `draft: false`), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea marks a draft by a `WIP:` title prefix — exactly what `tea pulls create --draft` does — so that is now implemented, and verified server-side to produce `draft: true`. Also verified live: `{owner}`/`{repo}` are substituted by tea from the repo context, with `--repo owner/name` supplying it when the cwd has no Gitea remote (checked with https and scp-style remotes, and from a GitHub-remote cwd); `url` on the create response is the browser page, so `.html_url // .url` lands the right one in the contract; and the body round-trips byte-identically, being built with `jq --arg` and fed on stdin (`-d @-`) rather than surviving an argv round-trip. The unresolvable-repo case used to surface as a bare `404 page not found`; it now names CODEV_PR_REPO as the remedy, matching the fail-fast ergonomics of `_lib.sh#gitea_repo` in cluesmith#1146 without taking a dependency on that PR — this change stands alone and the two can merge in either order. Tests: the gitea half of the concept suite is rewritten against a `tea api` stub. Every new case fails against the previous script and passes against this one, including an explicit assertion that no `pulls`/`list`/`--limit` call is made at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…luesmith#1458 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…luesmith#1458 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
86b82ac to
fa5a7cb
Compare
…luesmith#1458 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #1137.
Bugfix-protocol re-do of the earlier SPIR-style PR #1138 (now closed), per maintainer request. Same root cause, plus a real regression test.
Problem
The
giteaforge preset was authored against the Gitea REST API JSON shape, but the scripts invoke theteaCLI, whose output shape differs — and several concepts referenced flags/fields/subcommandsteadoesn't have. Per the in-repo#920note,teawasn't available in the authoring environment, so the preset was never run end-to-end.Fix
Route the read concepts through
tea api(raw REST passthrough returning the shapeforge-contracts.ts+ the jq normalizers expect):tea api user | jq .login(tea whoamihas no--output json)tea api repos/<repo>/pulls/N→PrViewResult(incl. additions/deletions)tea api repos/<repo>/pulls?state=open→PrListItem[]tea api repos/<repo>/pulls?state=allwith nested.head.ref/.mergedtea api repos/<repo>/issues/N+ a second call for the comments array (Gitea reportscommentsas an int count, which would crash.comments.filter(...))tea api repos/<repo>/pulls?state=closed, filter.merged, using real.merged_attea comments add(tea issueshas nocommentsubcommand)tea apineeds an explicit owner/repo path segment, so each api-based script derives owner/repo from the origin remote (honoringCODEV_REPOwhen set).Testing
teaon PATH answeringapi <endpoint>with captured Gitea REST fixtures (tea isn't in CI, per vscode: editor-tab webview for rich backlog search #920), runs each real script, and asserts the normalized output conforms toforge-contracts.ts— incl. comments-as-array, merged-only filtering, open/merged/closedpr-exists, andCODEV_REPOoverride.pr-existsassertion for the newstate=allquery param.🤖 Generated with Claude Code
Rebased onto
main(2026-08-14)This branch was 2063 commits behind and
mergeable=CONFLICTING. Rebased ontoupstream/main; force-pushed to the fork. All 5 original commits preserved.Exactly one file conflicted, twice —
scripts/forge/gitea/pr-view.sh.pr-viewnow emitsurlWhile this branch sat, PIR #1179 landed on
mainand gave giteapr-viewaurlfield mapped from Gitea'shtml_url:This PR rewrites that same script onto
tea apiwith an explicit normalizer — which emitted nourlat all. Taking either side of the conflict wholesale loses something: take ours and #1179 is silently reverted, take theirs and thetea apifix is lost.Resolution: both. The script keeps this PR's
tea apirouting and re-addsurl: (.html_url // .url).So, stated plainly rather than left in the diff: gitea
pr-viewnow returns aurlfield that the pre-rebase branch did not return. That is a restoration ofmain's behaviour, not a new invention —forge-contracts.tsdocuments the Gitea mapping by name ("Giteahtml_url— Gitea'surlis the API endpoint, do not use it") — but it is a real change to this concept's output versus what this PR previously proposed, so it should not be discovered from the diff.bugfix-1137-gitea-tea-api.test.tswas updated accordingly: thepulls/42fixture now carries bothhtml_urlandurl, and the assertion pins that the browser page, not the API endpoint, is what reaches the contract.Two smaller deliberate deviations
_lib.shis committed100755, not100644.scripts/postinstall.mjschmods everyscripts/forge/**/*.shto 755 unconditionally, so 644 is a mode that never survives an install and leaves a permanently dirty worktree for anyone who runspnpm install. The file is sourced, not executed; the bit is inert.Relationship to #1458
#1458 (
pr-createas a forge concept) landed while this PR was open, and its giteapr-create.shlooked the new PR up withtea pulls list --limit 200— the exact call this PR proves silently truncates. That has been fixed on #1458's branch, not here: it now creates viatea api -X POST …/pulls, which returns the created PR directly, so the lookup is gone rather than paginated.Re-confirmed live against Forgejo 15.0.2 while doing so:
settings/apireportsmax_response_items: 50, and a?limit=200request returns exactly 50 items on a list where paging at 50 returns 53. The premise behind this PR's pagination work holds.Merge-order implications are spelled out in full at the end of this description.
Verification
bugfix-1137-gitea-tea-api,bugfix-568-pr-exists-state-all,forge,bugfix-693-forge-exec-bit).@cluesmith/codevunit suite, rebased tip: 3193 passed, 126 failed (67 files).upstream/mainin the same worktree: 3176 passed, 126 failed (67 files) — the same 67 files and the same 126 tests.agent-farm/terminal/consolidate(attach, session-manager, shellper sockets, SQLite state), environment-dependent — this worktree has no builtdist/, which those tests spawn from, and a live Tower is running against the same state. None is in a file this PR touches, and every forge suite passes.Merge order with the sibling PR — verified, not assumed
#1146 and #1458 come from the same fork and both touch
packages/codev/scripts/forge/gitea/, so the ordering question is fair. The answer:Either order is safe. There is no dependency and no conflict.
git merge-treeon the two branch tips merges cleanly. The only file both touch is the builder thread log, which is the identical blob on both branches and auto-merges.pr-create.shdoes notsource _lib.shand does not callgitea_repoortea_api_paged. Nothing in it resolves against #1146._lib.shandpr-view.share byte-identical to #1146's versions andpr-create.shbyte-identical to #1458's — no silent blending. Thebugfix-693invariant (every entry under each provider dir is a*.sh) still holds with_lib.shpresent.Does #1458 duplicate something #1146 makes shared?
The paginator: no, and it shouldn't.
_lib.sh#tea_api_pagedexists to walk a truncating list endpoint.pr-createno longer lists anything — it reads the new PR out of the create response — so there is no pagination for it to share. That is the point of the reconcile rather than an oversight.Repo resolution: yes, there are two paths, and this is worth a follow-up.
. _lib.sh→gitea_repo(), which honoursCODEV_REPO, else derivesowner/repofrom the origin remote, and fails fast namingCODEV_REPOas the remedy.pr-create: tea's own{owner}/{repo}placeholders, withCODEV_PR_REPOforwarded astea --repo.They were kept separate deliberately, for two reasons rather than by omission:
pr-createtakesCODEV_PR_REPO; the read concepts takeCODEV_REPO.gitea_repo()reads the latter and takes no argument, sopr-createcould not call it without changing its signature — which would mean editing a [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 file from [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 and creating exactly the merge-order coupling this avoids.--repodoes more than fill a path. It also supplies tea's repo/login context, verified working from a cwd whose remote is not a Gitea host. A path-only helper does not do that.Recommended follow-up (not done here, deliberately): once both PRs have landed, unify the two behind one helper that takes the override variable as a parameter — e.g.
gitea_repo "$CODEV_PR_REPO"— so there is one repo-resolution path with one error message. Doing it now would couple two independent PRs; doing it never leaves two paths that will drift. It is a small, mechanical change against a tree where both are already present.The one ergonomic gap that split created has been closed in the meantime: an unresolvable repo used to surface from
pr-createas a bare404 page not found, and now namesCODEV_PR_REPOas the remedy, matchinggitea_repo()'s fail-fast message.