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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137
Sep 4, 2026
Merged

[Bugfix #1137] Fix gitea forge preset against the real tea CLI#1146
waleedkadous merged 16 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1137

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.

QuestionAnswerHow 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.

@waleedkadouswaleedkadous 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
ContributorAuthor

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.jsonfiles 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 #568pr-existsstate=all assertion stays green (the helper is still called with state=all).

@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

@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
ContributorAuthor

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

conceptresult
user-identityFAILIncorrect Usage: flag provided but not defined: -output
issue-viewFAILjq: Cannot index array with string "html_url"
pr-listFAIL, exit 0 — prints Error: invalid field 'description' and still returns success
recently-mergedFAIL, exit 0 — same
pr-viewreturns a list, not the requested PR
issue-list, issue-search, pr-exists, recently-closed, auth-statusOK

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

conceptresult
user-identityOK — user
issue-viewOK — object with title, body, state, url, comments[]
pr-listOK — normalized PrListItem[]
pr-viewOK — single PR object, correct one
pr-existsOK — true
recently-mergedOK — 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.

pseudoseedand 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 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 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>
@amrmelsayed

Copy link
Copy Markdown
Collaborator

Reviewer Integration Review (CMAP-3)

Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every finding below was re-verified by the reviewing architect against the actual source, and the tea-CLI claims against the installed release binary, before being credited.

Verdict: REQUEST CHANGES (light). The core fix is correct, honestly documented, and the best-evidenced community PR this repo has received: the tea api rerouting, the pagination premise, the #1179 conflict resolution, and the merge-order analysis vs #1458 all check out. Lane split: Gemini APPROVE; Codex and Claude REQUEST_CHANGES. Two items are blocking; the rest are tiered below so nothing has to be rediscovered later.

Verified solid (no action)

  • _lib.sh is safely inert as a concept: forge.ts builds presets from the explicit KNOWN_CONCEPTS allowlist, so a leading-underscore file is never registered. Verified at forge.ts:64.
  • $(dirname "$0") sourcing holds in the real call path: executeForgeCommand runs the absolute script path via sh -c.
  • package.jsonfiles ships scripts/forge, and the bugfix-693 invariant (every provider-dir entry matches *.sh) still holds with _lib.sh present. The 755-mode argument is correct (postinstall chmods unconditionally).
  • All four emitted shapes conform exactly to forge-contracts.ts (PrViewResult, PrListItem, MergedPrItem, IssueViewResult), and the --arg branch switch in pr-exists is a real injection fix.
  • Pagination premise re-confirmed: issue gitea forge preset is broken against the real tea CLI (0.14.2) #1137 is still open, no forge-script drift on main since your 08-14 rebase, branch is MERGEABLE.

Blocking

1. issue-comment.sh breaks on the current released tea (0.14.1).tea comments add only exists from tea 0.14.2; on 0.14.1 (current Homebrew release) it exits with No help topic for 'comments'. Verified against the installed 0.14.1 binary and the tea source at both tags: tea comment "$CODEV_ISSUE_ID" "$CODEV_COMMENT_BODY" exists on 0.14.1 and is explicitly preserved as a shorthand in 0.14.2+, so it works on every version. This slipped through because issue-comment is the one concept absent from the live-verification tables (that run used tea 0.14.2), and the fake-tea stub implements comments add by fiat, pinning the assumption rather than testing it. One-word fix plus updating the stub to answer comment instead.

2. Paginator failure exits 0 with empty stdout, which reads as a false-negative pr_exists gate.tea_api_paged's return 1 sits on the left of a pipe in POSIX sh (no pipefail), so the script's exit status is jq's (0) and stdout is empty. Porch's pr-exists check (checks.ts:365) maps anything that isn't "true" to passed: false, so a transient failure mid-walk reports "no PR exists": the exact failure mode pagination was added to prevent. Fix in all three list scripts by capturing first:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

This also converts the jq -s add raw-type-error wart you flagged in the thread log into a clean non-zero exit.

Strongly recommended in this PR (both small, both introduced here)

3. codev doctor regression for gitea users.extractExecutable (forge.ts:192) skips comments, if, and assignments, but not dot-sourcing: the first substantive token in the five sourced scripts is ., and which . fails, so doctor shows five false "not found" rows and stops verifying tea for those concepts. Note the fix is not just skipping .: after the source line and the REPO= assignment are skipped, the next token in the three list scripts is tea_api_paged (a shell function), which also fails the which check. The extraction needs to resolve through sourced-helper calls (or the gitea preset needs declared executables), plus a test; no existing test covers this path, which is why the suite stayed green.

4. The stop condition trusts that the server honored limit=50.max_response_items is admin-tunable; on a server capped below 50, every page is "short", the loop breaks after page 1, and silent truncation returns on exactly the servers that tuned the cap. Derive the effective page size from page 1's item count and break when a page is shorter than that (or stop only on an empty page, at the cost of one extra request per walk). Worth a test with a sub-50 cap.

Maintainer's call: in-PR or filed as follow-ups

  • recently-merged ignores CODEV_SINCE_DATE and now walks the full closed-PR history (up to 100 sequential requests, 2 jq spawns per page) inside forge's 30s timeout (forge.ts:323). On a large Gitea repo that risks timeout, and a timeout yields null: a worse dashboard outcome than truncation was. An early-exit on the since-date or a tighter page bound for this concept would cover it.
  • Non-paged reads don't validate tea api's exit-0-on-error responses. An HTTP error body piped into the pr-view/issue-view/user-identity normalizers emits a structurally valid all-null contract object at exit 0. [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 guards this exact mode explicitly, so the two halves of the preset currently disagree about a documented behavior. Same class: issue-view's comments degrade path only handles a blank response, not an error-object one (--argjson then fails with a raw jq error instead of the warned [] degrade).

Follow-up issues to file after merge (none blocking)

  • The preset is now two idioms: issue-list / issue-search / recently-closed still use tea pulls|issues list --fields … --limit 200/1000 with the same truncation premise this PR disproves. Migrate them onto tea api + tea_api_paged.
  • Repo-resolution unification (already proposed in the PR body) should absorb a third path: repo-archive.sh uses bare ${CODEV_REPO} with no fail-fast.
  • forge-contracts.ts doc drift: the reviewRequests comment still says "GitLab/Gitea emit []", but gitea now populates real reviewer logins.
  • Test gaps: no 3+ page walk, no mid-walk failure, no sub-50 server cap.
  • _lib.sh creates a sibling-file dependency for hand-copied .codev/scripts/forge/gitea/ overrides; worth a line in the header.

Process notes

Merge authority rests with the maintainer; this review is the reviewing architect's recommendation, not a gate approval.

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Follow-up from the reviewing architect: finding 3 just got smaller

Merge order has been ruled by the maintainer: #1458 first, then this PR.

That changes the shape of one item in the review above. #1458 ships a # forge-executable: <tool> declaration that extractExecutable honours ahead of its first-substantive-line heuristic. With #1458 merged first, the "strongly recommended" finding 3 (the codev doctor regression) collapses to adding one line to each of the five sourced scripts:

#!/bin/sh# Forge concept: pr-exists (Gitea via tea CLI)# forge-executable: tea

The deeper extraction work the review asked for (resolving through sourced-helper calls, plus a test) is not required of you — that burden moved to #1458's mechanism, and a hardening addition to its builtin skip list is being pressed on that PR separately.

Everything else stands as written, and none of it waits on #1458: the two blockers (tea comment on 0.14.1, and capture-first in the three paginated scripts so a mid-walk failure can't read as a false-negative pr_exists gate) can be fixed now in either order. Finding 4 (the sub-50 max_response_items stop condition) is likewise independent.

…ktree
# Conflicts:
#	codev/projects/bugfix-1137-gitea-forge-preset-is-broken-a/status.yaml
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
…n PR cluesmith#1458
1. linear preset now explicitly disables pr-create instead of silently
falling through to the github default (`gh pr create`) — the same
silent-fallthrough bug class cluesmith#1455 closes, just found by the
integration reviewer in a different preset.
2. Add `.` and `source` to extractExecutable's SHELL_BUILTINS, so a
script that opens with `. "$(dirname "$0")/_lib.sh"` (the shape
sibling PR cluesmith#1146's read scripts use) isn't misreported by `codev
doctor` as needing an executable literally named `.`.
3. gitea/pr-create.sh's default-branch resolution named CODEV_PR_BASE
as the remedy even when the real failure was an unresolvable repo
(GET 404) — the POST path already named CODEV_PR_REPO correctly for
the same root cause; the GET path now matches it.
Reviewer: amrmelsayed (CMAP integration review, 2026-08-17). Verdict:
APPROVE with these three pre-merge recommendations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseedand others added 2 commits August 20, 2026 19:41
… cmd, masked pipe failures, sub-limit page cap
PR cluesmith#1146 review (2026-08-17, amrmelsayed) — REQUEST_CHANGES, 2 blocking + 1
strongly-recommended item:
1. (blocking) issue-comment.sh called `tea comments add`, which only exists on
tea 0.14.2+ and fails on the still-current 0.14.1 release ("No help topic
for comments"). Switch to the `tea comment <id> <body>` shorthand, which
works on both 0.14.1 and 0.14.2+.
2. (blocking) pr-exists.sh, pr-list.sh, recently-merged.sh piped
`tea_api_paged | jq` directly. POSIX sh has no pipefail, so a mid-walk
pagination failure (tea_api_paged returns 1) was masked by jq's exit
status (0 on empty stdin) — pr-exists in particular would report "false"
for a real error instead of failing, silently passing a porch pr_exists
gate. Capture the paginator's output into a variable and check its exit
status before piping to jq.
3. (strongly recommended) tea_api_paged's stop condition compared each page's
item count against the *requested* limit (50). A server whose
max_response_items is tuned below that requested limit truncates every
page — including non-last pages — to its own cap, so every page looked
"short" and the loop broke after page 1. Compare against the size actually
observed on page 1 instead.
Item 4 (a `# forge-executable: tea` header convention) depends on cluesmith#1458's
extractExecutable convention landing first, which hasn't happened — left for
a follow-up once cluesmith#1458 merges, per the reviewer's stated merge order.
Regression tests added for all three: a 0.14.1-compatible `tea comment` stub,
a mid-walk pagination failure fixture exercised by all three paginated
scripts, and a sub-50-per-page server-cap fixture across 3 pages proving
pr-list keeps walking. Full local suite: 3197 passed, 126 failed (same 67
pre-existing environment-dependent files as the unmodified baseline) — zero
regressions, +4 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
ContributorAuthor

Review follow-up (2026-08-20)

Addressed both blocking items and the strongly-recommended item from the 2026-08-17 review, pushed as c9f55a537 (+ 481956e12 for the builder thread log):

1. (blocking) issue-comment.shtea comments add doesn't exist on 0.14.1
tea comments add is a 0.14.2+-only subcommand and fails on the still-current 0.14.1 release ("No help topic for comments"). Switched to the top-level tea comment <id> <body> shorthand, which works on both 0.14.1 and 0.14.2+.

2. (blocking) masked pipe failures in the paginated scripts
pr-exists.sh, pr-list.sh, and recently-merged.sh piped tea_api_paged | jq directly. POSIX sh has no pipefail, so a mid-walk pagination failure (tea_api_paged returning 1) was masked by jq's exit status — jq exits 0 on empty stdin, so pr-exists in particular would print "false" for a real API failure instead of erroring. That's a silent false-negative that could pass a porch pr_exists gate on a genuine error rather than an absent PR.

Fixed by capturing the paginator's output into a variable and checking its exit status before piping to jq:

PULLS="$(tea_api_paged "repos/${REPO}/pulls""state=all")"||exit 1
printf'%s'"$PULLS"| jq …

3. (strongly recommended, done) sub-limit server cap
tea_api_paged's stop condition compared each page's item count against the requested limit (50). A server whose max_response_items is tuned below that requested limit truncates every page — including non-last pages — to its own cap, so every page looked "short" against 50 and the loop broke after page 1, silently missing pages 2+. Now compares against the size actually observed on page 1 instead, so it keeps walking until a page is genuinely shorter than what the server has been returning.

4. (deferred, not done in this PR)
The # forge-executable: tea header convention depends on #1458 landing extractExecutable's header-parsing support first. Checked — #1458 is still open/unmerged, so this isn't actionable yet. Left as a follow-up once #1458 merges, matching the merge order already noted in the PR description (#1458 first, then this PR).

Testing

Added regression coverage for all three fixes in bugfix-1137-gitea-tea-api.test.ts:

  • Updated the fake-tea stub to answer comment (not comments add).
  • A mid-walk pagination failure fixture (full page 1, erroring page 2) exercised against all three paginated scripts — asserts non-zero exit and empty stdout rather than a silently wrong/empty result.
  • A 3-page sub-50-per-page-cap fixture (30 + 30 + 6 items) proving pr-list keeps walking past a page that's full-but-capped rather than stopping because it's short relative to the requested limit.

Full local suite: 3197 passed, 126 failed (67 files) — the same pre-existing environment-dependent failures (agent-farm/terminal/consolidate; no built dist/ in this worktree) as the unmodified baseline reported earlier in this PR. Zero regressions, +4 new passing tests over the prior 3193.

@waleedkadouswaleedkadous 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.

First — apologies for how long this sat; two months without a review is not how we want to treat a contribution, and this one is good: the real-script fake-CLI suite (contract normalization, pagination past page one, sub-50 server caps, mid-walk failures, tea 0.14.1 comment compatibility) is exactly the evidence this kind of fix needs, and moving branch input through jq --arg is a security improvement. Both integration reviews (codex + claude) agree the core mapping is right. Four things I'd like fixed before merge, all in the same spirit as the fix itself — fail loudly, never silently:

  1. codev doctor regression. The five scripts that now source _lib.sh make doctor's executable extractor report . (and later tea_api_paged) instead of tea — verified, which . fails under /bin/sh. #1458 introduces a # forge-executable: tea header declaration for exactly this; please add those five lines here regardless of merge order so the regression can't ship.
  2. Silent truncation at GITEA_MAX_PAGES. Reaching 100 pages without seeing a terminal short/empty page returns a partial array at exit 0 — the same false-negative pr-exists class the paginator exists to prevent. Fail explicitly instead.
  3. Error bodies normalized into valid-looking output.tea api exits 0 on HTTP errors, and pr-view/user-identity/issue-view normalize without validating the response shape — an error object becomes an all-null PR (pr-view even leaks the error body's url into the contract) or the username null, at exit 0. Validate required fields/types before normalizing; the paginated scripts already fail loudly, so this brings the rest in line.
  4. recently-merged.sh ignores CODEV_SINCE_DATE and can issue up to 100 sequential requests inside forge's 30-second timeout — on an established repo that turns the analytics path into null. Honoring the since-date bounds it.

Smaller, take-or-leave: CODEV_REPO is now overloaded (repo-archive's foreign-repo input vs. gitea read targeting) — a line in forge.md and both SKILL.md copies would help; and forge-contracts.ts's reviewRequests/isDraft comments go stale with this PR.

Merge order: #1458 first (it brings the # forge-executable mechanism), then this. Happy to turn the two around quickly once these land — and thank you for sticking with it.

@waleedkadous

Copy link
Copy Markdown
Contributor

Given how long this waited on us, we'll take the last mile ourselves rather than hand you a list: a builder will push the review items directly onto this branch (you enabled maintainer edits — thank you), your commits and authorship stay as they are, and I'll re-review and merge once green. If you'd rather make the changes yourself, just say so and we'll hold off.

waleedkadousand others added 5 commits September 3, 2026 21:23
… bodies, page ceiling, and bound recently-merged
Maintainer follow-up on PR cluesmith#1146, pushed onto the contributor's branch. All four
required items from the 2026-09-03 review, in the same spirit as the fix itself:
fail loudly, never silently.
1. `# forge-executable: tea` headers. `codev doctor` infers the CLI a concept
needs from the script's first substantive line; the five scripts that source
`_lib.sh` open with `.`, so doctor reported `.`/`tea_api_paged` as missing
tools and stopped checking for `tea`. The header (mechanism in cluesmith#1458, inert
comment until then) declares it. `user-identity.sh` gets one too: fixing its
exit-0-on-error handling below moves `tea` off the first substantive line, so
without the header that fix would have caused the very regression this item
closes.
2. The paginator fails at the `GITEA_MAX_PAGES` ceiling. Reaching 100 pages with
no terminal short/empty page means we do not know we have the whole list;
returning the partial array at exit 0 was the silent truncation the paginator
exists to prevent — a short `pr-exists` walk reads as "no PR exists" and
passes a porch pr_exists gate on a repo we merely failed to finish reading.
3. `pr-view`, `user-identity` and `issue-view` type-check the response before
normalizing. `tea api` exits 0 on HTTP errors and prints the error body,
which carries a `url` (the swagger link) — so `url: (.html_url // .url)`
succeeded on it and shipped that link as the PR's browser page inside an
otherwise all-null contract object; `user-identity` printed the literal
username "null". They now fail with the server's own message on stderr.
`issue-view` is validated before its comments are fetched, so a bad id
reports only its own error, and its comments degrade path now tests for an
actual JSON array — an error OBJECT used to reach `--argjson` and blow up
with a raw jq error instead of the warned [] degrade.
4. `recently-merged` honors `CODEV_SINCE_DATE`. It feeds a 24h analytics window
but walked the repo's entire merge history inside forge's 30s timeout, and a
timeout yields `null` — worse than truncation. It now asks for
`sort=recentupdate` and stops at the first page reaching back past the
cutoff. The stop filter refuses to trust the sort blindly: it fires only when
the page is actually non-increasing in `updated_at`, so a server that ignores
the parameter falls back to the full walk rather than silently dropping
merges. `updated_at >= merged_at` always holds, so nothing merged after the
cutoff can sit beyond that page.
Timestamps go through a new `gitea_epoch` jq helper in `_lib.sh`: Gitea marshals
RFC3339 in the server's timezone, so `+02:00` is a real response and `Z` is not
guaranteed — `fromdateiso8601` rejects those and a lexicographic compare across
mixed offsets is wrong. It also accepts the bare `YYYY-MM-DD` that
`team-update.ts` passes. Unparseable input yields null and every caller treats
null as "don't know": keep the item, keep walking.
Also, from the review's take-or-leave list: document `CODEV_REPO`'s two meanings
(repo-archive input vs. gitea read-target override) in `forge.md` and both
`SKILL.md` copies, and correct `forge-contracts.ts`'s `reviewRequests`/`isDraft`
comments — both claimed GitLab and Gitea emit empty/false, but all three presets
populate them for real.
Tests: 12 new cases in the existing real-script fake-CLI suite. The fake `tea`
grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering
with an error object, a repo whose pages never end, and sorted/unsorted
since-date repos. 33 tests in the file; full suite 4886 passed | 48 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…ge boundary, non-array pages, empty bodies
Consultation review of the previous commit (Codex). Five findings, all real.
The important one: my stop filter for `recently-merged` checked only that the
CURRENT page was non-increasing in `updated_at`, and I claimed that proved the
server honored `sort=recentupdate`. It does not. A server that ignores the
parameter can still return an internally descending page 1 — say one entirely
older than the cutoff — while a genuinely recent merge sits on page 2, and we
would have stopped and dropped it. Page-local order is also what a server with
per-page rather than global sorting produces.
The filter now requires the ordering to survive a page boundary: the previous
page descending too, and its oldest entry no older than this page's newest. It
never fires on page 1, where there is nothing to compare against — one extra
request is the right price. `tea_api_paged` binds the previous page as `$prev`
to make that check possible. The reviewer's exact counterexample is now a
fixture (`acme/lagging`).
Also:
- A page that parses but isn't an array is a hard error, not the end of the
list. `jq length` is 0 for both `null` and `{}`, so an error body mid-walk —
which `tea api` hands us at exit 0 — looked exactly like an exhausted list and
returned the pages collected so far at exit 0.
- An empty body at exit 0 now fails. jq given empty stdin emits nothing and
exits 0, so `pr-view` and `user-identity` were "succeeding" with empty stdout,
and the shape validators never ran at all.
- `gitea_epoch`'s offset is bounded to the real UTC range, so `+99:99` yields
null instead of an epoch two days out. Its remaining leniency is documented
rather than claimed away: it validates shape, not the calendar, so
`2026-02-30` normalizes into March.
- Contract types are checked, not just defaulted: non-numeric `additions`/
`deletions` no longer pass through as strings, comment fields default to the
declared type instead of emitting nulls, and a whitespace-only login is
rejected like an empty one.
Two bugs of my own that the tests caught: inside a jq `range` body `.` is the
range value, not the array (the `descending` helper needs the array bound
first), and an apostrophe inside a single-quoted jq program closes the shell
string.
37 tests in the file; full suite 4893 passed | 48 skipped. Every path also
exercised under dash, which is what /bin/sh is on the CI runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…inux argv cap), probe past the page ceiling
Second consultation lane (Claude), which independently reproduced the ordering
flaw the first lane found and confirmed the cross-page fix closes it. Four new
findings, all real.
1. Passing a page to the stop filter through `--argjson` breaks on Linux. Linux
caps a SINGLE argv string at MAX_ARG_STRLEN (128KiB) regardless of ARG_MAX,
and a 50-item Gitea pulls page — each object embedding full `base.repo` and
`head.repo` objects — measures ~90KiB before anyone writes a long PR body.
Past the cap `exec` fails, the paginator returns non-zero, and forge yields
`null`. macOS has no per-argument cap, so this would have passed locally and
failed on CI and on every Linux adopter. Both pages now go in on stdin.
The `acme/heavy` fixture serves ~150KB pages so the regression bites where
the bug lives.
2. The page ceiling false-positived on a complete result. A list whose length is
an exact multiple of the page size reaches GITEA_MAX_PAGES with every page
full and nothing missing, and we hard-failed it. One probe request past the
ceiling settles it: empty means we already had everything.
3. A non-string `.message` — what a proxy or gateway between tea and Gitea
produces — was concatenated straight into the error text and threw a raw jq
error, defeating the point of a legible message. Now `(.message // .)
| tostring`.
4. The test environment inherited `CODEV_*` from the developer's shell, so the
"no CODEV_SINCE_DATE means walk everything" test was asserting the absence of
a variable it did not control. Stripped from the base env; each test supplies
what it means.
Also: `issue-view`'s comments guard checked the outer array but not its
elements, so `[1, 2]` got past it and died on `$comments[] | .body` instead of
degrading to [].
Both lanes noted that the `forge-executable` test asserts the declaration is
present, not that doctor reads it — that is sequencing, not coverage, and the
test now says so.
41 tests in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
…uesmith#1146
Records what the two consultation lanes broke and why, the ordering argument
that did not hold, the Linux-only argv finding that could not reproduce on
macOS, and the reason the forge-executable header had to go on six scripts
rather than the five the review named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch
@waleedkadous

Copy link
Copy Markdown
Contributor

Pushed the review items onto this branch as we offered — four commits on top of yours, nothing rebased or squashed, all ten of your commits and their authorship untouched. Thank you again for the contribution and for the patience; the real-script fake-CLI suite you built is what made all of this cheap to do, and every fix below is a test in your harness.

Also merged main in: #1458 landed, so the # forge-executable: mechanism is live.

The four required items

1. # forge-executable: tea headers. On six scripts, not five. The five that source _lib.sh are the ones the review named, but fixing item 3 in user-identity.sh required capturing tea api user before the jq pipe, which moves tea off the first substantive line — so without a header of its own, that fix would have caused the regression this item closes. Verified against the merged extractExecutable, not a simulation of it: all fourteen gitea concepts now resolve to tea.

2. The page ceiling fails loudly. Reaching GITEA_MAX_PAGES with every page full means we don't know we have the whole list, so it errors rather than returning the partial array at exit 0. One wrinkle worth naming: a list whose length is an exact multiple of the page size hits the ceiling with nothing missing, so it probes one page past before failing — a hard error on a complete result would be its own bug.

3. Shape validation before normalizing. The nastiest part of this one is that Gitea's error bodies carry a url (the swagger link), so url: (.html_url // .url)succeeded on them and shipped that link as the PR's browser page inside an otherwise all-null contract object. Required fields are now type-checked and failures carry the server's own .message on stderr. issue-view is validated before its comments are fetched, so a bad id reports only its own error instead of also warning about degraded comments on an issue that isn't there.

4. recently-merged honors CODEV_SINCE_DATE — it asks for sort=recentupdate and stops at the first page reaching past the cutoff. Details below, because this is where the reviewers earned their keep.

Plus the take-or-leave items: CODEV_REPO's two meanings documented in forge.md and both SKILL.md copies, and the reviewRequests/isDraft comments in forge-contracts.ts corrected — they claimed GitLab and Gitea emit empty/false, but all three presets populate them for real.

What the consultation caught, because it's the interesting part

I first wrote the since-date bound to fire when the current page was non-increasing in updated_at, reasoning that proved the server had honored sort=recentupdate. Both reviewers broke that, and they were right. Gitea's default order is index/created-DESC, and on a repo where PRs are opened and merged in order, index-DESC is non-increasing in updated_at. A long-lived PR opened in January and merged after the cutoff then sits on page 2, and we stop at page 1 and drop it — a silent loss in exactly the analytics path the bound was meant to protect.

The fix is to check for the property we actually need — the ordering — rather than for evidence that we asked for it. A stop now requires the order to survive a page boundary (previous page descending too, its oldest no older than this page's newest) and never fires on page 1, where there's nothing to compare against. A server that ignores the parameter falls back to your full walk: slower, never wrong. Costs exactly one extra request on an honest server.

The other finding I'm glad we ran: passing a page to the stop filter through jq --argjson blows up on Linux. MAX_ARG_STRLEN caps a single argv string at 128KiB regardless of ARG_MAX, and a 50-item Gitea pulls page — every object embedding full base.repo and head.repo — measures ~90KiB before anyone writes a long PR body. macOS has no per-argument cap, so it passed locally and would have failed on CI and for every Linux adopter. Both pages go in on stdin now.

Smaller ones from the same pass: jq length is 0 for both null and {}, so an error body mid-walk looked exactly like an exhausted list; jq on empty stdin emits nothing and exits 0, so two scripts were "succeeding" with empty stdout and never running their validators at all; a non-string .message threw a raw jq error instead of the legible one; and the test env inherited CODEV_* from the developer's shell, so the "no CODEV_SINCE_DATE" test was asserting the absence of a variable it didn't control.

Testing

41 tests in bugfix-1137-gitea-tea-api.test.ts, +20 on what you had. Your fake tea grew error bodies at exit 0 for pulls/issues/user, a comments endpoint answering with an error object and one answering with non-objects, a repo whose pages never end, one whose length is an exact multiple of the page size, ~150KB pages for the argv cap, and the sorted/lagging pair for the since-date bound — acme/lagging is the reviewer's counterexample verbatim.

Full suite after merging main: 5549 passed | 48 skipped, 0 failures. Every script also exercised under dash, which is what /bin/sh is on the CI runner, rather than only bash.

Two follow-ups from the earlier reviews are still unfiled and still worth doing, neither in scope here: migrating issue-list/issue-search/recently-closed off tea <entity> list --limit onto tea_api_paged (same truncation premise this PR disproved), and repo-archive.sh's bare ${CODEV_REPO} with no fail-fast.

Over to @waleedkadous for the re-review.

…p CI flake
Failed once on the first run of the pushed branch, passed on re-run with no
code change. Unrelated to this PR — nothing here touches Tower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HvYzPVkD7jYoE7j9q61Ch

@waleedkadouswaleedkadous 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.

Re-reviewed after the maintainer-side commits: six # forge-executable: tea headers (verified against the merged extractExecutable — all fourteen gitea concepts resolve to tea), the paginator now fails loudly at the page ceiling with a one-page probe so exact-multiple lists don't false-alarm, non-array error bodies are refused instead of normalized to null, recently-merged honors CODEV_SINCE_DATE with the stop filter fed via stdin (Linux argv cap), plus the docs and stale comments. 41 tests in the contributor's own fake-CLI harness; CI 7/7 green on a6eddb6. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.

@waleedkadous
waleedkadous merged commit 8eaf072 into cluesmith:mainSep 4, 2026
7 checks passed
Sign up for freeto 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)

3 participants

@pseudoseed@amrmelsayed@waleedkadous