Uh oh!
There was an error while loading. Please reload this page.
[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458
Conversation
…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>
…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>
pseudoseed
commented
Aug 14, 2026
Reconciled with #1146 — the |
…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>
915fa62 to
35d575cCompareamrmelsayed
commented
Aug 17, 2026
Reviewer Integration Review (CMAP-3)Three-lane consultation (Gemini, GPT-5.6 Sol via Codex, Claude Opus) plus an independent architect pass. Every credited finding was re-verified against the actual source, and every Verdict: APPROVE, with three small pre-merge recommendations. Lane split: Gemini APPROVE, Claude APPROVE, Codex REQUEST_CHANGES on one finding I assess below as a non-blocking hardening note. The concept lands at the right seam, the contract matches its 17 siblings, both trees are mirrored, and the tests pin behavior classes rather than instances — the inline-override test that executes the shipped prompt block, and the six-payload silent-success table, are the strongest test work we've seen on a community PR. Verified independently (not inherited from the #1146 review)
Ruling on the cross-PR inconsistency flagged in the #1146 reviewThe two halves of the gitea preset currently disagree about Recommended before merge (all small, all verified real)
Assessed non-blocking (Codex lane's REQUEST_CHANGES)Codex flagged the Cross-PR coordination note for the maintainerThis PR's Follow-ups (file after merge, none blocking)
Process notes
|
…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
commented
Aug 21, 2026
Integration-review follow-upAll three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in
Architect verification: reviewed the four-file follow-up diff and reran the two affected regression files — 104 passed, 0 failed. The builder also reports the full suite at 4,902 passed, 48 skipped, 0 failed, with a clean build. PR remains mergeable at Ready for upstream maintainer re-review. |
… 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>
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)
waleedkadous
left a comment
There was a problem hiding this comment.
Thank you for this — and apologies for the wait. The pr-create concept lands at the right seam: codex approved outright, and claude's lane verified provider routing, prompt substitution, twin-tree mirroring and the # forge-executable: header (which is a genuinely reusable improvement — we'll adopt it as house style and it also rescues #1146's doctor regression). One change I need before merge, and a few you might consider:
Must fix — the Linear preset.forge.ts now lists pr-create among Linear's disabled concepts, and forge.test.ts pins "linear provider disables pr-create instead of falling through to gh". That reverses spec 719's documented hybrid model: Linear owns issues and PRs stay on GitHub — the spec explicitly fixed buildPresetFromScripts so missing PR scripts fall through because nulling them "fundamentally breaks the hybrid forge model". Every other PR concept still falls through to gh for Linear, so as written Linear would be the one provider that can merge a PR but not open one. Please remove 'pr-create' from that list and drop the pinning test (a test asserting the fall-through would be welcome instead).
Non-blocking, your call:
- The ~75-word explanatory paragraph is duplicated across 10 prompt files;
{{> …}}includes are the established mechanism (loadPromptFileresolves them) and Spec 1280 pushed hard on prompt size — one include would do. - PIR's review prompt still hardcodes
gh pr edit/gh pr view/gh pr mergeafter the newly-routed create — thegh pr mergeone bypasses an existing concept. Pre-existing, but it leaves the target forges broken end-to-end in PIR; worth an issue so it isn't lost. doctor.ts~1052/~1070 still say "all 15 concepts" (pre-existing drift the PR otherwise corrected inforge.tsandarch.md).SHELL_BUILTINSnow also governs the inline-command branch ofextractExecutable— a small behavior change for user config that's untested and unmentioned.
Merge order: this one first, then #1146 (which needs the five # forge-executable: tea declarations this PR makes possible). I'll turn the re-review around quickly.
waleedkadous
commented
Sep 4, 2026
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. |
… close inline-branch executable gap Maintainer-side review fixes pushed onto PR cluesmith#1458 (contributor: pseudoseed). Contributor commits and authorship are unchanged; this only adds on top. 1. Revert eb70726's Linear change. Adding 'pr-create' to the linear preset's disabled list reverses spec 719's hybrid forge model, in which Linear owns *issues* and every PR concept falls through to the gh default. Spec 719's success criteria name the disabled list verbatim as ['team-activity', 'on-it-timestamps'], and its problem statement calls nulling missing PR scripts what "fundamentally breaks the hybrid forge model". Every other PR concept still falls through for Linear, so as written Linear was the one provider that could merge a PR but not open one — and porch substitutes a hard-failing stub for a disabled concept, so a Linear builder could not open a PR through the protocol at all. The pinning test is replaced by one asserting the fall-through, plus a class-level guard that Linear disables only those two concepts — so a future disable of any PR concept fails, not just pr-create by name. 2. eb70726 added `.`/`source` to SHELL_BUILTINS, which governs BOTH branches of extractExecutable, leaving them inconsistent: the script-file branch scans past a builtin to the real CLI, while the inline branch returned null at the first one. doctor treats a null executable as "nothing to check", so an inline override `. env.sh; gh issue view "$1"` reported healthy without ever checking for gh — cluesmith#1455's silent-success shape inside the reporter for it. The inline branch now scans past builtins like its sibling. Not on the maintainer's review list; fixed because it is small, introduced by this PR rather than pre-existing, and the alternative was a green test entrenching the gap as intended behavior. 3. doctor.ts said "all 15 concepts" in two comments; KNOWN_CONCEPTS has 18. doctor.test.ts's mock was stale in the same way and omitted pr-create itself, so the new concept's report row was never exercised; synced to all 18. 4. forge.md documented three providers; `linear` has shipped since spec 719 undocumented. One sentence records it and its hybrid model. Full suite: 4907 passed, 48 skipped, 0 failed (+5 over the 4902 baseline). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Ve2jGKEK3JxcS9p6iWgpW
Records why the Linear pr-create disable was reverted (spec 719's hybrid forge model), the inline-branch silent-success gap the CMAP round surfaced, the two items deferred to issues cluesmith#1610/cluesmith#1611, and two process scars: a consult lane mutating the live worktree mid-commit, and the missing-node_modules trap that makes a failed build read as green when piped to tail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Ve2jGKEK3JxcS9p6iWgpW
waleedkadous
commented
Sep 4, 2026
Maintainer review items pushed ( |
| branch | on hitting a builtin |
|---|---|
| script-file | keeps scanning, finds gh |
| inline command | returned null at the first one |
doctor reads r.executable ? commandExists(r.executable) : true — a null executable means nothing to check. So an inline override . env.sh; gh issue view "$1" rendered ✓ issue-view override — and doctor never verified gh was installed. Before your change it at least warned, albeit about the wrong thing.
That is #1455's own silent-success shape inside the reporter built for #1455, so we fixed it rather than documenting it: the inline branch now scans past builtins to the real CLI, matching its sibling. Your ./source addition was right — it just needed the other branch brought along with it. Four lines, full suite unaffected.
Small items
doctor.ts:1052,1070— "all 15 concepts" → 18, finishing the drift you'd already corrected inforge.tsandarch.md.doctor.test.ts— its mock was stale the same way and omittedpr-createitself, so this PR's new concept never appeared in a doctor report row under test. Synced to all 18.forge.md— added one sentence recording that thelinearprovider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through togh.
Deliberately not done here
Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:
- Prompt duplication: the pr-create explanatory paragraph is copied across 10 prompt files #1610 — the ~75-word
pr-createparagraph is copied into 10 prompt files.{{> include}}is the established mechanism and Spec 1280 pushed hard on prompt size. - PIR's review prompt still hardcodes gh pr edit/view/merge after the newly-routed pr-create #1611 — PIR's review prompt still hardcodes
gh pr edit/pr view/pr mergeafter the newly-routed create; theview/mergetwo bypass concepts that already ship. You flagged this in the PR body and it was right to flag.
Verification
Build clean. Full suite 4907 passed / 48 skipped / 0 failed — +5 over your 4902 baseline (−1 replaced, +6 added), and the inline-branch change broke nothing across 250 files. Reviewed by Codex and Claude lanes on the delta; both raised findings, both are folded in above.
Merge order is unchanged: this PR first, then #1146 with the five # forge-executable: tea declarations this one makes possible. Over to @waleedkadous for the re-review.
waleedkadous
left a comment
There was a problem hiding this comment.
Re-reviewed after the maintainer-side commits: Linear's disabled list is back to spec 719's exact ['team-activity', 'on-it-timestamps'] with a fall-through test and a hybrid-model guard replacing the pinning test; the extractExecutable inline branch now skips leading builtins instead of silently returning null; doctor's concept count corrected. CI 7/7 green on 85a0e78. Approving and merging — thank you, @pseudoseed, for the contribution and the patience.
Uh oh!
There was an error while loading. Please reload this page.
… 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
Summary
pr-createis now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out toghunconditionally.Fixes#1455
Root cause
Four linked gaps, not one:
packages/codev/src/lib/forge.ts—KNOWN_CONCEPTShad nopr-create. Everything derives from that list (getDefaultCommands,buildPresetFromScripts,resolveAllConceptsforcodev doctor,validateForgeConfig), sogetForgeCommand('pr-create', …)returnednullfor every provider and a hand-writtenforge["pr-create"]override was reported by doctor as an unknown concept — and read by nothing.pr-create.shin any provider directory.gh pr createwritten literally into 7 prompt files per tree (14 total).pr-merge(porch/next.ts:227and:768).Reads and
pr-mergeroute correctly, so a Gitea project looks fully configured right up to the one write that matters.The contract
The issue asked for agreement on the signature before anyone wrote it. This PR uses environment variables in, JSON on stdout — the shape every other concept already has:
CODEV_PR_TITLE(required),CODEV_PR_BODY(required — set it to""for an empty body; an absent one is rejected),CODEV_PR_BASE,CODEV_PR_HEAD,CODEV_PR_REPO,CODEV_PR_DRAFT(optional; gitea also readsCODEV_PR_LOGIN){"number": <int>, "url": "<web url>"}— the browser URL, never an API endpointThe alternative — a CLI-compatible wrapper taking
--title/--body/--base/--headon argv — would have preserved the existing prompt text verbatim, butexecuteForgeCommand()passes inputs asCODEV_*env and parses stdout, so an argv contract would makepr-createthe one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.Deliberately not included: a
CODEV_PR_BODY_FILEinput. The issue's own testing note describes a shim that silently dropped--body-fileand posted empty bodies at exit 0 for months; one way in is one thing to get wrong.What changed
forge.ts—pr-createinKNOWN_CONCEPTS(one line; presets, doctor and config validation follow).forge-contracts.ts—PrCreateResult.scripts/forge/github/pr-create.sh—gh pr createwith the flags it already took. Behaviour for GitHub users is unchanged.scripts/forge/gitea/pr-create.sh—tea pulls create --description(not--body). tea's rendered, line-wrapped, ANSI-decorated output goes to stderr; the new PR is then looked up by head branch throughtea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.scripts/forge/gitlab/pr-create.sh— scope deviation, argued rather than chosen silently. The issue asked for two scripts. Without a gitlab script the preset falls through to the GitHub default and runsgh, which is the bug this closes. It is markedglabis not installed in the authoring environment — same convention as the existinggitea/issue-search.sh. Say the word and I'll drop it.porch/prompts.ts—{{pr_create_command}}template variable, resolved per project config, falling back to "open the PR manually" when the concept is disabled.export, not an assignment prefix — see the CMAP section below. PIR moved off--body-filetoCODEV_PR_BODY="$(cat codev/reviews/….md)".extractExecutablehonours a# forge-executable: <tool>declaration, socodev doctorreports the CLI a script actually needs rather than the first line of a guard clause.codev/resources/commands/forge.mdand the byte-identical.claude/.codexforge SKILL.md pair.bugfix-685-close-keyword.test.ts— its PR-body-template regex now acceptsCODEV_PR_BODY=alongside--body. Guard intent unchanged.Verification
Live Forgejo,
tea0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:{"number":15,"url":"https://…/pulls/15"}and nothing else on stdout. The body was then read back from the server over the REST API: 428 bytes sent, 428 received, byte-identical —"quotes",`backticks`,$VAR,\backslash, a fenced code block, checkboxes and an em dash all intact. Server-sidebase,headandtitlematched the inputs. This is the assertion the issue asked for, not "exit 0".CODEV_PR_HEAD: thegit rev-parse --abbrev-ref HEADdefault and tea's own base default both resolved correctly.--head,tea pulls createneeds no--repo/--loginand does not prompt. Both are still forwarded when set, for multi-login hosts and for the 0.11.x autodetect-prompt failure.This PR was opened with
scripts/forge/github/pr-create.sh—CODEV_PR_REPO=cluesmith/codev,CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.Regression tests —
bugfix-1455-pr-create-concept.test.ts(dispatcher routing per provider; the scripts' contract exercised against fakegh/teaon PATH, including the multi-line tricky body byte-for-byte, the--description-not---bodymapping, cross-repo<user>:<branch>head matching, and the failure paths) andbugfix-1455-pr-create-prompt.test.ts(porch renders the right command for github/gitea/override/disabled). Removingpr-createfromKNOWN_CONCEPTSturns 7 of them red.Review: two CMAP rounds, four defects fixed
Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:
CODEV_PR_TITLE=… <cmd>). A script reading the environment works, but an inline override — the documented form, e.g."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 nowexport. Pinned by a test that renders the shipped prompt, extracts the bash block and executes it against an inline override.codev doctorreportedpr-create … set not found(claude).extractExecutablereturns a script's first substantive command, which isset -ehere — so doctor looked forseton PATH and could no longer tell a Gitea user thatteais missing. Fixed with the# forge-executable:declaration plus a shell-builtin skip list; verified against the builtdist.--limit(claude), 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 claiming it couldn't find it. Now--limit 200.Round 2 — gemini APPROVE, claude APPROVE, codex REQUEST_CHANGES:
--body ""succeeds on every forge — so a caller who omitted the variable got a bodyless PR at exit 0, the same silent failure the issue's testing notes describe. The scripts now distinguish unset from deliberately empty and fail before reaching the forge CLI.Also taken: the disabled-concept fallback used to render as a
#comment (valid shell, exit 0, no PR) and now fails loudly;CODEV_PR_LOGINis documented where it's read.Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken:
gitea/pr-create.shnow accepts.headas either a string (tea 0.14.2) or the object-with-.refshape sibling scripts assume, and the stale "15 concepts" counts inforge.tsandarch.mdare corrected to 18. Re-verified live against Forgejo afterwards.Test plan
pnpm buildcleancli-tower-mode.e2e.test.tsexpectshttp://localhost:…and this machine's Tower binds0.0.0.0)Disclosure: one unrelated test is timing-sensitive on my machine
spec-1280-measurement-instrument.test.tsintermittently fails here withTest timed out in 60000ms— usually 1 test, sometimes 2, alwaysPHASE_ITERS is a linear comparison constantand/or the determinism test. Both shell out toscripts/measure-prompt-surface.shtwice.codev/state/bugfix-1323_thread.mdwhen the 60 s budgets were set. Two calls per test lands right on the budget.upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.vitest runwith output to a file, 4884 passed).I deliberately did not raise those budgets in this PR — that's an unrelated test, and papering over a real performance signal inside a bugfix is the kind of scope creep a reviewer should reject. Worth its own issue if the numbers reproduce on your hardware.
Out of scope, found while testing (no changes made)
Three pre-existing
gitearead-concept bugs, all in #1137/#1146 territory rather than this one:tea pulls view <n> --output jsonignores<n>and returns a list of all pulls, sogitea/pr-view.sh'sjq '.url = (.html_url // .url)'receives an array.tea pulls listrejects--fields description, sogitea/pr-list.sh'sdescription → bodymapping can't populate.tea pulls list --output jsonreturnsheadas a plain branch string, butgitea/pr-exists.shfilters on.head.ref.Also:
gh pr edit --body-fileis still hardcoded in the PIR review prompt.pr-editisn't a concept and adding one is a separate change.🤖 Generated with Claude Code
Update (2026-08-14): gitea
pr-createno longer looks the PR upThe gitea script created the PR with
tea pulls createand then searched for it withtea pulls list --limit 200. That--limit 200was added here as a fix for a review defect, but it rests on an assumption the sibling PR #1146 disproves: Gitea caps every list response atmax_response_items, default 50 —--limit 200does not raise the cap, it silently truncates. On a busy repo the just-created PR falls off the first page and this script exited 1 for a PR that exists, inviting a duplicate retry.Re-confirmed live against Forgejo 15.0.2, not inferred:
settings/apireportsmax_response_items: 50, and a?limit=200request returned exactly 50 items on a list where paging at 50 returned 53.Fix: delete the lookup rather than paginate it.
tea api -X POST repos/{owner}/{repo}/pullsreturns the created PR —numberandhtml_url— in its response body. Nothing to search, nothing to race, nothing to truncate, and the<user>:<branch>head-matching heuristic goes too (the API resolves an owner-qualified head itself).Live testing turned up three defects in the obvious version of that change, each the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code rather than noted as a caveat:
tea apiexits 0 on HTTP errors. Since this replaces a lookup with a single call, trusting the exit code would reintroduce pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim #1455's silent success inside the fix for it. The response is asserted to be a PR object — an object with a numericnumberand a non-empty browser URL — or it fails loudly with the body. The case wherenumberis present but the URL is not gets its own message naming the number and saying explicitly not to retry: the PR was created, and reading that as "nothing happened" is how duplicates get opened.base([Base]: Required), wheretea pulls createdefaulted it client-side. An unsetCODEV_PR_BASEnow resolves the repo's default branch explicitly.draft: truein the payload is silently ignored (response comes backdraft: false), soCODEV_PR_DRAFT=1would have been an accepted-and-ignored flag. Gitea's draft marker is aWIP:title prefix — whattea pulls create --draftdoes — now implemented and verified server-side.Full detail, including the verified
{owner}/{repo}placeholder behaviour and byte-exact body round-trip, is in the reconcile comment.Testing
bugfix-1455-pr-create-concept.test.tsis rewritten against atea apistub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that nopulls/list/--limitcall is made at all.@cluesmith/codevunit suite on this branch: 3218 passed, 126 failed (67 files).upstream/mainin the same worktree: 3176 passed, 126 failed — the same 67 files and same 126 tests. Zero regressions; this branch adds 42 passing tests. The pre-existing failures areagent-farm/terminal/consolidate(shellper sockets, SQLite state), environment-dependent: no builtdist/, live Tower on the same state.pseudoseed/research; every scratch PR closed, every scratch branch deleted. Noteatoken scope widened.Merge order with the sibling PR — verified, not assumed
#1146 and #1458 come from the same fork and both touch
packages/codev/scripts/forge/gitea/, so the ordering question is fair. The answer:Either order is safe. There is no dependency and no conflict.
git merge-treeon the two branch tips merges cleanly. The only file both touch is the builder thread log, which is the identical blob on both branches and auto-merges.pr-create.shdoes notsource _lib.shand does not callgitea_repoortea_api_paged. Nothing in it resolves against #1146._lib.shandpr-view.share byte-identical to #1146's versions andpr-create.shbyte-identical to #1458's — no silent blending. Thebugfix-693invariant (every entry under each provider dir is a*.sh) still holds with_lib.shpresent.Does #1458 duplicate something #1146 makes shared?
The paginator: no, and it shouldn't.
_lib.sh#tea_api_pagedexists to walk a truncating list endpoint.pr-createno longer lists anything — it reads the new PR out of the create response — so there is no pagination for it to share. That is the point of the reconcile rather than an oversight.Repo resolution: yes, there are two paths, and this is worth a follow-up.
. _lib.sh→gitea_repo(), which honoursCODEV_REPO, else derivesowner/repofrom the origin remote, and fails fast namingCODEV_REPOas the remedy.pr-create: tea's own{owner}/{repo}placeholders, withCODEV_PR_REPOforwarded astea --repo.They were kept separate deliberately, for two reasons rather than by omission:
pr-createtakesCODEV_PR_REPO; the read concepts takeCODEV_REPO.gitea_repo()reads the latter and takes no argument, sopr-createcould not call it without changing its signature — which would mean editing a [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 file from [Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs #1458 and creating exactly the merge-order coupling this avoids.--repodoes more than fill a path. It also supplies tea's repo/login context, verified working from a cwd whose remote is not a Gitea host. A path-only helper does not do that.Recommended follow-up (not done here, deliberately): once both PRs have landed, unify the two behind one helper that takes the override variable as a parameter — e.g.
gitea_repo "$CODEV_PR_REPO"— so there is one repo-resolution path with one error message. Doing it now would couple two independent PRs; doing it never leaves two paths that will drift. It is a small, mechanical change against a tree where both are already present.The one ergonomic gap that split created has been closed in the meantime: an unresolvable repo used to surface from
pr-createas a bare404 page not found, and now namesCODEV_PR_REPOas the remedy, matchinggitea_repo()'s fail-fast message.