Skip to content

[Bugfix #1137] Fix gitea forge preset against the real tea CLI - #1146

Open
pseudoseed wants to merge 6 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Open

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
pseudoseed wants to merge 6 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseed pseudoseed commented Jul 6, 2026

Copy link
Copy Markdown

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 gitea forge preset was authored against the Gitea REST API JSON shape, but the scripts invoke the tea CLI, whose output shape differs — and several concepts referenced flags/fields/subcommands tea doesn't have. Per the in-repo #920 note, tea wasn'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 shape forge-contracts.ts + the jq normalizers expect):

  • user-identity: tea api user | jq .login (tea whoami has no --output json)
  • pr-view: tea api repos/<repo>/pulls/NPrViewResult (incl. additions/deletions)
  • pr-list: tea api repos/<repo>/pulls?state=openPrListItem[]
  • 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 reports comments as an int count, which would crash .comments.filter(...))
  • recently-merged: tea api repos/<repo>/pulls?state=closed, filter .merged, using real .merged_at
  • issue-comment: tea comments add (tea issues has no comment subcommand)

tea api needs an explicit owner/repo path segment, so each api-based script derives owner/repo from the origin remote (honoring CODEV_REPO when set).

Testing

🤖 Generated with Claude Code


Rebased onto main (2026-08-14)

This branch was 2063 commits behind and mergeable=CONFLICTING. Rebased onto upstream/main; force-pushed to the fork. All 5 original commits preserved.

Exactly one file conflicted, twice — scripts/forge/gitea/pr-view.sh.

⚠️ Behaviour change from the conflict resolution: pr-view now emits url

While this branch sat, PIR #1179 landed on main and gave gitea pr-view a url field mapped from Gitea's html_url:

tea pulls view "$CODEV_PR_NUMBER" --output json | jq '.url = (.html_url // .url)'

This PR rewrites that same script onto tea api with an explicit normalizer — which emitted no url at all. Taking either side of the conflict wholesale loses something: take ours and #1179 is silently reverted, take theirs and the tea api fix is lost.

Resolution: both. The script keeps this PR's tea api routing and re-adds url: (.html_url // .url).

So, stated plainly rather than left in the diff: gitea pr-view now returns a url field that the pre-rebase branch did not return. That is a restoration of main's behaviour, not a new invention — forge-contracts.ts documents the Gitea mapping by name ("Gitea html_url — Gitea's url is 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.ts was updated accordingly: the pulls/42 fixture now carries both html_url and url, and the assertion pins that the browser page, not the API endpoint, is what reaches the contract.

Two smaller deliberate deviations

  • _lib.sh is committed 100755, not 100644. scripts/postinstall.mjs chmods every scripts/forge/**/*.sh to 755 unconditionally, so 644 is a mode that never survives an install and leaves a permanently dirty worktree for anyone who runs pnpm install. The file is sourced, not executed; the bit is inert.
  • The test-fixture update was applied inside the test commit (via an interactive rebase stop) rather than as a trailing fixup, so every commit is green in isolation — verified by checking out and testing all 5 individually.

Relationship to #1458

#1458 (pr-create as a forge concept) landed while this PR was open, and its gitea pr-create.sh looked the new PR up with tea 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 via tea 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/api reports max_response_items: 50, and a ?limit=200 request 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

  • All 5 commits pass the forge suites individually (bugfix-1137-gitea-tea-api, bugfix-568-pr-exists-state-all, forge, bugfix-693-forge-exec-bit).
  • Full @cluesmith/codev unit suite, rebased tip: 3193 passed, 126 failed (67 files).
  • Baseline run of the same suite on unmodified upstream/main in the same worktree: 3176 passed, 126 failed (67 files) — the same 67 files and the same 126 tests.
  • So: zero regressions; this branch adds 17 passing tests and breaks nothing. The pre-existing failures are all agent-farm / terminal / consolidate (attach, session-manager, shellper sockets, SQLite state), environment-dependent — this worktree has no built dist/, 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.

Question Answer How it was checked
Do they conflict? No. git merge-tree on 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.
Must one merge first? No. #1458's pr-create.sh does not source _lib.sh and does not call gitea_repo or tea_api_paged. Nothing in it resolves against #1146.
Does the merged result hold together? Yes. In the merged tree, _lib.sh and pr-view.sh are byte-identical to #1146's versions and pr-create.sh byte-identical to #1458's — no silent blending. The bugfix-693 invariant (every entry under each provider dir is a *.sh) still holds with _lib.sh present.

Does #1458 duplicate something #1146 makes shared?

The paginator: no, and it shouldn't. _lib.sh#tea_api_paged exists to walk a truncating list endpoint. pr-create no 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.

They were kept separate deliberately, for two reasons rather than by omission:

  1. Different contract. pr-create takes CODEV_PR_REPO; the read concepts take CODEV_REPO. gitea_repo() reads the latter and takes no argument, so pr-create could 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.
  2. --repo does 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-create as a bare 404 page not found, and now names CODEV_PR_REPO as the remedy, matching gitea_repo()'s fail-fast message.

@waleedkadous waleedkadous left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 5, 2026
…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>
@pseudoseed

Copy link
Copy Markdown
Author

Thanks for the thorough review — all three items addressed in 3c2e3c2b.

1. Pagination — fixed (real page loop, not a comment). You're right that ?limit=200 inherited Gitea's max_response_items cap (default 50) and silently truncated. Added a shared tea_api_paged helper in a new scripts/forge/gitea/_lib.sh that requests page=1,2,3… at an explicit limit=50, concatenates each page's array (jq -s add), and stops when a page comes back shorter than the limit (or empty). Chosen behavior: it paginates, with a hard ceiling of 100 pages (100 × 50 = 5000 items) so a misbehaving server can't spin forever — documented in the helper. Wired into all three list reads: pr-exists (state=all), pr-list (state=open), recently-merged (state=closed). Output shape is unchanged — the concatenated array feeds the existing jq normalizers untouched. New tests serve a full 50-item page 1 + a short page 2 and assert an item that exists only on page 2 is found by each of the three scripts (pr-exists returns true for it, pr-list/recently-merged include it).

2. REPO fail-fast, factored once — done. The CODEV_REPO/origin-derivation line (duplicated in five scripts) now lives in _lib.sh#gitea_repo, sourced via . "$(dirname "$0")/_lib.sh" by issue-view, pr-exists, pr-list, pr-view, and recently-merged. It validates the result is a clean owner/repo; if not (no origin, unusual URL), it prints set CODEV_REPO=owner/repo to stderr and exits non-zero instead of letting tea api "repos//…" 404. Verified before factoring: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist (a leading-underscore file is never registered as a concept), package.json files ships scripts/forge so _lib.sh is packaged, it's POSIX sh (no bashisms), and $0-relative sourcing works when the script is invoked by absolute path (how forge runs it via sh -c). Tests cover missing-origin and garbage-URL → non-zero exit + the stderr remedy.

3. Degraded comments warn — done. issue-view still degrades a failed comments fetch to [], but now writes gitea forge: comments fetch failed for issue N; reporting 0 comments to stderr while stdout stays pure JSON. Test asserts both (comments: [] on stdout and the stderr warning).

Full suite green in the worktree: 3449 passed | 48 skipped, 0 failures (pnpm build + pnpm test). The #568 pr-exists state=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
Author

@waleedkadous let me know if there's anything else that needs to be addressed with this one :)

@pseudoseed

pseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Author

Verified against a real Forgejo + tea — all six concepts pass

The PR notes that tea wasn't available in the authoring environment and the preset was never run end to end. It has now been. I ran this branch's scripts against a live Forgejo instance with tea 0.14.2, on a repo with ~355 issues and ~350 PRs.

Environment: Forgejo, tea 0.14.2 (go-sdk v1.1.0), single configured login, scripts invoked directly with CODEV_REPO set.

Before — released version, same environment

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.

pseudoseed and others added 5 commits August 14, 2026 07:46
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>
@pseudoseed
pseudoseed force-pushed the builder/bugfix-1137 branch from 3c2e3c2 to 86b82ac Compare August 14, 2026 14:16
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 14, 2026
…luesmith#1458

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 14, 2026
…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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 14, 2026
…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>
@pseudoseed
pseudoseed force-pushed the builder/bugfix-1137 branch from 86b82ac to fa5a7cb Compare August 14, 2026 14:27
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 14, 2026
…luesmith#1458

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gitea forge preset is broken against the real tea CLI (0.14.2)

2 participants