Skip to content

Adopt our two stranded upstream fixes: gitea tea-api reads (#1146) + pr-create forge concept (#1458) - #5

Merged
pseudoseed merged 25 commits into
mainfrom
fork/adopt-forge-fixes
Aug 21, 2026
Merged

Adopt our two stranded upstream fixes: gitea tea-api reads (#1146) + pr-create forge concept (#1458)#5
pseudoseed merged 25 commits into
mainfrom
fork/adopt-forge-fixes

Conversation

@pseudoseed

Copy link
Copy Markdown
Owner

Adopts two fixes that have been sitting unmerged on cluesmith/codev into this fork, so we stop waiting on upstream.

Upstream PR Opened Still open upstream What it does
#1146 2026-07-06 6.5 weeks Fix the gitea forge preset against the real tea CLI — routes gitea reads through tea api, paginates, fails fast on REPO, warns on degraded comments
#1458 2026-08-14 1 week Add a pr-create forge concept so non-GitHub forges can open PRs (gitea/github/gitlab scripts + contract + porch prompt wiring)

Both authored by @pseudoseed.

Why this is safe

Merge, don't squash

Per this repo's convention, merge with --merge. The individual commits document the work and
matter here for a second reason: when upstream eventually merges cluesmith#1146/cluesmith#1458, our next
upstream/main sync will see the same changes and resolve as a no-op rather than a conflict.

Note on CI

main is currently red on tower-api.e2e.test.ts (13 failures, all 401 where a 400/404 was
expected). That is inherited from the upstream commit we fast-forwarded to (ff855d3e7) and is
unrelated to this PR — upstream's own CI is green on that identical SHA, and the file passes
locally here. Tracked separately.

pseudoseed and others added 25 commits August 13, 2026 22:27
…rge concept

`pr-create` was the one forge operation with no concept behind it. It was
absent from KNOWN_CONCEPTS, no provider shipped a script for it, and every
protocol prompt wrote `gh pr create` literally — so a project with
`forge.provider: gitea` fully configured still shelled out to `gh` at the
single most important write in the protocol, and only worked if someone kept
a `gh`→forge shim on PATH.

Contract (env in, JSON out — the shape every other concept uses, so it stays
callable from executeForgeCommand):
  in:  CODEV_PR_TITLE, CODEV_PR_BODY, and optional CODEV_PR_BASE / _HEAD /
       _REPO / _DRAFT
  out: {"number": <int>, "url": "<web url>"}

- scripts/forge/github/pr-create.sh — `gh pr create` with the flags it already
  took, so nothing changes for GitHub users.
- scripts/forge/gitea/pr-create.sh — `tea pulls create --description` (not
  `--body`), tea's rendered output pushed to stderr, and the new PR looked up
  via `tea pulls list --output json` instead of parsing that rendered view.
- scripts/forge/gitlab/pr-create.sh — `glab mr create`, marked UNVERIFIED
  (`glab` is not installed here); without it the gitlab preset falls through
  to `gh`, which is this bug.
- porch substitutes `{{pr_create_command}}` into phase prompts, mirroring the
  existing `pr-merge` injection, and falls back to "open the PR manually" when
  the concept is disabled.

Verified end to end against a live Forgejo with tea 0.14.2: the created PR's
body, read back from the server, is byte-identical to the input (428 bytes,
quotes/backticks/$VAR/backslash/fenced block all intact), and base, head and
title match. On 0.14.2 with a single login and an explicit --head, `tea pulls
create` needs no --repo/--login and does not prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Porch substitutes every occurrence of the token, so the sentence explaining
the token rendered as "Porch substitutes /abs/path/pr-create.sh with your
forge's pr-create concept command". Found by rendering the real BUGFIX pr
prompt through the locally built porch. The prose now refers to "the command
above", and the regression test pins exactly one occurrence per prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…QUEST_CHANGES)

Three real defects, each with a test that fails without the fix.

1. Inline overrides received empty inputs (codex). The prompts set the inputs
   as an assignment prefix — `CODEV_PR_TITLE=… <cmd>`. A script reading the
   environment works, but an inline override, which is the documented form
   (`"pr-merge": "glab mr merge \"$CODEV_PR_NUMBER\" --yes"`), has its
   `"$CODEV_PR_TITLE"` argument expanded by the calling shell BEFORE the
   assignment applies, so it got "". The prompts now export. Pinned by a test
   that renders the shipped prompt, extracts the bash block and executes it
   against an inline override.

2. `codev doctor` reported `pr-create … set not found` (claude). extractExecutable
   returns a script's first substantive command, which is `set -e` here, so
   doctor looked for `set` on PATH and could no longer tell a Gitea user that
   `tea` is missing — for the one write that matters. Added a
   `# forge-executable: <tool>` declaration honoured ahead of the heuristic,
   and a shell-builtin skip list. Verified against the built dist:
   github→gh, gitea→tea, gitlab→glab.

3. `gitea/pr-create.sh` looked the new PR up without `--limit`, unlike every
   sibling gitea script. On a repo with more open PRs than tea's default page,
   it would create the PR and then exit 1 saying it could not find it. Now
   `--limit 200`, matching the house convention.

Also: the disabled-concept fallback rendered as a `#` comment — valid shell
that exits 0 without opening a PR. It now writes to stderr and returns false.

gemini APPROVE. Full suite 4888 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing an empty one

CMAP round 2 (codex, REQUEST_CHANGES): the scripts validated CODEV_PR_TITLE
but not CODEV_PR_BODY. `--body ""` / `--description ""` succeeds everywhere,
so a caller who forgot the variable entirely got a bodyless PR at exit 0 —
exactly the silent failure cluesmith#1455's testing notes describe. The scripts now
distinguish unset from deliberately empty (`${CODEV_PR_BODY+x}`) and fail
before reaching the forge CLI, with a test per provider covering both cases.

Also documents CODEV_PR_LOGIN (read by the gitea script, previously named
nowhere) in the script header, the contract and forge.md — claude's minor
note on the same round.

gemini APPROVE, claude APPROVE. Full suite 4891 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t concept counts

CMAP round 3 non-blocking notes (all three lanes APPROVE):

- `gitea/pr-create.sh` read `.head` as a plain string (true on tea 0.14.2)
  while sibling `pr-exists.sh` reads `.head.ref`. Accept either shape — a
  lookup miss here reports failure for a PR that was actually created, which
  invites a duplicate on retry.
- `forge.ts` and `arch.md` still said "15 concepts" and omitted `pr-create`
  (along with `issue-search` and `repo-archive`). Now 18, listed.

Re-verified live against Forgejo after the change (PR #17: body, base and head
correct read back from the server; closed and branch deleted). Full suite 4891.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
…rop the lookup

The gitea `pr-create` ran `tea pulls create` (whose output is a rendered,
ANSI-decorated view, not parseable) and then searched for the PR it had just
made with `tea pulls list --limit 200`.

That search was built on a disproven assumption. cluesmith#1146 established, and this
change re-confirmed against live Forgejo 15.0.2, that Gitea caps every list
response at the server's `max_response_items` — default 50. `settings/api`
reports 50, and a `?limit=200` request returns exactly 50 items where paging at
50 returns 53. So `--limit 200` silently truncates: on a busy repo the
just-created PR falls off the first page, and pr-create reported

    created the PR but could not find an open pull for head '<branch>'

and exited 1 for a PR that exists — inviting a duplicate retry at the single
most important write in the protocol.

Rather than paginate the lookup, remove it. `tea api -X POST
repos/{owner}/{repo}/pulls` RETURNS the created PR — `number` and `html_url` —
in its response body, so there is nothing to search, nothing to race, and
nothing to truncate. It also drops the `<user>:<branch>` head-matching
heuristic: the API resolves an owner-qualified head itself.

Live verification against tea 0.14.2 + Forgejo 15.0.2 turned up three defects
in the obvious version of that change. Each is the same bug class as cluesmith#1455
itself — an operation accepted and then silently not performed — so each is
handled in code, not left as a caveat.

1. `tea api` EXITS 0 on HTTP errors, printing the error body. Since the whole
   change replaces a lookup with a single call, trusting that exit code would
   reintroduce cluesmith#1455's silent success inside the fix for it: a 404 or 422 would
   be reported as a created PR. The response is therefore asserted to BE a PR
   object — an object carrying a numeric `number` AND a non-empty browser URL —
   and anything else fails loudly with the response body. Pinned by tests that
   feed an error object, an array, a string-typed `number`, a numberless object,
   `null` and an empty body, all at exit 0.

   The one case where `number` is present but the URL is not gets its own
   message: the PR WAS created, so it names the number and says not to retry.
   Reading that as "nothing happened" is how duplicates get opened.

2. `base` is REQUIRED by the API — it answers `[Base]: Required` — where
   `tea pulls create` defaulted it client-side. Silently posting against the
   wrong base would be worse than erroring, so an unset CODEV_PR_BASE now
   resolves the repo's default branch explicitly, and fails with a clear
   message if that cannot be resolved.

3. `draft: true` in the payload is SILENTLY IGNORED (the response comes back
   `draft: false`), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored
   flag. Gitea marks a draft by a `WIP:` title prefix — exactly what
   `tea pulls create --draft` does — so that is now implemented, and verified
   server-side to produce `draft: true`.

Also verified live: `{owner}`/`{repo}` are substituted by tea from the repo
context, with `--repo owner/name` supplying it when the cwd has no Gitea remote
(checked with https and scp-style remotes, and from a GitHub-remote cwd); `url`
on the create response is the browser page, so `.html_url // .url` lands the
right one in the contract; and the body round-trips byte-identically, being
built with `jq --arg` and fed on stdin (`-d @-`) rather than surviving an argv
round-trip.

The unresolvable-repo case used to surface as a bare `404 page not found`; it
now names CODEV_PR_REPO as the remedy, matching the fail-fast ergonomics of
`_lib.sh#gitea_repo` in cluesmith#1146 without taking a dependency on that PR — this
change stands alone and the two can merge in either order.

Tests: the gitea half of the concept suite is rewritten against a `tea api`
stub. Every new case fails against the previous script and passes against this
one, including an explicit assertion that no `pulls`/`list`/`--limit` call is
made at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…luesmith#1458

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…luesmith#1458

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…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>
@pseudoseed
pseudoseed merged commit 73689ca into main Aug 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant