* fix(oss-codex): prefer user's uploaded Codex auth over OSS subsidy
OSS-allowlisted repos with `CODEX_AUTH_JSON` uploaded via `pullfrog auth
codex` were still being routed through the OSS OpenRouter subsidy because
two paths ignored managed credentials:
- `hasProviderKey()` only checked `provider.envVars`, so an `openai/*`
model with only `CODEX_AUTH_JSON` present silently fell back to
`opencode/big-pickle` via `selectFallbackModelIfNeeded` — the maintainer
saw "opencode/big-pickle (resolved from openai/gpt)" on CI even though
Codex was configured.
- `run-context` set `proxyModel` for every OSS run unconditionally, which
the action runtime threads through `payload.proxyModel` and uses to
overwrite `OPENROUTER_API_KEY`. Even if `big-pickle` fallback hadn't
fired, the runner would consume the $10 OSS subsidy key instead of the
user's ChatGPT subscription.
Fix:
- Add `getModelAuthEnvVars()` covering both `envVars` and
`managedCredentials` in `action/utils/apiKeys.ts`; route
`hasProviderKey` + `validateAgentApiKey` through it.
- `run-context` now skips `proxyModel` for OSS repos when the configured
model's provider has matching auth in Pullfrog-stored account/repo
secrets, so the runner authenticates directly with the user's Codex
subscription (or any other user-provided provider auth).
Triggered by mrlubos (`hey-api/openapi-ts`). Companion follow-up tracked
for the opaque "(no error message)" classifier swallow that masked the
OSS $10 cap exhaustion on PR #3872 runs 25815370844 + 25815443234.
* fix(oss): force Kimi K2 for OSS proxy + hide picker in console UI
OSS-funded runs were resolving `repo.model` through OpenRouter, so a
single Opus / GPT-5.5 run could burn an entire `oss_subsidy` key against
the per-key cap and crash mid-stream (e.g. `hey-api/openapi-ts` PR #3872
runs `25815370844` + `25815443234`, ~$9.20 each on a single key).
Force `DEFAULT_PROXY_MODEL` (Kimi K2.6 — ~10-50× cheaper) for every OSS
proxy mint, regardless of `repo.model`. Per-run spend stays bounded
within the cap by structure, not by hope. `repo.model` stays in the DB
unchanged — overriding at runtime means leaving the program restores the
user's prior pick without a migration.
UI: hide the model picker entirely on OSS repos in `AgentSettings`. The
field is effectively inert until the repo leaves the program, so
exposing it as if it were live was misleading. Replaced with a banner
naming Kimi K2 and pointing to `pullfrog auth …` as the opt-out path —
that lands the user on the existing #844 bug-2 branch (Pullfrog-stored
auth suppresses the OSS proxy entirely; runner uses user credentials
+ their preferred model).
ModelCostsInfo already has its own `isOss` branch for the cost copy, so
that section is unchanged.
* fix(oss): lowercase comment casing per AGENTS.md
* fix(oss): revert banner copy to 'It's on us.' framing per review
Maintainer felt 'Kimi K2' as the banner headline lost the warm 'we've
got you covered' framing that the existing OSS cost banner uses. Restore
'It's on us.' as the headline, move the model name into the body where
it explains the hardcoded choice and points to the opt-out (pullfrog
auth codex / account secret).
* docs(agents): screenshots must be of the live route, never synthetic
Caught myself building a temp `/dev/oss-ui-preview` route with hardcoded
JSX copy-pasted from the real component just to grab a screenshot — the
result told us nothing about whether the actual integrated UI worked,
and the user (rightly) called it out as a waste. Strengthen the rule:
screenshots must come from the live route in the running app, driven by
the actual component tree and real props. Note the GH OAuth interstitial
gotcha so the next agent gets through Clerk → GitHub sign-in on the
first try instead of bailing to a fake render. Also bans side-by-side
comparison screenshots unless explicitly requested.
* fix(oss): one 'It's on us.' banner, not two
OSS Agent settings was showing the message twice — once in the Model
section, once in the Model costs section right below it. Fold the cost
coverage into the model banner ('at no cost to you' + the spend stat)
and hide the Model costs subsection entirely for OSS. ModelCostsInfo
no longer needs `isOss` / `ossSpendThisMonthUsd` props — call site is
gated, so the OSS branch is dead. Removed it and the now-unused props.
Non-OSS rendering is unchanged: full Model picker + Model costs
subsection with Router / BYOK branches.
* feat(action): corepack-aware package manager provisioning before setup
customer setup scripts that did `npm i -g pnpm && pnpm install` were
installing whatever pnpm "latest" happens to be on the day the run fires,
not what the repo declares — and pnpm 11.3 silently writes a new
`packageManagerDependencies` block into lockfiles, which the agent's
"always push changes" rule then packages into a noisy PR (see #844).
resolve the project's pnpm/yarn pin from `package.json` (honoring pnpm
11+ precedence: `devEngines.packageManager` over `packageManager`) and
activate it via `corepack prepare ... --activate` BEFORE the setup hook
runs. corepack is bundled with node, so this is a no-op on managed
infra; failure (no corepack, no network, range-only version) degrades
to a warning and the existing PATH binary still runs.
also replaces the legacy `npm install -g <pm>@<v>` path in prep with
the same helper so behavior is consistent end-to-end. bun/deno still
use the legacy installer because corepack doesn't ship shims for them.
* chore(console): drop 'npm i -g pnpm' anti-pattern from setup-script placeholder
the suggested example trained customers to install pnpm unpinned, which
silently picks up whatever's latest at run time. that's exactly the
behavior #844 traced lockfile drift back to. now that prep handles
package-manager provisioning via corepack from the repo's declared pin,
the placeholder is just a frozen-lockfile install — load-bearing only
when the repo wants `pnpm install` to actually run (prep already does
that), but a much safer default for customers who do paste it in.
* refactor(action): introspect opencode models for BYOK detection
Replace the static `provider.envVars + provider.managedCredentials`
catalog gate in `selectFallbackModelIfNeeded` + `validateAgentApiKey`
with two `opencode models` captures around the auth merge:
- `captureBaselineModels` BEFORE dbSecrets + Codex auth.json
- `captureAuthorizedModels` AFTER both
The authorized set is the authoritative source for "can OpenCode route
this model" — strictly more accurate than the catalog, which can miss
new auth shapes (Codex was one, there will be more). The diff between
baseline and authorized is logged as `BYOK auth enabled N model(s)`
for operator visibility.
Sequencing changes in main.ts:
- `createTempDirectory` hoisted out of the try block so
`PULLFROG_TEMP_DIR` is set before the early opencode install
- `agents.opencode.install()` + baseline capture before dbSecrets
- `installCodexAuth()` hoisted up (idempotent — agent re-calls it
inside run() and writes the same file)
- authorized capture after Codex auth.json materializes
- fallback + validateAgentApiKey receive the authorized set as a
parameter; tests inject directly with no mocks
Deleted: `hasProviderKey`, `getModelAuthEnvVars`, `knownApiKeys` in
`action/utils/apiKeys.ts` (only `selectFallbackModelIfNeeded` consumed
them, and PR #844's catalog-extension fix is superseded by introspection).
`getModelEnvVars` / `getModelManagedCredentials` stay exported for UI
and the server-side OSS proxy heuristic in run-context/route.ts.
For the claude agent path, validateAgentApiKey keeps the static
single-provider check on `ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN`
— `opencode models` is opencode-specific. validateBedrockSetup /
validateVertexSetup also stay; they cover region/location/model-id
which `opencode models` doesn't catch.
When fallback engages, the post-fallback model is the guaranteed-free
`opencode/big-pickle`, so validateAgentApiKey is skipped — the fallback
gate already authoritatively decided "this model is OK to run".
* test(oss): temp add preview-844 to ossRepos for O4 e2e
* Revert "test(oss): temp add preview-844 to ossRepos for O4 e2e"
This reverts commit 8167e560126b2ac516c32ba1c63c36aa32ae4019.
* test(oss): temp add preview-844 to ossRepos for O5 e2e
* fix(action): skip validateAgentApiKey when proxyModel is set
The new opencode-models BYOK introspection in PR #844 captures the
authorized set BEFORE runProxyResolution mints OPENROUTER_API_KEY, so
the proxy slug (e.g. `openrouter/moonshotai/kimi-k2.6`) is never in
the set. validateAgentApiKey then spuriously threw "no API key found"
on every OSS run, even though the proxy key was minted correctly and
the inference would have worked.
Mirrors the analogous skip in `selectFallbackModelIfNeeded`: when
proxyModel is set, the server-side gate (`run-context/route.ts`) is
the authority and the proxy mint itself is the validation.
Caught by O5 e2e on `pullfrog/preview-844-heyapi-oss-bug`.
* Revert "test(oss): temp add preview-844 to ossRepos for O5 e2e"
This reverts commit 3bae075ceeb188ee272c45c13b5080e15bcd00a5.
* fix(action): discard hook-generated tracked-file drift before agent sees it
addresses bug 3 in #844: customer setup/post-checkout hooks like `pnpm install`
or `corepack prepare` left the working tree dirty (e.g. `M pnpm-lock.yaml`),
the agent took the prompt's "must push" rule literally, opened a spurious bot
PR for the lockfile drift, and we ate runs+spend on noise.
after each setup / post-checkout hook (opt-in via `normalizeWorkingTreeAfter`),
discard tracked-file mods with `git restore --staged --worktree .`. untracked
files are preserved — a hook that materializes a `.env` from a template, or
emits codegen output, stays visible to the agent.
guarded by a pre-hook `git status --porcelain` snapshot: if the tree was
already dirty before the hook ran (shouldn't happen — setup runs before any
working-tree writes; checkout_pr refuses to run dirty), we warn and skip the
discard rather than clobber whatever was there.
prepush hook (action/mcp/git.ts) intentionally does NOT opt in — its job is
to read the about-to-be-pushed state, not normalize it.
* test(oss): temp add preview-844 to ossRepos for bug 3 e2e (revert before merge)
* fix(action): skip eager pnpm/npm/etc install when no lockfile exists
second half of bug 3 in #844. the eager prep step assumed `pnpm install
--frozen-lockfile` (and equivalents) would fail cleanly without a lockfile,
leaving the tree untouched. that assumption is false for pnpm 11.1.1 against
a no-deps `package.json`: the command reports "Already up to date" with exit 0
AND silently materializes an empty `pnpm-lock.yaml` despite the `--frozen-lockfile`
flag. the resulting untracked file trips the post-run dirty-tree gate, the
agent reads it as "must push uncommitted work", and a spurious
"Add pnpm lockfile" PR lands. smoking gun: pullfrog/preview-844-heyapi-oss-bug
PRs #1/#2/#3, all auto-opened by the bot against a repo that contains nothing
but a one-line README + a no-deps package.json.
guard explicitly with an `existsSync` per manager. if the lockfile is absent,
skip eager prep entirely with an info log; the agent can install on demand
via the `setup` lifecycle hook (which non-frozen `pnpm install` would handle
correctly), or just leave deps uninstalled when the prompt doesn't need them
(e.g. the O5 "tell me a joke" path).
orthogonal to the lifecycle-hook normalization in 0051bd2a — together they
cover the full bug 3 surface:
- eager prep can't materialize a lockfile (this commit)
- setup/postCheckout hooks that rewrite tracked files have the drift
discarded before the agent sees it (prior commit)
* fix(action): address Pullfrog review on hook normalization
two fixes in `executeLifecycleHook` from review on f6f3b32:
1. pre-hook snapshot was `git status --porcelain` which counts untracked
files; in practice any repo with pre-existing untracked content (e.g.
`.plans/`, an ignored-but-not-yet-gitignored scratch dir, codegen
artifacts) would trip the guard and silently skip normalization,
defeating the fix. switch to `git diff --name-only HEAD` so the gate
measures the same thing the discard targets — tracked-file mods only.
pre-existing untracked files are safe regardless because `git restore
--staged --worktree .` never touches them.
2. normalization fired only on the happy path; a hook that updated a
lockfile then exploded on a peer-dep conflict left tracked drift for
the agent. move the call into a `finally` so it runs on success,
non-zero exit, timeout, AND spawn failure. the pre-hook guard still
protects pre-existing work in every case.
* Revert "test(oss): temp add preview-844 to ossRepos for bug 3 e2e (revert before merge)"
This reverts commit f6f3b325d6bf9a1720754ed1d39d248dab76cfa8.
* fix(action): use detect lockfile strategy for eager-prep gate
addresses Pullfrog review on be3c207b. two findings, one root cause:
- the hardcoded LOCKFILE_BY_MANAGER map missed `bun.lockb` and
`npm-shrinkwrap.json`, two managers' accepted lockfile variants.
- `existsSync(join(cwd, lockfile))` only checked the immediate directory,
breaking monorepo subpackages where the lockfile lives at the workspace
root.
both fall out by replacing the custom check with `detect({ strategies:
["lockfile"] })`. the detector already walks up the tree (subpackage →
workspace root) and recognizes every accepted lockfile name across all
managers it supports. restricting to the `lockfile` strategy is load-
bearing: the default strategy set also matches on `packageManager` /
`devEngines.packageManager` package.json fields, which would return
non-null and re-mask the very case we're trying to detect (declared
manager, no lockfile committed — the O5 / hey-api preview repro).
drops the LOCKFILE_BY_MANAGER map entirely; no need for a second
detect() call since the existing one was only used for `agent`
resolution and that consumer is now after the lockfile gate, where
`detected` is guaranteed non-null.
reflection turn is a meta-turn for editing the learnings file; the
task's `result` output was already finalized on the previous turn.
gemini pro re-triggers on the standing "call set_output when done"
system instruction during reflection and clobbers the value with the
literal word "done" (see ci run 26529624199, smoke test on
providers-live google/gemini-pro). add an explicit prohibition to the
reflection prompt; the snapshot/restore in runPostRunRetryLoop
remains as defense in depth.
models occasionally call `pullfrog_git({command:"status", args:["status"]})`,
which shells out to `git status status`. git silently treats args[0] as a
pathspec — when no file/dir matches, status prints "nothing to commit,
working tree clean" even on a dirty tree. observed in production
(Skn0tt/beckerbuch run 26519563044): the agent looped trying to reconcile
that against a real diff, burned ~$3 / ~6min of opus, and only escaped when
it switched to `args: ["--porcelain"]`. generalises to every subcommand
(`diff diff`, `log log`, ...).
guard `args[0]?.toLowerCase() === command.toLowerCase()` with a directed
throw pointing the model at the disambiguated `args: ["--", "<name>"]`
escape hatch for the rare legitimate pathspec case (`--` works under every
subcommand, unlike a bare positional which can be parsed as a ref by
log/diff/checkout/restore/reset).
description also leads with the no-args case and explicitly forbids
repeating the subcommand in args. schema already had args.optional().
also includes in-flight working-tree work:
- postRun: snapshot/restore toolState.output across reflection turn so reflection prompt can't clobber task-turn output (gemini pro regression)
- toolState: widen `output` to `string | undefined` for assignability
- uninstallFeedback: suspend-mode emails now CTA the GitHub unsuspend page when accountType is known; delete events keep console pointer
* fix(askpass): scope code + script lifetime to one $git() call, not first password prompt
LFS pre-push (and any auth-bound sibling subprocess) consumed the single-use
code AND triggered the script self-delete, so git's own push call then hit
`fatal: cannot exec '/tmp/pullfrog-…/askpass-…js'` and our server treated
the legitimate retry as tamper, revoking the installation token. Observed
on nteract/nteract#2987 (LFS repo).
`gitAuthServer` codes are now `active` until `$git()`'s finally calls
`revoke()`; the script no longer self-deletes (finally already unlinks).
Replay after revoke still trips 409 + token revocation, which is the
realistic exfiltration vector we care about.
* fix(askpass): drop wall-clock TTL on active codes
Copilot review on #841 noticed the 5-minute CODE_TTL_MS still applied to
active codes, which would re-introduce the original LFS failure mode at a
different boundary: a large LFS push lasting >5min would hit a 404 mid-
call. $git() uses `activityTimeout: 0` precisely because git fetch/push
can take arbitrarily long, so any wall-clock TTL on active codes is wrong.
Active codes now live until revoke() is called (in $git()'s finally) or
the auth server is closed. Revoked codes keep their 60s replay trap.
* docs(askpass): purge stale single-use vocabulary; align error message
Pullfrog review on #841 surfaced four doc-drift sites that still described
the pre-PR single-use model:
- wiki/security.md — 3 references (overview prose, bullets, threat-mitigation table)
- action/utils/gitAuth.ts — file-level JSDoc + the 409 error message
- wiki/askpass.md — error message quoted in the tamper-evident section
- action/utils/gitAuthServer.ts — per-prompt invocation comment was ambiguous
All updated to match the active|revoked vocabulary; error message is now
"askpass code was replayed after revoke, token revoked".
* fix: 6 unaddressed log-audit / run-audit findings
- #836 + #818 (clerk middleware SyntaxError on action-runtime endpoints):
narrow proxy.ts matcher to exclude /api/repo/<owner>/<repo>/run-context,
/api/runtime/*, /api/proxy-token. these are server-to-server with their
own auth and have no Clerk session to evaluate, so clerkMiddleware's
decodeJwt throws turn into 500s on every request.
- #837 (npx EBADDEVENGINES on customer's package.json): change runCli's
bootstrap cwd from $GITHUB_WORKSPACE to os.tmpdir() so npm v11+ doesn't
enforce devEngines.packageManager from the customer's tree before our
bootstrap can install pullfrog. CLI process.chdir's to payload.cwd
internally, so the runtime work still happens in $GITHUB_WORKSPACE.
- #838 (createWorkflowDispatch silently dropped 39 user runs on a 5xx
spike): add bounded retry (3 attempts, ~750ms total) on Octokit 5xx and
network errors inside dispatchReservedRun. preserves the existing 422
"Unexpected inputs" path. retry budget stays well under GitHub's 10s
webhook redelivery window.
- #833 (bail() redirect("/signout") propagated NEXT_REDIRECT into webhook
handlers, 40+ 500s/24h): drop the redirect side-effect; bail now just
classifies bad-credentials as non-retryable and propagates. UI flows
that wanted auto-signout on revoked tokens can detect it themselves;
the side-effect was wrong for any non-page caller.
- #835 (BYOK provider billing-exhausted fell through to raw error
renderer, 37 review-mode runs/24h with no PR-side signal):
- extend providerErrors patterns with Anthropic "credit balance is too
low" + extract isProviderBillingExhausted / extractProviderId helpers
- add a renderer branch in runErrorRenderer.ts that names the provider
(parsed from providerID=) and links to its billing dashboard
- route handleAgentResult's !result.success path through the renderer
+ reportErrorToComment with createIfMissing, so review-mode and
silent triggers get an actionable PR comment regardless of mode
- #834 (post-hook ERR_MODULE_NOT_FOUND already fixed on main, hardening):
- add a vitest invariant that walks the entryPost.ts import graph and
refuses any non-relative / non-node: specifier — catches the next
`@actions/core` slip-up before publish
- add an analyze-logs classifier so future entryPost crashes surface as
failure:post-hook-module-not-found instead of hiding inside
failure:unknown / failure:git-lock-file
Co-authored-by: Cursor <cursoragent@cursor.com>
* anneal: fix dead-code matcher, 404 url, silent-trigger gate, and grammar regression
Round-1 anneal pass on the audit-fixes PR surfaced two critical issues +
several majors that the original fixes shipped with:
- proxy.ts matcher 1 (`(?!_next|[^?]*\.(...))`) still caught every
/api/... route because matcher arrays are OR'd. The narrowing in
matcher 2 was dead code → middleware still 500'd on /api/runtime/*,
/api/proxy-token, /api/repo/.../run-context. Carve-out now lives in
BOTH matchers.
- opencode.ai/billing returns 404; canonical top-up surface is /zen.
deepseek /usage is the consumption page, not the top-up flow (/top_up
is correct). google /apikey is the keys list, not billing (/usage is
the spend dashboard). All three URL strings updated.
- handleAgentResult gated reportErrorToComment behind `if (!ctx.silent)`,
contradicting the createIfMissing intent — silent IncrementalReview /
pull_request_synchronize / auto-label still got zero PR signal on BYOK
billing exhaustion, the exact failure mode #835 was meant to fix.
Moved createIfMissing into finalizeSuccessRun's existing render-and-
post block (single source of truth), reverted handleAgentResult to its
prior shape. Side benefit: drops the double-PATCH that fired on every
non-silent !success path with an existing progress comment.
- Anthropic-direct error rendered "**Your your provider account is out
of credit.**" because Anthropic SDK has no providerID= tag, so
extractProviderId returned null and the headline composed
"Your " + "your provider". Added detectProviderId Anthropic fallback
(matches "Anthropic API" / "credit balance is too low") so the link
is reachable AND made the headline conditional on whether a provider
id was detected.
- Reordered renderRunError classifier: BYOK billing-exhausted now runs
BEFORE api-key auth detection. Providers commonly return 401 for
billing exhaustion (DeepSeek, Gemini), and the OpenCode harness logs
often include "API Error: 401" in the raw error body, which
isApiKeyAuthError would otherwise match — surfacing "rotate your key"
when the actual fix is "top up credits".
- isTransientUpstreamError missed ENOTFOUND / ENETUNREACH / EHOSTUNREACH
(undici DNS-class failures Octokit doesn't wrap with a status). Added
to the prefix alternation.
- Tightened "10s webhook redelivery budget" / "GitHub redelivers"
wording in triggerWorkflow.ts and bail.ts JSDoc — GitHub's 10s is the
response timeout (it doesn't auto-redeliver); upstream webhook proxy
retries are what multiplied the failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* anneal r2: unbreak proxy.ts matcher 2, extend carve-out, mkdtemp bootstrap
Round-2 anneal (security + cross-cutting lenses) on top of the round-1
audit-fixes commit caught a critical regression + several majors:
- proxy.ts matcher 2 from r1 (`/(api|trpc)(?!...)(.*)`) does NOT compile.
path-to-regexp rejects a top-level `(?!` after `)` as "Pattern cannot
start with '?'", and Next.js's SourceSchema runs the same validator at
build time and aborts via `process.exit(1)`. PR #840's Vercel + preview
deployments have been failing since 978eca26 for exactly this reason.
Fixed by nesting the lookahead inside an outer parameter group:
`/(api|trpc)((?!...).*)`. Same shape matcher 1 already uses, which is
why m1 always compiled. Verified `pnpm next build` succeeds end-to-end.
- proxy.ts carve-out was incomplete relative to its own justification.
The "no Clerk session, decodeJwt 500" failure mode applies to ALL
server-to-server action-runtime endpoints — five more share the exact
shape: /api/repo/.../learnings, /api/repo/.../pr/.../summary-comment,
/api/repo/.../issue/.../plan-comment, /api/workflow-run/, and
/api/github/installation-token. Extended both matchers. /api/upload/
signed-url stays in (dual auth: Clerk session OR bearer JWT — needs
middleware for the user path).
- proxy.ts carve-outs were unanchored: a future /api/proxy-token-info,
/api/proxy-tokens, or /api/repo/X/Y/run-context-foo would silently
bypass Clerk. Added `(?:$|/)` for path-prefix carve-outs (allow exact
match or sub-path), `$` for routes with browser-callable siblings
(e.g. `learnings/history` is browser-Clerk, `learnings$` is action-
bearer-JWT). Verified 30/31 routes via path-to-regexp test harness.
- runCli.ts cwd flipped from $GITHUB_WORKSPACE to os.tmpdir() in r1
(#837 fix for npm v11 devEngines.packageManager EBADDEVENGINES). But
$TMPDIR is overridable from a prior $GITHUB_ENV step — a customer-
authored or compromised prior step can plant /atk/node_modules/
pullfrog/ and `echo "TMPDIR=/atk" >> $GITHUB_ENV`, and our npx
--yes pullfrog@<v> bootstrap resolves the local install first,
executing attacker code with full action env (provider keys, OIDC,
installation token, CODEX_AUTH_JSON). Switched to mkdtempSync(join
(tmpdir(), "pullfrog-bootstrap-")) — fresh per-invocation 0700 dir,
not pre-writable by anything earlier in the job.
- runLifecycle.ts: writeRunErrorOutputs (catch-path) didn't pass
createIfMissing: true, contradicting the symmetric intent of the
r1 finalizeSuccessRun fix. Silent triggers (IncrementalReview /
pull_request_synchronize / auto-label) that throw past the success
path still got zero PR signal — exact failure mode #835 was meant
to close. Now both paths pass createIfMissing: true.
- runErrorRenderer.ts detectProviderId regex `/Anthropic API|credit
balance is too low/i` could mis-tag a non-Anthropic billing-exhausted
error that mentioned "Anthropic API" in passing (fallback-chain agent
prompt text, OpenCode harness logs). Tightened to /credit balance is
too low/i — Anthropic-specific phrasing, sufficient for the direct-
Anthropic SDK case the fallback exists to handle.
- runErrorRenderer.ts JSDoc: r1's classifier reorder put hang at #6 in
code but the JSDoc still listed it at #2. Reordered the doc to match
dispatch order, with explicit note that hang is a sub-source for the
api-key check (which is why hangBody is precomputed early).
- analyze-logs.ts: ERR_MODULE_NOT_FOUND.*entryPost regex needs `s`
flag so it survives Node v23+ stack-trace reformatting onto multiple
lines. One-char fix to defend the #834 classification bucket.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
- utils/ossRepos.ts: add middleapi/orpc (orpc.dev, 5.2k stars) to the
oss allowlist so any future install mints via mintOssKey ($10 cap,
pullfrog absorbs).
- scripts/skill.ts + pnpm skill: scaffold .agents/skills/<name>/SKILL.md
+ .claude/skills/<name> symlink. patch + skill skills written using it.
- AGENTS.md: hard-ban vi.* mocking apis; document pnpm skill workflow.
- audit follow-through: drop pure-mock test files (action/utils/lifecycle,
action/utils/timer, test/handleNoInstall) and trim action/mcp/review to
the non-mock cases.
- wiki/scripts.md: row for scripts/skill.ts.
c45a07e5 swapped the opencode harness default to moonshotai/kimi-k2,
which broke push-enabled (Kimi reported delete_branch as "auth failed"
where sonnet handles it). The agnostic test matrix expects Sonnet-grade
tool-calling; switching to Kimi was scope creep on top of the original
"don't default to opus" ask — opencode's default was already sonnet, not
opus, so nothing needed to change there.
* expose push: restricted to users
the action runtime already understood `disabled | restricted | enabled`
for `push` (see action/external.ts, action/mcp/git.ts); the DB enum, the
zod schema, the React settings UI, and the action.yml input docs lagged
behind. catches all four up. UI surfaces a 2-state toggle (restricted
↔ enabled) in the Security card alongside shell isolation — `disabled`
stays in the enum so non-UI callers (action.yml input) still accept it
but is intentionally hidden from the console.
* address reviews: doc full restricted scope + disabled UI edge case
reviewers flagged that "restricted" blocks more than default-branch
pushes (also delete_branch + push_tags per action/mcp/git.ts:621-700),
and that the popover's "off = full push access" claim is wrong for
repos whose workflow sets `push: disabled` (also renders as OFF since
the toggle is `props.push === "restricted"`). action.yml description
now enumerates the full restricted scope, and the popover frames off
behavior in terms of the workflow input so disabled repos aren't
misrepresented.