[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

3 participants

@pseudoseed@amrmelsayed@waleedkadous
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e 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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 \u003e 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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

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 #1455] Add a pr-create forge concept so non-GitHub forges can open PRs - #1458

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

[Bugfix #1455] Add a pr-create forge concept so non-GitHub forges can open PRs#1458
waleedkadous merged 19 commits into
cluesmith:mainfrom
pseudoseed:builder/bugfix-1455

Conversation

@pseudoseed

@pseudoseedpseudoseed commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

pr-create is now a forge concept, so PR creation routes through the same dispatcher as the other 17 concepts instead of shelling out to gh unconditionally.

Fixes#1455

Root cause

Four linked gaps, not one:

  1. packages/codev/src/lib/forge.tsKNOWN_CONCEPTS had no pr-create. Everything derives from that list (getDefaultCommands, buildPresetFromScripts, resolveAllConcepts for codev doctor, validateForgeConfig), so getForgeCommand('pr-create', …) returned null for every provider and a hand-written forge["pr-create"] override was reported by doctor as an unknown concept — and read by nothing.
  2. No pr-create.sh in any provider directory.
  3. gh pr create written literally into 7 prompt files per tree (14 total).
  4. Nothing injected a resolved command into the PR-opening prompts, although the precedent existed for pr-merge (porch/next.ts:227 and :768).

Reads and pr-merge route 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:

InputsCODEV_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 reads CODEV_PR_LOGIN)
Output{"number": <int>, "url": "<web url>"} — the browser URL, never an API endpoint
Failurenon-zero exit, diagnostics on stderr, no JSON

The alternative — a CLI-compatible wrapper taking --title/--body/--base/--head on argv — would have preserved the existing prompt text verbatim, but executeForgeCommand() passes inputs as CODEV_* env and parses stdout, so an argv contract would make pr-create the one concept unreachable from TypeScript. Happy to flip it if you'd rather have the passthrough.

Deliberately not included: a CODEV_PR_BODY_FILE input. The issue's own testing note describes a shim that silently dropped --body-file and posted empty bodies at exit 0 for months; one way in is one thing to get wrong.

What changed

  • forge.tspr-create in KNOWN_CONCEPTS (one line; presets, doctor and config validation follow).
  • forge-contracts.tsPrCreateResult.
  • scripts/forge/github/pr-create.shgh pr create with the flags it already took. Behaviour for GitHub users is unchanged.
  • scripts/forge/gitea/pr-create.shtea 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 through tea pulls list --fields index,url,head --output json, which is machine-readable, rather than parsed out of that view.
  • scripts/forge/gitlab/pr-create.shscope 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 runs gh, which is the bug this closes. It is marked ⚠️ UNVERIFIED in-file because glab is not installed in the authoring environment — same convention as the existing gitea/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.
  • 14 prompt files across both trees now read:
    export CODEV_PR_TITLE=""export CODEV_PR_BODY="$(cat <<'EOF'EOF)"
    {{pr_create_command}}
    export, not an assignment prefix — see the CMAP section below. PIR moved off --body-file to CODEV_PR_BODY="$(cat codev/reviews/….md)".
  • extractExecutable honours a # forge-executable: <tool> declaration, so codev doctor reports the CLI a script actually needs rather than the first line of a guard clause.
  • Docs: codev/resources/commands/forge.md and the byte-identical .claude/.codex forge SKILL.md pair.
  • bugfix-685-close-keyword.test.ts — its PR-body-template regex now accepts CODEV_PR_BODY= alongside --body. Guard intent unchanged.

Verification

Live Forgejo, tea 0.14.2 — three scratch PRs, all closed and their branches deleted afterwards:

  • Explicit base/head: the script printed {"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-side base, head and title matched the inputs. This is the assertion the issue asked for, not "exit 0".
  • No CODEV_PR_HEAD: the git rev-parse --abbrev-ref HEAD default and tea's own base default both resolved correctly.
  • Answering the issue's open question: on tea 0.14.2, with a single configured login and an explicit --head, tea pulls create needs no --repo/--login and 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.shCODEV_PR_REPO=cluesmith/codev, CODEV_PR_HEAD=pseudoseed:builder/bugfix-1455.

Regression testsbugfix-1455-pr-create-concept.test.ts (dispatcher routing per provider; the scripts' contract exercised against fake gh/tea on PATH, including the multi-line tricky body byte-for-byte, the --description-not---body mapping, cross-repo <user>:<branch> head matching, and the failure paths) and bugfix-1455-pr-create-prompt.test.ts (porch renders the right command for github/gitea/override/disabled). Removing pr-create from KNOWN_CONCEPTS turns 7 of them red.

Review: two CMAP rounds, four defects fixed

Round 1 — gemini APPROVE, codex REQUEST_CHANGES, claude REQUEST_CHANGES:

  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 — 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 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. Fixed with the # forge-executable: declaration plus a shell-builtin skip list; verified against the built dist.
  3. The gitea lookup had no --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:

  1. An absent body posted an empty PR (codex). The scripts validated the title but not the body, and --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_LOGIN is documented where it's read.

Round 3 — gemini APPROVE, codex APPROVE, claude APPROVE, with two cosmetic notes taken: gitea/pr-create.sh now accepts .head as either a string (tea 0.14.2) or the object-with-.ref shape sibling scripts assume, and the stale "15 concepts" counts in forge.ts and arch.md are corrected to 18. Re-verified live against Forgejo afterwards.

Test plan

  • Regression tests added; they fail without the fix
  • pnpm build clean
  • Unit suite: 4891 passed / 0 failed (see the disclosure below)
  • CLI integration suite: 90 passed
  • Tower integration suite: 173 passed, 1 pre-existing failure unrelated to this change (cli-tower-mode.e2e.test.ts expects http://localhost:… and this machine's Tower binds 0.0.0.0)
  • Live end-to-end against a real Forgejo

Disclosure: one unrelated test is timing-sensitive on my machine

spec-1280-measurement-instrument.test.ts intermittently fails here with Test timed out in 60000ms — usually 1 test, sometimes 2, always PHASE_ITERS is a linear comparison constant and/or the determinism test. Both shell out to scripts/measure-prompt-surface.sh twice.

  • That script takes 25–30 s per invocation on this machine at ~17% CPU (process-spawn bound), against the ~2.0 s recorded in codev/state/bugfix-1323_thread.md when the 60 s budgets were set. Two calls per test lands right on the budget.
  • Nothing here touches it: this branch is 0 commits behind upstream/main, and both the test file and the script are byte-identical to main — so main fails identically on this machine.
  • The whole suite passes when that file's runtime isn't competing with the rest (vitest run with 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 gitea read-concept bugs, all in #1137/#1146 territory rather than this one:

  • tea pulls view <n> --output json ignores <n> and returns a list of all pulls, so gitea/pr-view.sh's jq '.url = (.html_url // .url)' receives an array.
  • tea pulls list rejects --fields description, so gitea/pr-list.sh's description → body mapping can't populate.
  • tea pulls list --output json returns head as a plain branch string, but gitea/pr-exists.sh filters on .head.ref.

Also: gh pr edit --body-file is still hardcoded in the PIR review prompt. pr-edit isn't a concept and adding one is a separate change.

🤖 Generated with Claude Code


Update (2026-08-14): gitea pr-create no longer looks the PR up

The gitea script created the PR with tea pulls create and then searched for it with tea pulls list --limit 200. That --limit 200 was 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 at max_response_items, default 50--limit 200 does 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/api reports max_response_items: 50, and a ?limit=200 request 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}/pulls returns the created PR — number and html_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:

  1. tea api exits 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 numeric numberand a non-empty browser URL — or it fails loudly with the body. The case where number is 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.
  2. The API requires base ([Base]: Required), where tea pulls create defaulted it client-side. An unset CODEV_PR_BASE now resolves the repo's default branch explicitly.
  3. draft: true in the payload is silently ignored (response comes back draft: false), so CODEV_PR_DRAFT=1 would have been an accepted-and-ignored flag. Gitea's draft marker is a WIP: title prefix — what tea pulls create --draft does — 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

  • The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was run against the previous script first and fails there, so the pin is real — including an explicit assertion that no pulls / list / --limit call is made at all.
  • Full @cluesmith/codev unit suite on this branch: 3218 passed, 126 failed (67 files).
  • Baseline, same suite on unmodified upstream/main in 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 are agent-farm / terminal / consolidate (shellper sockets, SQLite state), environment-dependent: no built dist/, live Tower on the same state.
  • All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR closed, every scratch branch deleted. No tea token 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.

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.

pseudoseedand others added 15 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>
…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>
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

Copy link
Copy Markdown
ContributorAuthor

Reconciled with #1146 — the --limit 200 lookup is gone

tl;dr:gitea/pr-create.sh no longer looks the new PR up at all. tea api -X POST repos/{owner}/{repo}/pulls returns the created PR in its response body, so there is nothing to search, nothing to race and nothing to truncate.

The problem

This PR's 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:

tea pulls list --state open --limit 200 --fields index,url,head --output json

That --limit 200 was added here as a fix for a review defect, but it rests on an assumption #1146 disproves: Gitea caps every list response at the server's max_response_items, default 50. --limit 200 does not raise the cap — it silently truncates.

Re-confirmed live against Forgejo 15.0.2, not inferred:

  • GET settings/api{"max_response_items":50, …}
  • a ?limit=200 request returned exactly 50 items on a list where paging at 50 returned 53

So on a busy repo the just-created PR falls off the first page and this script prints

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

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

The fix: delete the lookup, don't paginate it

Routing the lookup through #1146's paginated passthrough was the obvious option. Returning the PR directly is strictly better, and it turns out to be possible: the REST create returns the full PR object, number and html_url included. That also removes the <user>:<branch> head-matching heuristic — the API resolves an owner-qualified head itself.

Three defects live testing found in the obvious version of that change

Each is the same bug class as #1455 itself — an operation accepted and then silently not performed — so each is handled in code, not noted as a caveat.

1. tea api exits 0 on HTTP errors. It prints the error body and returns 0. Since this change replaces a lookup with a single call, trusting that exit code would reintroduce #1455's silent success inside the fix for it — a 404 or 422 reported as a created PR. The response is therefore asserted to be a PR object: an object with a numeric numberand a non-empty browser URL. Anything else fails loudly with the body. Pinned by a test table feeding 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 explicitly not to retry. Reading that as "nothing happened" is how duplicates get opened.

2. The API requires base. Without it: {"message":"[Base]: Required"}. tea pulls create defaulted it client-side, so moving to REST would have broken every caller relying on that default. Silently posting against the wrong base is worse than erroring, so an unset CODEV_PR_BASE now resolves the repo's default branch explicitly and fails clearly if it cannot.

3. draft: true in the payload is silently ignored — the response comes back draft: false. 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 (tea 0.14.2 + Forgejo 15.0.2)

  • {owner}/{repo} are substituted by tea from the repo context, and --repo owner/name supplies that context when the cwd has no Gitea remote. Checked with https and scp-style Gitea remotes, and from a GitHub-remote cwd.
  • url on the create response is the browser page (unlike the GET shape, where url is the API endpoint), so .html_url // .url lands the right one in PrCreateResult.
  • The body round-trips byte-identically: built with jq --arg and fed on stdin (-d @-) rather than surviving an argv round-trip.
  • Error paths: duplicate head, missing branch, unresolvable repo — all exit 1 with a useful message.

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 #1146.

Merge order

None. This deliberately does not source #1146's _lib.sh: pr-create takes CODEV_PR_REPO (a different input from the read concepts' CODEV_REPO), and tea's own {owner}/{repo} placeholders cover it. The two PRs stay independent and can merge in either order.

Testing

The gitea half of bugfix-1455-pr-create-concept.test.ts is rewritten against a tea api stub. Every new case was checked against the previous script first and fails there, so the pin is real rather than decorative — including an explicit assertion that no pulls / list / --limit call is made at all.

All Gitea verification ran against scratch repo pseudoseed/research; every scratch PR is closed and every scratch branch deleted. No tea token scope was widened.

…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 credited finding was re-verified against the actual source, and every tea surface this PR introduces was checked against the installed released binary (0.14.1) rather than the author's verification environment.

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)

  • tea 0.14.1 compatibility — clean, unlike [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146. The gitea script's entire tea surface (tea api, -X, -d @-, --repo, --login, {owner}/{repo} placeholders, options-before-endpoint) exists on the current released tea 0.14.1, verified against the installed binary's own help output. No version floor. This was checked precisely because [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's tea comments add turned out to be 0.14.2-only.
  • The reconcile shape re-verified from scratch. The POST response carries the created PR, so there is genuinely nothing to look up, paginate, or race; the test suite pins that no pulls/list/--limit call is ever made, and the six non-PR payloads (error object, array, string-typed number, numberless object, null, empty) all fail loudly. The duplicate-retry hazard the old lookup carried is structurally gone, and the created-but-no-URL path's "do not retry" message is exactly right.
  • Merge-order independence vs [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 re-confirmed: both branches are MERGEABLE against current main, the only shared file is the byte-identical thread log, and neither script calls into the other's helpers.
  • The seam is right: KNOWN_CONCEPTS is the single derivation point (presets, doctor, config validation all follow from the one-line addition), {{pr_create_command}} mirrors the pr-merge injection precedent, and the export-not-prefix fix is correct shell semantics for the documented inline-override form.
  • Twin-tree mirroring holds: the seven prompt-file diffs are identical across codev/ and codev-skeleton/, and the .claude/.codex SKILL.md pair stays byte-identical.

Ruling on the cross-PR inconsistency flagged in the #1146 review

The two halves of the gitea preset currently disagree about tea api's exit-0-on-HTTP-error behavior. This PR's half wins. Response-shape validation (assert the payload IS the object you asked for, or fail loudly with the body) is the correct pattern, and #1146's non-paged reads should adopt the same principle — that item is already tiered on #1146's review as the maintainer's call there. New forge scripts should treat #1458's guard as the house style.

Recommended before merge (all small, all verified real)

  1. The linear preset silently falls through to gh for pr-create (Claude lane; verified in forge.ts:130): linear's disabled list has only team-activity/on-it-timestamps, there is no linear script, so resolution falls through to the GitHub default — the exact fall-through class 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 closes, at the exact write it closes it for. One line converts that into this PR's own loud "open the PR manually" fallback.
  2. Add . and source to SHELL_BUILTINS (Claude lane; independently identified during the [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146 review): sibling PR [Bugfix #1137] Fix gitea forge preset against the real tea CLI #1146's read scripts open with . "$(dirname "$0")/_lib.sh", which this heuristic would report as executable .. Two list entries here pre-empt that.
  3. The gitea default-base failure names the wrong remedy (Claude lane; verified in the script): when CODEV_PR_BASE is unset and the repo context is unresolvable, the GET answers 404 page not found, jq yields empty, and the error says "set CODEV_PR_BASE" — but the actual remedy is CODEV_PR_REPO, which the POST path's 404 message already names correctly. Match them.

Assessed non-blocking (Codex lane's REQUEST_CHANGES)

Codex flagged the .html_url // .url fallback as able to emit an API endpoint, contradicting PrCreateResult's browser-URL guarantee. Assessment: the fallback fires only when html_url is absent, which real Gitea/Forgejo Pull objects don't exhibit (the author verified the POST shape live, and the stub test pins html_url winning when both are present). Worst case on a nonstandard server is a degraded-but-identifying URL instead of a hard failure — a defensible trade. If contract purity is preferred, routing a url-only response into the existing created-but-no-URL path is a small tightening; the maintainer can take it or leave it.

Cross-PR coordination note for the maintainer

This PR's # forge-executable: <tool> declaration is the clean fix for the codev doctor regression identified on #1146 (where skipping . alone is insufficient because the next token is the tea_api_paged shell function). Whichever PR merges second should add # forge-executable: tea to #1146's five sourced scripts. With recommendation 2 above taken as well, the doctor heuristic is then robust from both directions.

Follow-ups (file after merge, none blocking)

  • pr-edit as a concept: pir/prompts/review.md still runs gh pr edit --body-file two steps after the newly routed create, so PIR on Gitea creates through tea and then hits gh. The author flags this in the PR body; it deserves its own issue.
  • Unify the two gitea repo-resolution paths (_lib.sh#gitea_repo vs --repo "$CODEV_PR_REPO") once both PRs land, absorbing repo-archive.sh's bare ${CODEV_REPO} as the third path — already proposed in the PR body, endorsed.
  • # forge-executable: could accept a list so jq is reportable as a dependency alongside tea.
  • The GitLab script is honestly marked UNVERIFIED in-file (no glab in the authoring environment); the flag mapping reads plausibly against glab's documented mr create surface, and the alternative (falling through to gh) is the bug itself. Keep it, and smoke-test it in a follow-up when a glab environment exists.
  • Pre-existing cosmetic drift, not introduced here: doctor.ts:1052/:1070 still say "all 15 concepts".

Process notes

  • The PR body's disclosure discipline (timing-sensitive unrelated test, baseline-controlled suite numbers, scratch-repo cleanup, scope deviations argued rather than slipped in) continues to be the model for community contributions.
  • The status.yaml in the diff records a pr gate approval from the author's own fork-side porch run; it carries no authority here. Merge authority rests with the maintainer — this review is the reviewing architect's recommendation, not a gate approval.

…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

Copy link
Copy Markdown
ContributorAuthor

Integration-review follow-up

All three pre-merge recommendations from @amrmelsayed's CMAP-3 review are addressed in eb7072695:

  1. The Linear preset explicitly disables pr-create, so it cannot fall through to gh.
  2. extractExecutable skips both . and source; regression coverage exercises both sourced-helper forms.
  3. Gitea default-base resolution now distinguishes an unresolvable repository (names CODEV_PR_REPO) from a resolvable repository with no default branch (names CODEV_PR_BASE).

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

Ready for upstream maintainer re-review.

pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
… 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>
pseudoseed added a commit to pseudoseed/codev that referenced this pull request Aug 21, 2026
Adopt our two stranded upstream fixes: gitea tea-api reads (cluesmith#1146) + pr-create forge concept (cluesmith#1458)

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

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 (loadPromptFile resolves 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 merge after the newly-routed create — the gh pr merge one 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 in forge.ts and arch.md).
  • SHELL_BUILTINS now also governs the inline-command branch of extractExecutable — 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

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 2 commits September 3, 2026 21:28
… 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

Copy link
Copy Markdown
Contributor

Maintainer review items pushed (4233e2835, 85a0e7819)

@pseudoseed — as promised, we took the last mile rather than hand you a list. Your commits and authorship are untouched; this is two commits on top, no rebase and no squash. Thank you for your patience through the wait, and for the disclosure discipline in the PR body — the baseline-controlled suite numbers made it genuinely easy to verify our own changes against yours.

The must-fix: Linear's pr-create (forge.ts:130-134)

Removed 'pr-create' from the Linear preset's disabled list, restoring the fall-through, and replaced the pinning test with one asserting it.

To be clear, this is us correcting our own review, not you correcting yours. Our CMAP-3 integration review asked for that disable, you implemented exactly what was asked, and the request was wrong. Spec 719's success criteria name Linear's 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": Linear owns issues, and PR concepts stay on GitHub because the repository really is on GitHub.

The blast radius was larger than cosmetic. porch/prompts.ts substitutes a hard-failing stub for a disabled concept, so a Linear user could not have opened a PR through the protocol at all — while pr-merge kept resolving to gh pr merge.

The distinction worth carrying forward, and it is genuinely subtle: fall-through is the #1455 bug only where the fallback tool cannot do the job. For Gitea and GitLab, gh cannot open the PR — silent failure. For Linear, gh is the correct tool. Same mechanism, opposite verdict, decided by the provider's nature. Your instinct to close silent fall-throughs was right; Linear is the one place it doesn't apply.

We added a class-level guard so this can't recur by a different name: the test now asserts Linear disables only those two concepts, rather than checking pr-create specifically.

One defect your SHELL_BUILTINS change surfaced (forge.ts:238-247)

Writing the test for the untested half of that change turned up a real gap. SHELL_BUILTINS governs both branches of extractExecutable, and after eb7072695 they disagreed:

branchon hitting a builtin
script-filekeeps scanning, finds gh
inline commandreturned 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 in forge.ts and arch.md.
  • doctor.test.ts — its mock was stale the same way and omitted pr-create itself, 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 the linear provider exists and is hybrid. It had shipped undocumented since spec 719; someone configuring it had no way to learn PR concepts fall through to gh.

Deliberately not done here

Both of these are yours-were-right observations that deserve their own change rather than being smuggled into this one:

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.

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

@waleedkadous
waleedkadous merged commit 70f9fd2 into cluesmith:mainSep 4, 2026
7 checks passed
waleedkadous added a commit to pseudoseed/codev that referenced this pull request Sep 4, 2026
… 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
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.

pr-create is not a forge concept: gh pr create is hardcoded in the skeleton prompts, so non-GitHub forges need a gh shim

3 participants

@pseudoseed@amrmelsayed@waleedkadous