76 Commits

Author SHA1 Message Date
Colin McDonnell e52206b8ca test: switch opus defaults to sonnet/kimi for cheaper coverage
- opencode test default: claude-sonnet-4-6 → moonshotai/kimi-k2. claude-code
  default stays on sonnet (the agent-under-test for that path is Claude
  Code); the opencode path doesn't need an Anthropic model.
- byok-no-keys-fallback fixture: anthropic/claude-opus → moonshotai/kimi-k2.
  test verifies fallback to opencode/big-pickle, so the configured model
  is never invoked; picking a non-opus alias avoids burning credits if
  the fallback ever regresses.
- BEDROCK_MODEL_ID in both test workflows: us.anthropic.claude-opus-4-6-v1
  → us.anthropic.claude-sonnet-4-6. env var is required by the ci.test
  invariant that every provider's env vars are wired into the workflow.
2026-05-23 01:07:20 +00:00
Colin McDonnell 01e4daa0b5 checkout_pr: refuse unconditionally on dirty working tree (#808)
* checkout_pr: refuse unconditionally on dirty working tree

drop the live-HEAD comparison from the guard introduced in #796. any
checkout_pr call with staged or unstaged changes now throws, even when
HEAD is already on pr-N. no stashing, no idempotent escape hatch.

motivation is the zed-industries/cloud (2026-05-18) incident: shared-cwd
subagents make "carry edits along" semantics dangerous, and the
HEAD-equality predicate let a re-checkout silently inherit working-tree
state from a sibling agent. forcing commit/discard before any
PR-context operation eliminates the entire carry-forward failure class.

error names the PR number, lists dirty paths, and tells the agent to
commit/push/restore/clean before retrying.

* improve dirty-tree error: precise discard commands

copilot caught two sloppy bits in the error string:
- "push" alone does not clean a dirty tree (needs commit first)
- bare `git clean` is a no-op without `-fd`

reword to "commit (then push if needed), or discard with
`git restore --staged --worktree .` / `git clean -fd`" so the
guidance is actually actionable.

* checkout_pr: initial-branch invariant

setupGit captures `toolState.initialBranch` at run start via live
`git rev-parse --abbrev-ref HEAD`. checkout_pr refuses unless current
HEAD matches the run-entry branch or the target `pr-N` (idempotent
same-PR re-checkout). uses live rev-parse, not toolState.issueNumber
(poisonable per the PR #796 review).

refusal error names the current branch, target PR, recovery path
(`git checkout <initialBranch>` with the literal branch name), and
explicitly states routing around via the `git` tool is not sanctioned.

closes the zed-industries/cloud (2026-05-18) shape where a subagent
parked HEAD on someone else's `pr-X` and the orchestrator's next
checkout_pr inherited that position.

* reviewfrog: enforce canonical diff + pre-commit halt; align Build dispatch

extend REVIEWER_SYSTEM_PROMPT with two prepended HARD CONSTRAINTS:
- first action MUST be `git diff origin/<base>` (single-rev, captures
  uncommitted). no other diff first; no checkout_pr; no alt-ref fetches;
  no branch listing; no `gh pr list`.
- empty canonical diff + claimed-changes dispatch ⇒ reply exactly with
  `no changes detected — likely pre-commit Build self-review;
  orchestrator should commit then re-dispatch` and stop. do not guess
  PR numbers (the zed thrash that ended in `checkout_pr({2582})`).

reshape Build mode reviewfrog dispatch step around a verbatim template
that names: (a) the situation is pre-commit, (b) canonical diff command,
(c) halt-on-empty-diff rule. orchestrator side now says the same thing
as the reviewer's baked-in prompt. delegation-discipline bullets and
orchestrator-evaluation guidance kept intact.

* checkout_pr: handle detached-HEAD entry in initial-branch invariant

pullfrog incremental review caught a defense-in-depth gap: `git
rev-parse --abbrev-ref HEAD` returns the sentinel string `"HEAD"` on
detached entry, which is the default `actions/checkout` state for
`pull_request` events. with the previous string-typed `initialBranch`,
both the captured value and the live probe would equal `"HEAD"` on
any detached state, trivially satisfying the invariant — including a
subagent doing `git checkout --detach <sha>`.

discriminate the captured HEAD: probe `git symbolic-ref --short HEAD`
first (works on named branches), fall back to `git rev-parse HEAD`
(SHA) on detached entry. store as
`{ kind: "branch"; name } | { kind: "detached"; sha }`. checkout_pr
runs the identical probe at call time and compares like-with-like
(branch name vs branch name, SHA vs SHA).

refusal error renders both heads via a small `describeHead` helper and
chooses the right `git checkout` recovery target (branch name or SHA).
no inline-discriminant `as` casts — uses a top-level `headsEqual` that
narrows via the discriminator.
2026-05-22 22:38:57 +00:00
Colin McDonnell c43ed65c3b Add Vertex AI routing support (#753)
* add Vertex AI routing support

* include Vertex smokes in action CI
2026-05-20 16:49:08 +00:00
Colin McDonnell a0576a702a opencode v2: harness adapted to opencode-ai 1.15+ SDK-v2 / Effect-ts CLI rewrite (#767)
* opencode v2: harness adapted to opencode-ai 1.15+ SDK-v2 / Effect-ts CLI rewrite

Bumps `opencode-ai` from `1.1.56` → `1.15.1` and ports the harness to the
v2 NDJSON event contract. The legacy `opencode.ts` is kept as reference;
`opencode_v2.ts` is the active runner via `agents/index.ts`.

Why: `1.1.56` doesn't echo Gemini `thought_signature` back through the
MCP tool-call serializer, so direct-Google reviews 400 on the 3rd-ish
tool call. The fix only exists in the `1.14.x`+ line, which also ships
the SDK-v2 / Effect-ts CLI rewrite — taking the rewrite is mandatory.
Also unblocks the Codex ChatGPT-subscription auth path.

Surface area:

  - drop `init` / `message` / `result` / `tool_result` event types and
    handlers (no longer emitted at v1.14+ per upstream
    `cli/cmd/run.ts:588-601`).
  - `tool_use` is now a single event covering both `state.status:
    "completed"` and `"error"`. duration / subagent-finish bookkeeping
    moves from the v1 `tool_result` handler into the consolidated
    `tool_use` handler.
  - new `reasoning` event handler — gated on `--thinking`, surfaces
    Gemini-3 / OpenAI / Anthropic thinking blocks. `--thinking` added to
    `baseArgs`.
  - drop `pendingTaskDispatches` FIFO + `knownNonTaskCallIDs` set: at
    v1.15 the `task` tool callID is stable across the whole
    `tool-input-* → tool-call → tool-result/tool-error` chain
    (`session/processor.ts:282-330`). exact-match map is sufficient.
  - drop `experimental.batch_tool: true` from injected config — declared
    but inert at v1.15. re-add once upstream wires it back.
  - bin path: `bin/opencode` → `bin/opencode.exe` (postinstall renames
    the platform-specific binary into `opencode.exe` for every OS now).

Validated locally:

  - `pnpm test` 610/610 ✓
  - `pnpm play --raw` end-to-end with Anthropic via OpenRouter ✓
  - `pnpm play --raw` with `google/gemini-3.1-pro-preview`: 6 tool calls,
    multiple reasoning blocks visible, `set_output` propagates, exit 0 ✓
    (this is the headline `thought_signature` fix)
  - runtest opencode: smoke ✓, restricted ✓, nobash ✓, token-exfil ✓
  - runtest opencode: skill-invoke and mcpmerge fail (model-behavior
    drift on the new system prompt; wiring confirmed intact via direct
    repro showing both `robinMCP` and `pullfrog` MCP tools exposed).
    Tracked for follow-up; does not gate the migration.

Plugin (`opencodePlugin.ts`) and skill discovery paths are unchanged at
v1.15 — verified upstream and reused as-is. Bus subscription via
`bus.subscribeAll()` and the `event` hook still fan out every payload.

* model-smoke: bump opencode bin path to opencode.exe (v1.14+ rename)

The v1.14+ postinstall.mjs renames the platform-specific binary to
`bin/opencode.exe` for every OS (incl. linux/darwin), not just Windows.
Mirrors the fix in action/agents/opencode_v2.ts.

* opencode v2: set PWD env explicitly to fix skill / project-config discovery

Root cause for skill-invoke + mcpmerge harness regressions: opencode-ai 1.15
reads `process.env.PWD` first (with `process.cwd()` as fallback) when
resolving the SDK client's `directory` parameter — see upstream
`cli/cmd/run.ts:282`:

  const root = Filesystem.resolve(process.env.PWD ?? process.cwd())

We pass `cwd: repoDir` to spawn, but the child inherits the harness's PWD
via `...process.env`. Under `pnpm runtest` (and `pnpm play`) PWD is the
`action/` directory, not the cloned test repo. Result: opencode creates
two instances per session — one at `process.cwd()` (correct) and one at
`PWD` (wrong) — and the agent's session runs in the PWD-derived one,
which can't see the project's `.opencode/skills/` or `.claude/skills/`.

Empirically traced via the full opencode stderr trace under the runtest
harness: `service=skill count=3 init` (no `pullfrog-skill-check`) plus a
second `service=default directory=<harness-pwd> creating instance` line
per run. With `PWD=repoDir` set explicitly, `count=4 init` includes the
test skill, the agent reaches for `skill({"name":"pullfrog-skill-check"})`
exactly as the validator expects, and mcpmerge's `robinMCP_get_test_value`
becomes accessible too.

Validated locally: skill-invoke-opencode ✓, mcpmerge-opencode ✓, smoke ✓,
restricted ✓, nobash ✓, token-exfil ✓ (flaked once on a model-narration
match, passes on retry; unrelated to PWD).

* opencode v2: drop ThinkingTimer; use opencode's reasoning.part.time directly

opencode-ai 1.15 emits `reasoning` parts with `time.start` / `time.end`
on terminal state (`cli/cmd/run.ts:671`), giving us a precise per-block
"thought for X s" duration straight from the runtime. The v1
ThinkingTimer heuristic — measuring wall-clock between markToolResult
and the next markToolCall — was an approximation when no native source
existed; with v2 it's redundant and noisy (it would log alongside the
real reasoning event, and conflated network latency with model thinking).

Removed: `ThinkingTimer` import, `thinkingTimers` Map, `timerFor()`
helper, both `markToolCall` / `markToolResult` call sites in `tool_use`.
The `reasoning` handler now reads `part.time.start/end` directly and
prefixes the visible preview with `(X.Ys)`.

Output before: `» thinking: <preview>` + `» thought for 4.0s` (separate)
Output now:    `» thinking (4.0s): <preview>` (one line, sourced)

For models that don't emit reasoning (Sonnet without extended thinking,
GPT-4o, etc.), there's just no thinking line — which matches reality
better than the gap-heuristic, which would fire on any pause >3s
including provider-side latency that wasn't actual model reasoning.

Validated locally: skill-invoke ✓, mcpmerge ✓, smoke ✓, Gemini play
shows `» thinking (4.0s)` and `» thinking (0.8s)` from real durations.

* claude.ts: same PWD fix as opencode v2; entryPost: refresh stale comment

claude-code 2.1.x reads `process.env.PWD` and registers it as a "session"
additional-working-directory when it differs from `process.cwd()` (per the
bundled cli.js: `let H = process.env.PWD; if (H && H !== Y7() && ...)
j.set(H, { path: H, source: "session" })`). Without overriding PWD on the
spawn env, claude inherits the harness's PWD via `...process.env` — under
`pnpm runtest` / `pnpm play` that's `action/`, not the cloned test repo —
and adds the wrong dir to the agent's allowed working set.

Symmetric to the opencode v2 fix in 52337f9. Pre-empts the same class of
"agent's session sees the wrong cwd" failures on the claude side.

Also refresh the stale `action/agents/opencode.ts` reference in
entryPost.ts to point at opencode_v2.ts (the active runner), with the v1
file noted as kept-for-reference.

* opencode: extract shared helpers into opencodeShared.ts; v2 cleanup

Code-quality pass on the v2 work:

1. New `agents/opencodeShared.ts` (144 lines) for genuinely-shared helpers
   between v1 and v2:
   - `OpenCodeConfig` type
   - `geminiHighThinkingOverrides()` (registry-driven Gemini thinking pin)
   - `buildReviewerAgentConfig()` (reviewfrog config builder, was in v1
     and re-imported by v2 via a back-reference)
   - `installOpencodeCli({ binPath })` (parameterized — v1 passes
     `bin/opencode`, v2 passes `bin/opencode.exe` via a per-version
     `installCli` lambda; matches each pinned version's npm shape)
   - `autoSelectModel()` + `getOpenCodeModels()` model-registry fallback

   v2 drops the `import { ... } from "./opencode.ts"` back-reference; v1
   keeps a one-line `export { geminiHighThinkingOverrides }` re-export
   so `opencode.test.ts` keeps working unchanged. Once v1 is retired
   (post burn-in) opencodeShared collapses back into v2.

2. `opencode_v2.ts` cleanup:
   - drop dead state (`currentStepId`, `stepHistory` were write-only —
     their reader was the v1 `tool_result` handler we deleted)
   - hoist `state` in `tool_use` handler; replace nested-ternary payload
     extraction with a `terminalPayload(state)` helper
   - extract `formatPartDuration(time)` for the reasoning-block
     "(X.Ys)" suffix
   - tighten `OpenCodeBusEnvelopeEvent` type to include `tool` /
     `callID` fields directly, drop the `partWithToolFields` cast
   - trim docblocks per AGENTS.md "≤ 2-3 lines per code line": reasoning
     handler, tool_use handler, bus envelope handler all shortened
   - `step_start` becomes an explicit `() => {}` no-op so the dispatcher
     doesn't log "unhandled event" for every step

3. `subagentRegistration.test.ts` retargeted at the new file split —
   reads opencodeShared.ts for the buildReviewerAgentConfig assertions
   and opencode_v2.ts for the orchestrator-model wire-through.

Net: -306 source lines (1339+1130 → 1228+1031+144). Tests + lint + format
+ typecheck all green; skill-invoke-opencode ✓ and smoke ✓ verified
against the refactored v2 runtime.

* opencode v2: address PR review feedback

Three fixes from the inline review threads on #767:

1. Activity-diagnostic ordering bug (Copilot review at L705): the chunk-
   level `markActivity()` resets the module-level idle counter, so the
   per-event `getIdleMs()` sample inside the dispatch loop was always
   ~0ms — the "no activity for Xs" diagnostic never fired. Replaced with
   a runner-local `lastEventAt` so we measure real event-to-event silence
   instead of chunk-arrival latency. Drop the unused `getIdleMs` import.

2. TDZ-defensive hoist (Pullfrog review nit): `agentErrorEvent`,
   `lastProviderError`, and `recentStderr` are closed over by the
   `handlers` const but were declared after it. No current bug because
   handlers only fire inside the awaited `spawn()`, but a future
   refactor that triggers a handler synchronously during setup would
   surface a TDZ. Hoisted above `handlers`.

3. `step_finish.part.tokens.reasoning` follow-up (Pullfrog review at
   L566): leave a `TODO` comment marking the gap until `AgentUsage`
   grows a `reasoningTokens` field — separate PR with schema work.
   Cost totals stay correct because `part.cost` is summed independently.

Other thread states for the record:
- Copilot L63 (geminiHighThinkingOverrides import from legacy): already
  fixed by the opencodeShared.ts extraction in 83a7cab.
- Copilot L672 (ThinkingTimer over-reports on terminal events): already
  fixed by dropping ThinkingTimer in a1e536b — we use opencode's own
  `reasoning.part.time.{start,end}` for thinking durations now.
- Pullfrog L642 (onToolUse double-fire on subagent dispatch): re-checked
  the bus-envelope flow; the plugin filters orchestrator events except
  for status=running task dispatches, and bus-envelope returns before
  calling handlers.tool_use on those. No double-fire under current code.

Validated: 610/610 unit tests, lint + format + typecheck clean,
skill-invoke-opencode ✓.

* DX: flip pnpm play / pnpm runtest to docker-by-default

Restores the script shape wiki/docker.md has documented since the docker
rewrite (#750). PR #756 inadvertently reverted action/package.json's
gha/play/runtest scripts to host-only and dropped the :local variants;
the wiki kept the new shape, so docs and reality drifted. The OpenCode-v2
migration agent ran `pnpm play --raw …` host-side throughout because the
host entry was the only thing that existed.

scripts (root → action):
- pnpm play       → pnpm -C action gha play.ts          (docker, default)
- pnpm play:local → pnpm -C action play:local           (host)
- pnpm runtest    → pnpm -C action gha test/run.ts      (docker, default)
- pnpm runtest:local → pnpm -C action runtest:local     (host)
- pnpm gha is restored in action/package.json (re-adds `node gha.ts`)

action/package.json deliberately ships only the :local variants — bare
`pnpm -C action play` now errors instead of silently bypassing docker.
This is a tradeoff per the user prompt's "consider whether NAMES should
change" hint: the explicit error is worth the small CI churn.

CI workflows: `.github/workflows/test.yml` and
`action/.github/workflows/test.yml` flipped from `pnpm runtest …` to
`pnpm runtest:local …`. Semantics unchanged — they still execute
`node test/run.ts` directly on the GHA Linux runner; nesting docker on
GHA is unnecessary overhead. Only the script name changed to match the
new package.json.

Webhook tester: the existing root `pnpm play` was actually a webhook
handler smoke harness (root play.ts), unrelated to the action runtime.
Renamed root play.ts → webhook.ts and exposed it as `pnpm webhook` so
`pnpm play` can carry the docker-by-default action shortcut without
collision. README updated.

File headers updated:
- action/play.ts: invocation block now points at `pnpm play` /
  `pnpm play:local`
- action/test/run.ts: same
- action/gha.ts: usage block calls out the new shortcut wrappers

AGENTS.md: extended the existing "local sanity checks of action tool
logic" rule with the play / play:local / runtest / runtest:local
selection guidance and the `cd action; pnpm play` footgun note.

wiki/docker.md unchanged — already described the now-real shape.

* test/crossagent: add codex-auth smoke

Pins openai/gpt-5.5 (in opencode's Codex ALLOWED_MODELS) and runs the
full opencode harness against the env-provided CODEX_AUTH_JSON. Verifies:

  - installCodexAuth() materializes auth.json under the test HOME
  - opencode routes openai requests through ChatGPT subscription auth
    (no OPENAI_API_KEY in env, AT path forced via expires: 0)
  - the refresh chain advances during the run (refresh_token rotates)
  - detectCodexRefresh() would surface the rotation to entryPost.ts

The post-hook write-back fetch isn't reachable from `pnpm runtest`
(it's a separate GHA `post:` step). The integration boundary that
matters end-to-end is "did the on-disk auth.json change in a way
detectCodexRefresh recognizes" — that's exactly what this test asserts.

CI wiring (already committed in a1c1fd4f as part of the DX flip):
  - .github/workflows/test.yml: CODEX_AUTH_JSON via secrets in
    action-agents env block
  - action/.github/workflows/test.yml: same; codex-auth in the
    hardcoded test matrix with a claude exclude

The provisioning step on the user's side is `gh secret set
CODEX_AUTH_JSON --repo pullfrog/app < auth.json`.

ci.test.ts: expectedAgentEnvVars now includes provider
`managedCredentials` so the "env vars cover all provider API keys"
invariant stays self-correcting as more managed credentials land.

* docs(codex-auth): make storage requirement unmissable

A previous reviewing agent on this branch came away thinking
`CODEX_AUTH_JSON` could live in GitHub Actions secrets. It can't —
`entryPost.ts` rewrites the rotated refresh token after every run, and GH
Actions secrets are immutable at runtime, so any non-Pullfrog-Postgres
storage breaks the refresh chain on the first rotation (~1h silent
expiry).

- wiki/codex-auth.md: prominent `[!IMPORTANT]` callout above the fold,
  with the words "GitHub Actions secrets DO NOT WORK" verbatim and an
  enumeration of broken alternatives.
- action/utils/codexHome.ts + action/entryPost.ts: header comments now
  loudly contrast Pullfrog secret store vs GH Actions and explain the
  writeback constraint.
- AGENTS.md: terse one-bullet rule next to the model-resolution rule so
  future agents don't repeat the mistake.
- .github/workflows/test.yml + action/.github/workflows/test.yml: added a
  comment marking the existing `secrets.CODEX_AUTH_JSON` injection as a
  CI smoke-testing shortcut, not the canonical pattern. CI wiring itself
  unchanged per scope.

* auth codex: auto-open device URL, drop --scope flag

- detect `https://auth.openai.com/codex/device...` from codex CLI output
  and best-effort launch it in the user's default browser (open / xdg-open
  / cmd start, wslview fallback on linux). gated so we only open once per
  flow; failures are swallowed so manual copy-paste still works.
- drop the `--scope` flag entirely. the device-code flow is fundamentally
  interactive (browser approval), so a "skip-the-prompt" flag for just one
  of the prompts was dead weight. collapses scope selection to "always
  prompt on org-owned, always account on user-owned".

* rename gha→docker, flip play/runtest defaults to host

the previous shape conflated "real GitHub Actions" with the local docker
container that mocks it, and made the slow docker path the default for
fast-iteration scripts.

- `action/gha.ts` → `action/docker.ts` (banner, --doctor, --help, image
  tag `pullfrog-docker:*`, volume `pullfrog-docker-node-modules-*`,
  tmpdir, error messages)
- `pnpm play` / `pnpm runtest` now default to host (fast iteration);
  `pnpm play:docker` / `pnpm runtest:docker` run inside the container
- `pnpm gha` → `pnpm docker` (the container runner shortcut)
- `pnpm webhook` → `pnpm play:webhook` (fits the play: namespace; the
  bare name implied a webhook server, which hookdeck-cli already is)
- update docs (`wiki/{docker,action-tests,billing,adversarial,browser}.md`,
  `README.md`, `AGENTS.md`), CI workflows
  (`.github/workflows/test.yml`, `action/.github/workflows/test.yml`),
  and code headers (`action/{play,test/run,utils/runFixture}.ts`,
  `webhook.ts`, `action/test/coverage.ts`)

`action/commands/gha.ts` keeps its name — it's the real GitHub Actions
entry point for the `pullfrog gha` CLI command (not the docker mock).

* fix(codex): route post-hook writeback through apiFetch + conditional skip

Three threads addressing PR #767 followups.

action/entryPost.ts: replace raw fetch() with apiFetch() so the
PUT /api/runtime/secret call carries the x-vercel-protection-bypass
header/query when targeting a preview deployment. raw fetch silently
401s against the Vercel SSO gate, so every preview-env Codex run was
losing its rotated refresh token. production is unaffected (no SSO).

action/test/crossagent/codexAuth.ts: gate the test on CODEX_AUTH_JSON
via new TestRunnerOptions.skipIf hook. when the secret is absent
(forks, contributors without it), runTestForAgent short-circuits to a
passing-with-skipped ValidationResult before any agent spawn — so the
matrix's fail-fast: true setting doesn't cascade-cancel siblings. CI
on pullfrog/app and dev-local with .env both still run the test for
real. printSingleValidation/printResults now render skipped entries
distinctly.

doc/comment drift:
- docs/codex-auth.mdx, wiki/codex-auth.md: drop stale --scope flag
  mention (removed in 10be96db, scope is now always interactively
  prompted or implicit).
- wiki/codex-auth.md: tighten Claude-defense wording — materialization
  is agent-gated (opencode/opencode_v2 harness), not model-gated;
  opencode runs with non-OpenAI models still materialize the file,
  it's just not read.
- action/Dockerfile, action/docker-entrypoint.sh: pnpm gha / gha.ts
  → pnpm docker / docker.ts (renamed in a2a63929).
- app/api/runtime/secret/route.ts: refer to the save-time scope prompt
  instead of the dropped --scope flag.

* smoke: force ≥2 tool calls; document test-bar in wiki + AGENTS

upgrade crossagent/smoke prompt to call pullfrog_git status before
set_output. this exercises the 2nd model→agent round-trip across every
providers-live flagship, catching bugs like the Gemini thought_signature
echo that single-tool-call tests can't see.

also adds the "bar for adding new LLM-driven tests" section to
wiki/action-tests.md and an extension to the existing AGENTS.md
no-tests rule pointing at it — prefer upgrading existing matrix entries
over adding new ones.

local: pnpm runtest smoke opencode passes against both
anthropic/claude-sonnet-4-6 and google/gemini-pro.

---------

Co-authored-by: Colin McDonnell <colinmcd94@M1chelle.local>
2026-05-20 04:05:16 +00:00
Colin McDonnell f3d18401ac eager signup credit + free-OpenCode fallback when BYOK has no key (#789)
* eager signup credit + free-OpenCode fallback when BYOK has no key

addresses the silent-churn pattern that took out 15 first-run-failure
accounts post-launch: GH Actions secret references resolved to empty
strings (because the secrets didn't exist on the repo), the action
launched Claude Code with no key, the LLM provider 401'd, and the run
died in seconds with a synthetic "Invalid API key" message. those
accounts had no Router credits to fall back to because the lazy claim
required a dashboard visit they never made.

three changes, one PR:

1. Eager $10 signup credit at account creation. Both account-creation
   sites (`upsertAccountByClerkId` for dashboard signin, `fetchOrCreateRepo`
   for CLI / GH-App-only) now insert the `CreditGrant { reason: "signup" }`
   in the same transaction as the `accounts` row. CLI installers who
   never sign in get the credit. The dashboard `/signup-credit/claim`
   POST stays as an idempotent backstop for accounts created before
   this shipped.

2. Free-OpenCode fallback in the action. When the configured BYOK slug
   needs a provider key the runner doesn't have, swap to
   `opencode/minimax-m2.5-free` before agent selection so the run still
   succeeds. Surfaced via a `» fell back from <slug> to <free>` warning
   in the action log. Skipped on Router runs (Pullfrog mints the key)
   and when no model is configured (auto-select-with-throw still fires
   for the genuinely-misconfigured case).

3. New action-test fixture `byok-no-keys-fallback` that empty-strings
   every known provider key (matching how GH Actions handles missing
   secrets) and asserts the run succeeds with the fallback log line
   present. plus a unit test for the helper covering each skip case.

skipping the schema flip from `byok` to `router` — that's coming via
the onboarding-stepper PR (#762).

* fallback: skip Bedrock + surface in PR-comment footer

addresses copilot review on #789 (real bug — parseModel throws on
Bedrock raw IDs that have no slash, would crash before
validateBedrockSetup could surface its own error) and the user-side
ask to make the fallback visible in PR comments.

- selectFallbackModelIfNeeded skips when resolvedModel has no '/' so
  Bedrock routing IDs (e.g. us.anthropic.claude-opus-4-7) don't crash
  inside hasProviderKey -> parseModel. unit test covers it.
- toolState.modelFallback records the configured slug we fell back
  from. set in main.ts when fallback engages.
- buildPullfrogFooter accepts fallbackFrom and renders
  "Using `MiniMax M2.5` (free) (credentials for Claude Opus not
  configured)" so the substitution is visible in PR comments,
  reviews, PR bodies, and error reports.
- threaded through all four action-side footer call sites
  (mcp/comment, mcp/pr, mcp/review, utils/errorReport). server-side
  call sites in triggerWorkflow.ts / handleWorkflowRunWebhook.ts
  fire pre-action and don't have toolState — left as-is.

* fallback footer: use provider display name + document email asymmetry

addresses pullfrog reviewer findings on #789:

- footer now shows 'credentials for Anthropic not configured' (provider
  display name from `providers.anthropic.displayName`) instead of the
  per-model name. credentials are provider-scoped (ANTHROPIC_API_KEY
  covers all Anthropic models), so this matches what the user actually
  needs to fix.

- document the intentional asymmetry between eager and lazy signup
  credit paths: eager skips both the signupCreditClaimedEmail and the
  per-grant team@ alert. comment explains why (the 'new account
  created' alert already covers it on the eager path; the user-facing
  email assumes a user-initiated action that hasn't happened yet for
  CLI/GH-App-only signups).

- skipping the backfill for the 15 historical accounts per user's
  earlier decision — they all uninstalled, so the cohort self-selected
  out of being reachable.

* fallback: gate on resolvedModel + skip resolveModel re-resolve post-swap

local agnostic fixture run surfaced two real bugs the unit tests didn't
catch:

1. fallback gate was on configuredSlug (=payload.model) but the test
   uses PULLFROG_MODEL to set the model, which is read by resolveModel
   AFTER its slug arg. configuredSlug stayed undefined → fallback never
   fired. drop configuredSlug from the helper signature; gate purely on
   resolvedModel since that's the same value regardless of how the
   model was specified (DB config vs PULLFROG_MODEL env).

2. when fallback engaged, the post-swap resolveModel({slug: fallback.to})
   call was ALSO honoring PULLFROG_MODEL, re-overriding the fallback
   target back to the unkeyed model. validateAgentApiKey then threw
   "no API key found" against the original model. fix: skip the
   re-resolve. fallback.to is already a CLI-ready specifier.

unit tests updated for the new helper signature (8 tests, all pass).
fallback log line confirmed emitted in the local run pre-second-fix;
the second fix unblocks the validation that previously threw.
2026-05-20 02:17:22 +00:00
Colin McDonnell 8f9208bd3f feat: Amazon Bedrock support via routing slug (#720)
* add Amazon Bedrock as a routing slug

introduces a single `bedrock/byok` catalog entry that the harness translates
to the appropriate Bedrock model ID at run time via `BEDROCK_MODEL_ID`. routes
Anthropic IDs through claude-code (with `CLAUDE_CODE_USE_BEDROCK=1`) and
everything else through opencode's `amazon-bedrock` provider — keeps the
catalog flat for an audience that needs version pinning rather than aliasing.

accepts either `AWS_BEARER_TOKEN_BEDROCK` or `AWS_ACCESS_KEY_ID` +
`AWS_SECRET_ACCESS_KEY` for auth; both validated alongside `AWS_REGION` and
`BEDROCK_MODEL_ID` in `validateAgentApiKey`. catalog drift tests, the bumps
cron, and per-alias smoke scripts all skip routing slugs since there's no
fixed `resolve` to validate.

docs/bedrock.mdx walks through setup; wiki/model-resolution.md has a section
explaining why bedrock breaks the usual alias pattern.

closes pullfrog/pullfrog#40

* ci: add bedrock env vars to test workflows

mirrors the new bedrock provider's required env vars (AWS_BEARER_TOKEN_BEDROCK
inherited from org secret + AWS_REGION + BEDROCK_MODEL_ID hardcoded) into both
.github/workflows/test.yml files so the ci.test "env vars cover all provider
API keys" assertion passes.

* docs(bedrock): clearer setup flow + screenshot of model selector

restructures the setup section into three concrete steps in execution order:
select Bedrock from the dropdown, store the bearer token as a secret (Pullfrog
or GitHub — links to keys.mdx for the trade-off), then add region + model id
directly in pullfrog.yml since neither is sensitive. enable-model-access in
the Bedrock console moved to step 4 (only required once per model and only
when AWS rejects the call, not blocking on first run).

adds a screenshot of the console model selector with Amazon Bedrock selected
so readers can recognize the UI state they're aiming for.

* fix(bedrock): tolerate raw Bedrock model IDs in validateAgentApiKey

main.ts passes the resolved model into validateAgentApiKey
(`payload.proxyModel ?? resolvedModel ?? payload.model`). For Bedrock,
`resolveModel` translates `bedrock/byok` into the raw AWS model ID
(e.g. `us.anthropic.claude-opus-4-6-v1`), which has no `/` and so
trips parseModel inside getModelEnvVars.

Detect the no-slash case and re-run the bedrock setup check (auth +
region; BEDROCK_MODEL_ID is already enforced upstream by resolveModel).

Caught by PR #720 e2e dispatch on pullfrog/preview-720-bedrock —
"invalid model slug 'us.anthropic.claude-opus-4-6-v1' — expected
'provider/model'". Two regression tests cover the raw-ID path.

* fix(bedrock): always prepend amazon-bedrock/ prefix when bedrock-routed

opencode.ts was gating the prefix-injection on `!isBedrockAnthropicId(rawModel)`,
on the theory that Anthropic Bedrock IDs always go through claude-code. But
`PULLFROG_AGENT=opencode` is a documented escape hatch — when it forces
opencode for an Anthropic Bedrock model, the prefix still has to be added or
opencode fails with 'Model not found: <modelId>/.'.

The Anthropic-vs-other discriminant only belongs in resolveAgent. Once an
agent is selected, it should consistently honor the bedrock route.

Caught by the PULLFROG_AGENT=opencode + Opus 4.6 e2e on
pullfrog/preview-720-bedrock — run 25823437606.

* ui+docs(bedrock): bespoke setup callout + clearer docs

UI:
- BedrockSetupCallout in components/AgentSettings.tsx covers both the
  Model costs section and the onboarding card. Detects bedrock via
  resolveDisplayAlias().routing === "bedrock", shows a dedicated message
  ("store AWS_BEARER_TOKEN_BEDROCK as a secret, then put AWS_REGION +
  BEDROCK_MODEL_ID directly in pullfrog.yml") + link to the setup guide.
  Replaces the generic "X, Y, or Z is required" prompt that misrepresented
  the three values as three separate secrets to add (and used the wrong
  "or" connector for what's actually an AND).
- OnboardingCard re-uses the same callout with the gradient-card variant.

Docs:
- Drop the obsolete "Enable model access" step. AWS retired the manual
  enrollment page; foundation models auto-enable on first invocation.
  Anthropic models still need a one-time use-case form for first-time
  users — surfaced under the AccessDenied troubleshooting entry.
- Drop the "Testing a different model in one run" PULLFROG_MODEL note.
  It introduced the secrets-vs-vars distinction we want to keep out of
  the bedrock setup story.
- Step 3 already recommends hardcoding region + model id in pullfrog.yml.

Workflow template:
- The default pullfrog.yml customers receive (utils/github/pullfrog.yml.ts)
  now references AWS_BEARER_TOKEN_BEDROCK from secrets but inlines
  AWS_REGION and BEDROCK_MODEL_ID as plain values. Matches the docs.

* fix(bedrock): three review-caught edges in routing + UI copy

Addresses three real issues from PR #720 review:

1. agent.ts: PULLFROG_MODEL=bedrock/byok no longer leaks the literal
   sentinel "bedrock" downstream. resolveCliModel returns the alias's
   resolve field verbatim, which for routing entries IS the sentinel.
   Refactored both the env-override and slug-lookup paths through a
   shared resolveSlug() that recognizes routing aliases and defers to
   their backing env var (BEDROCK_MODEL_ID).

2. models.ts: isBedrockAnthropicId() now anchors on a discrete
   dot/slash/colon-segment match (case-insensitive) instead of a
   substring contains. The substring check was fragile in both
   directions for inference-profile ARNs (BEDROCK_MODEL_ID accepts
   ARNs per AWS docs) — a non-Anthropic profile whose user-chosen name
   contained "anthropic" would mis-route to claude-code, and an
   Anthropic profile whose name omitted it would miss
   CLAUDE_CODE_USE_BEDROCK=1.

3. AgentSettings.tsx: BedrockSetupCallout's configured-state copy
   showed "AWS_BEARER_TOKEN_BEDROCK configured" even when the user
   satisfied the gate via AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY,
   gaslighting access-key users about a secret they never set.
   Detect which auth method is actually present and name the right
   secret(s) in the success message.

Regression tests in models.test.ts (5 new isBedrockAnthropicId cases
including positive and negative ARN forms) and agent.test.ts (2 new
PULLFROG_MODEL=bedrock/byok cases). 171/171 action tests pass.

* yml template: add commented AWS access-key alternative for Bedrock auth

Mirrors the IAM access-key path verified end-to-end on PR #720 e2e
run 25830764987. Bearer token stays as the primary nudge; the access-key
pair is the fallback for users who can't mint Bedrock API keys.

* yml template: drop redundant 'or, alternatively' annotation

* ui+docs(bedrock): rewrite callout copy + refresh screenshot

Reframes the BedrockSetupCallout away from generic BYOK language to a
Bedrock-specific message: leads with "Amazon Bedrock is configured
entirely via environment variables", lists all four (auth, region,
model id), and ends with the requested CTA sentence ("click below to
learn more about Bedrock support in Pullfrog").

Promotes the "Bedrock setup guide" docs link from an inline anchor to
a prominent button (always visible, regardless of auth state). The
"Add AWS_BEARER_TOKEN_BEDROCK" affordance is now a secondary chip
shown only when no auth secret is configured.

Refreshes docs/images/model-selector-bedrock.png to capture the new
callout — the prior screenshot still showed the old generic
"BYOK / X, Y, or Z required" wording.
2026-05-14 02:12:38 +00:00
Colin McDonnell 60cc8772a6 fix(log-audit): kill 404 noise from /api/github/installation-token at source (#693) (#708)
* fix(log-audit): kill 404 noise from `/api/github/installation-token` at source (#693)

Closes #693. Issue diagnosed a surface symptom (`log.error` on expected
404s) but missed the actual root causes. Investigation revealed two
distinct populations producing identical 3-call 404 bursts:

1. **Fork-CI on `pullfrog/pullfrog`**: `test-token.yml` and
   `trigger-sync.yml` ship with `on: push: main`, so every fork inherits
   them and 404s our token endpoint on first push. Self-inflicted noise
   that scales with fork count.
2. **Real users hitting the full action without installing the App**:
   `/api/repo/.../run-context` uses the caller's `GITHUB_TOKEN` to read
   the repo from GitHub and then unconditionally lazy-provisions
   Account+Repo rows via `fetchOrCreateRepo`, even when the App isn't
   installed. Generates phantom DB rows and false `new account created`
   team@ alerts. (Confirmed via Prisma: `ezcorp-org` has an Account row
   with `installerLogin: null`, never installed our App.)

Both populations then trip the client retry loop in
`acquireTokenViaOIDC`, which matched `"Token exchange failed"` and
retried 3× on terminal 4xx — tripling log volume and wasting CI time.

## Changes

- `action/.github/workflows/{test-token,trigger-sync}.yml`: gate jobs
  with `if: github.repository == 'pullfrog/pullfrog'`. Forks inherit
  the files but the jobs no-op.
- `app/api/repo/[owner]/[repo]/run-context/route.ts`: call
  `getRepoInstallation` first; return 404 with install URL if the App
  isn't installed, before any DB writes or GitHub repo fetch.
- `action/utils/github.ts`: introduce `TokenExchangeError` for non-2xx
  server responses; `acquireNewToken` no longer retries it. Retry now
  fires only on genuine network/timeout failures. 404 surfaces a
  user-actionable error pointing at the install URL.
- `app/api/github/installation-token/route.ts`: move `log.error` inside
  the 500 branch only. 404 branch is silent (expected user-state) and
  returns the same install URL message for consistency.

## Effect

- Better Stack `level=error` lines from this path: 6/day → 0.
- Failed user-trial CI time: 3 wasted token requests → 1.
- User-facing error: opaque `Token exchange failed: 404` → actionable
  install URL.
- No more phantom Account rows from never-installed callers.

Skipped per design discussion: phantom-account cleanup (conservative —
stop the bleed, leave history), `AGENTS.md` rule (overgeneralized).

* review: address oracle leak + per-env install URL + retryable 5xx

Addresses pullfrog[bot] (IMPORTANT) and Copilot review findings on #708:

- **Install-status oracle in `run-context`** [pullfrog, Copilot]:
  `getRepoInstallation` runs with our App's JWT, *before* the caller's
  bearer token is validated against the repo. Pre-PR the route was
  uniformly bad-token-shaped; the new install-specific 404 turned it
  into an unauthenticated oracle distinguishing "Pullfrog installed
  here" from "not installed". Collapsed the 404 message to match the
  outer catch's ambiguous "repository not found or token lacks access".
  Legit runners still get the actionable install URL from
  `/api/github/installation-token`, which IS gated by OIDC.

- **Hardcoded `github.com/apps/pullfrog`** [Copilot]: server-side
  `installation-token` now uses `GITHUB_APP_INSTALL_URL` from
  `app/globals.ts`, so dev/staging deployments with a different
  `GITHUB_APP_SLUG` direct users to the correct app. Action-side
  echoes the server's `error` body when present (single source of
  truth) and falls back to a generic message only if the body isn't
  JSON.

- **Transient 5xx/429 made terminal** [Copilot]: `shouldRetry` now
  returns `true` for `TokenExchangeError` with `status >= 500` or
  `status === 429`. 4xx remains terminal (the actual #693 fix). Real
  outages no longer fail the workflow immediately.

- **Stale comment** [pullfrog, Copilot]: reworded the comment at
  `installation-token/route.ts:141` to reflect the new retry policy
  ("the action surfaces this once (no retry)" instead of "the action
  retries on this").

* review: restore caller-token-first auth in run-context

Pre-PR, `getEnrichedRepo({owner, repo, token})` used the caller's
token as the auth boundary — `getRepo({token})` succeeding was the
proof-of-access check. My initial install-gate inverted the order
and ran the App-credentialed `getRepoInstallation` first, which is
how it became:

- an install-status oracle (pullfrog bot, addressed previously by
  matching the outer-catch wording), and
- an outbound amplifier against our App JWT for arbitrary `owner/repo`
  (pullfrog bot, this commit).

Reordered so `getRepo({token})` runs first. Garbage / unauthorized
bearers get rejected by github (mapped to 403 by the outer catch)
before any App-credentialed call fires. `getRepo` is cached 5min,
so `getEnrichedRepo` below remains a free re-hit.
2026-05-13 17:47:13 +00:00
Colin McDonnell f87e0f878c action: minimize pullfrog.yml permissions and drop actions:read (#594)
* action: minimize pullfrog.yml permissions and drop actions:read

The recommended pullfrog.yml workflow asked for a permissions block that's
broader than what the action actually uses with the workflow GITHUB_TOKEN —
all real work (git push, PR comments, reviews) goes through installation
tokens that the action mints via OIDC. Customer security scanners flagged
the workflow-level block as too permissive.

- Move permissions to the job level and reduce to id-token: write,
  pull-requests: write, issues: write. contents:read is the implicit default
  and covers actions/checkout; contents:write, checks:read are unused by
  any GITHUB_TOKEN consumer; actions:read was only used by post-cleanup's
  listJobsForWorkflowRun call.
- Replace listJobsForWorkflowRun with a SIGTERM/SIGINT handler in main.ts
  that calls core.saveState("cancelled", "true"); post-cleanup reads it
  back via core.getState. Same cancel-vs-failure UX, no extra scope needed.
- Sync the docs (headless-action, getting-started, action/README) and the
  two dogfood pullfrog.yml workflows to the new minimal block. Update the
  post-cleanup wiki to describe the saveState approach.

* action: drop pull-requests/issues from required workflow scopes

Switch postCleanup.ts to mint its own short-lived installation token via OIDC
(acquireNewToken with issues:write + pull_requests:write) instead of using the
workflow GITHUB_TOKEN. Same comment-update behavior, but the workflow no longer
needs those scopes — the only permissions Pullfrog ever asks for are id-token:write
(OIDC exchange) and contents:read (actions/checkout).

Also fixes a bug from the previous commit: setting an explicit permissions block
drops every unlisted scope to none (with metadata as the only exception), so
omitting contents would have broken actions/checkout. Restored at both workflow
and job level.

* action: scope id-token:write to pullfrog job, not workflow level

id-token:write is the powerful one — it lets a job mint OIDC tokens that can
be exchanged for cloud credentials or our installation tokens. Keeping it at
workflow level means any future job added to this file silently inherits it.
Move it to the job level where it's actually used; leave only contents:read
at workflow level as a safe baseline for any future jobs.

* action: move stuck-comment cleanup server-side, drop write perms entirely

The action's post-cleanup step lived inside the runner and used the workflow
GITHUB_TOKEN to update the "Leaping into action…" progress comment when a run
failed/cancelled, requiring pull-requests:write + issues:write at the workflow
level. Move that responsibility to the workflow_run.completed webhook handler:
it already has installation-token access via the GitHub App, runs server-side
(no Pullfrog API dependency loop on failure), and lets us drop both write perms.

Recommended workflow permissions block is now truly minimal:

  permissions:
    contents: read
  jobs:
    pullfrog:
      permissions:
        id-token: write
        contents: read

Server side
- handleWorkflowRunCompleted: when conclusion != "success" and the WorkflowRun
  has progressCommentId, mint installation octokit and update the stuck comment
  in place. Try issues.getComment first, fall back to pulls.getReviewComment on
  404 (we don't store comment type — one wasted GET on the rarer review case).
- Reuses buildPullfrogFooter and updateProgressComment from pullfrog/internal,
  matching the wording the action used to write client-side.

Client side
- Delete action/utils/postCleanup.ts and action/post.ts.
- Remove post: + post-if: from action/action.yml.
- Drop runPostCleanup wiring from action/commands/gha.ts and action/play.ts.
- Remove the SIGTERM/saveState handler I added in main.ts in the previous commit
  (no longer needed; cancel/fail signal comes from the webhook hook payload).

Plumbing
- Extract isLeapingIntoActionCommentBody into action/utils/leapingComment.ts so
  the predicate can be re-exported via pullfrog/internal without dragging the
  MCP server's transitive type graph into the Next.js app's typecheck.
- mcp/comment.ts re-exports from the new location for backward compat.

Wiki
- Delete wiki/post-cleanup.md (obsolete; cleanup is now a one-liner branch in
  the workflow_run webhook handler).

* chore: ignore .worktrees in biome config

Recently-added pnpm worktree feature creates nested git worktrees under
.worktrees/, each with their own biome.jsonc declaring root. Biome's
recursive scan trips on the nested config and fails pnpm lint. Excluding
the directory matches the existing .gitignore entry.

* fix: address PR #594 review findings

Two real bugs caught by code review:

1. handleWorkflowRunWebhook.ts:323 — drop the /m flag on the stuck-comment
   detection regex. With /m, ^ matches any line start, so any finalized
   progress comment that embeds a task list (report_progress writes
   `- [x]`/`- [ ]` lines via todoTracking.ts) would be flagged as "stuck"
   and silently overwritten with the "This run croaked" boilerplate
   whenever the workflow concluded non-success after the agent's final
   summary already landed. Restores the body-start anchoring the original
   in-process postCleanup.ts:90 had.

2. action/scripts/check-entrypoint-imports.ts — drop ../post.ts from the
   esbuild entry-point list (the file was deleted in aa43b9af). The
   `pnpm check:entrypoints` step in test.yml would have failed on every
   run with an unresolvable-entry-point error.

Plus three small follow-ups:
- main.ts:580 — comment said "post-cleanup has its own verify-retry loop"
  but post-cleanup is gone. Updated to describe the new server-side path.
- mcp/comment.ts:443 — comment said "so post script doesn't think the run
  failed". Updated to describe the actual current consumers of wasUpdated.
- commands/gha.ts:84 — `--post` help text said "run post-cleanup flow" but
  with the post-cleanup path removed, --post is only valid alongside the
  `token` subcommand for installation-token revocation. Updated wording.

* fix(action): scope --post help text to gha token subcommand

Root gha help text was documenting --post, but --post only makes sense
paired with the token subcommand (it's how the post step revokes the
installation token previously acquired in the main step). Move it to a
dedicated gha token help section and add a parser layer that rejects
--post on the bare gha command.

  $ pullfrog gha --help
  usage: pullfrog gha [subcommand]
  ...
  options:
    -h, --help   show help

  $ pullfrog gha token --help
  usage: pullfrog gha token [--post]
  ...
  options:
    -h, --help   show help
    --post       revoke the previously-acquired token (post-step usage only)

* webhook: artifact-aware cleanup of stranded leaping comments on success

Previously the workflow_run.completed cleanup only handled non-success
conclusions. Extend it to also catch the rare case where a successful
run leaves a "Leaping into action…" comment stuck (in-process cleanup at
action/main.ts:723 normally handles this, but can be skipped on SIGKILL,
runner host crash, or any exit path that bypasses main()'s finally block).

New behavior in cleanupStuckProgressComment:

  - cancelled       → update with "cancelled 🛑" body  (unchanged)
  - failure (other) → update with "croaked 😵" body    (unchanged)
  - success + artifact recorded → delete the comment (the artifact is the
                                  user-facing surface; the leaping comment
                                  is just stale UI noise at this point)
  - success + no artifact recorded → delete the comment AND alert
                                     team@pullfrog.com via emailAlert

The "success + no artifact" path is "should never happen" territory: the
run claims success but produced no review, PR, issue, plan, or summary
comment. The team alert helps us catch in-process cleanup regressions or
artifact-tracking gaps. hasRecordedArtifact reads {review,pr,issue,
planComment,summaryComment}NodeId off the WorkflowRun row to make the call.

* webhook: narrow stuck-comment detection to leaping prefix only

Drop the stranded-todo-pattern branch from cleanupStuckProgressComment.
The leaping prefix is highly specific and impossible to confuse with a
legitimate summary; a leading todo line is not — the agent's
error-reporting paths can produce useful explanatory comments whose
body leads with a checklist (e.g. "here's what I was working on" + the
incomplete todo list), and we don't want to silently overwrite those
with the generic "croaked" boilerplate.

In-process cleanup at action/main.ts:723 still handles the stranded-todo
case in the common path (gated on !finalSummaryWritten with full access
to the in-memory tool state). Missing the rare runner-died-mid-todo case
server-side is a worthwhile trade vs. the false-positive risk on real
explanatory comments.
2026-05-07 18:59:52 +00:00
Colin McDonnell 4b3c5ca905 rename agent key to opencode and add skill invocation coverage (#541)
* rename agent key to opencode and add skill invocation coverage.

add skill-invoke tests for claude and opencode with local play-based validation signals, update CI matrices, and include the current tracked refactors in this branch for review.

Made-with: Cursor

* exclude agent-specific skill-invoke tests from wrong agent in CI matrix

* address review follow-up and preserve workflow run UI tweak.

switch changed-agents ci coverage to exercise the opentoad implementation path while keeping the opencode expectation, and include the local workflow run client interaction updates requested on this branch.

Made-with: Cursor

* remove opentoad agent filename from runtime.

rename the opencode harness implementation file from opentoad.ts to opencode.ts and update ci coverage input accordingly so action code no longer carries the old filename.

Made-with: Cursor

* ensure security prompt bypass is set on every test fixture.

this keeps adversarial and permissions harnesses from being blocked by the default prompt-injection refusal path during CI security tests.

Made-with: Cursor

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-04-15 19:38:36 +00:00
Colin McDonnell 3c2f3722ff fix action package exports and build ESM library entrypoints.
emit real ESM runtime + declaration outputs for programmatic imports, align package exports/types with built files, and add a no-cjs policy note.

Made-with: Cursor
2026-04-12 19:49:21 +00:00
Colin McDonnell 6541bdc4f4 test-token: use auth-only endpoint to actually verify the token
Made-with: Cursor
2026-04-12 19:44:03 +00:00
Colin McDonnell 1393ffb7b8 fix test-token workflow: use full action ref so runCli takes local path
Made-with: Cursor
2026-04-12 19:37:24 +00:00
Colin McDonnell f663d5e34d add workflow_dispatch test for get-installation-token action
Made-with: Cursor
2026-04-12 19:34:53 +00:00
Colin McDonnell ed90735ba0 drop redundant NODE_AUTH_TOKEN="" from publish step
Made-with: Cursor
2026-04-12 19:03:22 +00:00
Colin McDonnell bbcf91a06e fix publish workflow: add build step, use OIDC trusted publishing, bump 0.0.196
publish was missing a build step so the npm tarball had no dist/.
switch from NPM_TOKEN to OIDC trusted publishing — explicitly unset
NODE_AUTH_TOKEN so setup-node's .npmrc doesn't override the OIDC flow.
bump version since v0.0.195 tag exists from the failed publish attempt.

Made-with: Cursor
2026-04-12 19:02:32 +00:00
Colin McDonnell 421607cf97 fix push-to-action: use CLI direct invocation for token acquisition
the inline `node -e` + `TOKEN=$(...)` approach broke because
`core.getIDToken()` in @actions/core writes `::debug::` and
`::add-mask::` to stdout, polluting the captured value.

`node cli.ts gha token` uses `core.setOutput()` which writes to
the $GITHUB_OUTPUT file instead of stdout.

Made-with: Cursor
2026-04-12 00:47:41 +00:00
Colin McDonnell 8cd36d221a sandbox native filesystem tools to prevent /proc/self/environ exfiltration (#509)
* sandbox native filesystem tools to prevent /proc/self/environ exfiltration

the agent's native Read/Grep/Edit tools can bypass the shell sandbox by
reading /proc/self/environ directly. this adds agent-native filesystem
restrictions using the highest-precedence, non-overridable config for each CLI:

OpenCode: OPENCODE_PERMISSION env var with external_directory deny-all + /tmp allow,
plus deletion of untrusted .opencode/plugins/ and .opencode/tools/ before launch.

Claude Code: managed-settings.json at /etc/claude-code/ with denyRead, permissions.deny,
allowManagedPermissionRulesOnly, allowManagedHooksOnly. also --setting-sources user and
--disallowedTools path patterns as belt-and-suspenders.

Made-with: Cursor

* add Glob to Claude Code /proc and /sys deny lists

closes gap identified in review — Glob can enumerate /proc entries.
added to both managed-settings.json permissions.deny and --disallowedTools.

Made-with: Cursor

* run token-exfil test with both agents, hint at native /proc reads

changed tag from "agnostic" (opentoad-only) to "security" so the test
runs with both opentoad and claude. updated prompt to explicitly instruct
the agent to try reading /proc/self/environ via native Read tool.
added API keys to action-agnostic CI job for claude support.

Made-with: Cursor

* move token-exfil to crossagent matrix, remove redundant permissions.deny

- moved token-exfil from agnostic/ to crossagent/ so it runs via the
  agent matrix (claude + opentoad in parallel) instead of sequentially
- removed permissions.deny per-tool rules from managed-settings.json;
  sandbox.filesystem.denyRead is the single enforcement mechanism
- reverted action-agnostic env vars to minimal set
- updated wiki to match

Made-with: Cursor

* document post-spawn API key deletion analysis in security wiki

evaluated whether API key env vars can be deleted from agent processes
after spawn. OpenCode snapshots env at startup (safe to delete), but
Claude Code re-reads process.env per request (not viable). documented
as further exploration item with per-agent breakdown and caveats.

Made-with: Cursor

* fix stale tokenExfil path references in wiki docs

moved from test/agnostic/ to test/crossagent/ in directory tree
and adversarial test example.

Made-with: Cursor

* revert accidental prisma.config.ts changes

Made-with: Cursor

* hardcode PULLFROG_MODEL per agent in test runner to avoid DB model mismatch

when PULLFROG_AGENT forces a specific agent, the DB-configured model may
belong to a different provider (e.g. openai model with claude agent).
PULLFROG_MODEL short-circuits the DB slug resolution entirely.

Made-with: Cursor
2026-04-02 22:31:41 +00:00
Colin McDonnell 0055aef618 feat: add Claude Code agent for Anthropic model users (#502)
* feat: add Claude Code agent for Anthropic model users

Re-adds Claude Code support (removed in #478) so users with Anthropic API
keys or Claude Code OAuth tokens can use their Claude subscriptions directly.

When an Anthropic model is selected and Claude Code credentials are available,
the system auto-selects the Claude agent instead of OpenCode. The harness
mirrors opentoad's security model: native Bash blocked via --disallowedTools,
MCP ShellTool for restricted shell, ASKPASS for git auth. Includes NDJSON
streaming, provider error detection, cache/cost tracking, browser skill,
and todo progress tracking.

Key changes:
- action/agents/claude.ts: full Claude Code harness
- action/utils/agent.ts: auto-select Claude for anthropic/* models
- action/utils/providerErrors.ts: extracted shared provider error detection
- action/utils/skills.ts: extracted shared skill installation (agent-aware)
- action/models.ts: add CLAUDE_CODE_OAUTH_TOKEN to anthropic envVars
- action/utils/docker.ts: add CLAUDE_CODE_OAUTH_TOKEN to test env allowlist
- CI: add claude to test matrix, pass CLAUDE_CODE_OAUTH_TOKEN secret

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused toolId variable, fix apiKeys test env cleanup

The apiKeys test cleanup stripped *_API_KEY vars but missed
CLAUDE_CODE_OAUTH_TOKEN which doesn't match that pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: strip provider prefix from PULLFROG_MODEL in Claude agent

the env override path was returning the raw value (e.g.
"anthropic/claude-sonnet-4-5") without stripping the provider prefix,
causing the Claude CLI to receive an invalid model ID.

Made-with: Cursor

* fix: remove dead cliPath field, add CLAUDE_CODE_OAUTH_TOKEN to workflows

remove unused cliPath from Claude agent RunParams, and pass
CLAUDE_CODE_OAUTH_TOKEN through all pullfrog.yml workflow templates
so users with Claude Pro/Team subscriptions can use their membership.

Made-with: Cursor

* fix: block Bash subagent in Claude Code disallowedTools

Made-with: Cursor

* chore: update model snapshot (opencode/openrouter latest → qwen3.6-plus-free)

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:29:54 +00:00
Colin McDonnell e6d34ee01b add OpenRouter proxy for managed model routing and OSS program (#488)
* add OpenRouter proxy for managed model routing and OSS program

proxy layer that mints ephemeral OpenRouter keys for users without BYOK
API keys. two paths: pro plan users get their selected model proxied via
OpenRouter; OSS program repos (hard-coded allowlist in config.ts) get
free Claude Opus. BYOK env vars (PULLFROG_MODEL/OPENCODE_MODEL) always
take precedence.

frontend: OSS repos see a static "Opus (Free)" badge with the model
dropdown disabled and no API key requirement. all models now carry
openRouterResolve metadata for proxy target resolution.

Made-with: Cursor

* implement OSS program: proxy infrastructure for free model credits

server-side OSS allowlist determines eligible repos. action mints
ephemeral OpenRouter keys via OIDC-authenticated /api/proxy-token
endpoint (idempotent on runId, $10 per-key safety limit). keys are
disabled on workflow completion when no running refs remain. HWM-based
usage sync tracks cumulative spend per account.

schema: ProxyKey model, Account.usageUsd/activeKeyId, WorkflowRun.proxyKeyId
action: OIDC credential stashing, resolveProxyModel uses server oss flag
frontend: isOss flows from server page to components (no client allowlist)
Made-with: Cursor

* address PR review: repo cross-check, key retirement lifecycle, dead code removal

- proxy-token: verify runId belongs to OIDC-authenticated repo via repo relation
- add retireKey() shared primitive: disable in OpenRouter first, then mark disabled in DB
- rotateKey: retire old active key after swap to prevent orphans
- webhook: replace inline cleanupProxyKey with retireKey calls
- syncAccountUsage: skip disabled keys
- remove vestigial AccountPlan/plan field from action types
- add disabled field to ProxyKey schema + migration

Made-with: Cursor

* replace deprecated opencode/mimo-v2-flash-free with mimo-v2-pro-free

Made-with: Cursor

* fix migration ordering: rename disabled migration to sort after table creation

Made-with: Cursor

* squash proxy key migrations into single migration

Made-with: Cursor

* add preview repo to OSS allowlist for testing

Made-with: Cursor

* populate OSS allowlist from oss-program-invitees.json

Made-with: Cursor

* format oss-program-invitees.json

Made-with: Cursor

* add installed public repos to OSS allowlist

split ossRepos into three provenance-tracked lists:
- internalRepos (pullfrog, colinhacks, RobinTail)
- installedPublicRepos (external public non-fork repos with active installs)
- invitees (from oss-program-invitees.json)

also adds scripts/list-oss-candidates.ts to regenerate the installed list

Made-with: Cursor

* fix: resolve tokens before clearing OIDC env vars

resolveTokens → acquireNewToken → isOIDCAvailable() checks
ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN env vars. The new OIDC
stashing code was deleting them in restricted shell mode before
resolveTokens ran, causing it to fall through to the GitHub App
path which requires GITHUB_APP_ID/GITHUB_PRIVATE_KEY.

Made-with: Cursor

* derive proxy-token auth from OIDC claims, add ensureWorkflowRun upsert

- proxy-token no longer requires body.runId; uses claims.run_id + claims.repository
- shared ensureWorkflowRun upsert called from both webhook and proxy-token
- workflow_run_requested handler now eagerly creates WorkflowRun records
- eliminates race condition between webhook and action proxy-token call

Made-with: Cursor

* hardcode PULLFROG_ACCOUNT_ID, document preview debugging lessons

GitHub node IDs are constant — no reason for this to be an env var.
Removes the trailing-newline bug that caused P2025 errors.
Adds wiki docs on workflow testing, Vercel env gotchas, and Neon
preview branch discovery.

Made-with: Cursor

* fix: parse OpenRouter create-key response correctly

the API returns `key` at the top level, not inside `data`

Made-with: Cursor

* onboarding cards, unlock OSS model selection, simplify console

- add OnboardingCard component with two states: workflow install
  and model+test (dispatches "Tell me a joke" for test run)
- replace PromptBox overlay gates with dedicated onboarding cards;
  PromptBox is now just the form, always enabled
- use hasWorkflowRuns DB check to decide onboarding vs promptbox
- unlock ModelSelector for OSS repos (was locked to Opus badge);
  resolve proxyModel from repo's selected model alias in run-context
- rename "API key" row to "Model costs" with pure client-side states:
  OSS covered, auto-resolve, free model, BYOK with env var names
- add "(Recommended)" badge to model aliases with recommended: true
- remove OSS_MODEL_DISPLAY_NAME constant and secrets-fetching logic

Made-with: Cursor

* update stale xai model snapshot

Made-with: Cursor

* rename Permissions to Security, hide git push toggle, add shell isolation toggle with disabled state for public repos, remove opentoad agent name from logs

Made-with: Cursor

* chevron hover states, sidebar hooks/security entries

Made-with: Cursor

* address PR review feedback: rename recommended→preferred, fix dispatch orphan, update wiki docs

- rename `recommended` to `preferred` in model alias registry to distinguish
  from the UI "Recommended" badge (which is hardcoded for opus + codex only)
- cancel precreated WorkflowRun when workflow lookup fails in dispatch-workflow
- replace run_sql/vercel env pull in wiki docs with neonctl + Prisma pattern
- extend scripts/neon-branch.ts to output DATABASE_URL via neonctl
- add Recommended badge to GPT Codex alongside Claude Opus in ModelSelector

Made-with: Cursor
2026-03-25 17:19:55 +00:00
Mateusz Burzyński cccf1775d6 Update actions/setup-node 2026-03-18 12:55:52 +00:00
Colin McDonnell cbbcb64859 restructure docs: split triggers into usage pages, add model resolution docs
- split triggers.mdx into direct-prompting, pr-reviews, issue-enrichment, coding-tasks
- rename manual-setup.mdx to headless-action.mdx (CI integration)
- reorganize sidebar into Getting started / Usage / Reference groups
- add redirects for /triggers and /manual-setup
- add PULLFROG_MODEL env var support across action, workflows, and docs
- rewrite models.mdx with aliases, free models, resolution chain, routers
- update all cross-references in app, components, and docs

Made-with: Cursor
2026-03-12 17:45:15 +00:00
Colin McDonnell 6d25adfd1a Agent & model refactor (#478)
* agent & model refactor with ASKPASS git auth, UI restructure, clerk v7

Made-with: Cursor

* fix stale agent/effort refs, add tests for askpass + model resolution

- reviewCleanup.ts: payload.agent -> payload.model, remove effort
- selectMode.ts PlanEdit: remove delegation/subagent/effort references
- pullfrog.yml.ts: update env vars (drop GOOGLE_API_KEY/CURSOR_API_KEY,
  add GOOGLE_GENERATIVE_AI_API_KEY/XAI_API_KEY/MOONSHOT_API_KEY/OPENCODE_API_KEY)
- FlagsSettings/RepoInstructionsSection: remove stale effort/timeout copy
- new: gitAuthServer.test.ts (10 tests — lifecycle, token delivery, tamper detection, script gen)
- new: agent.test.ts (4 tests — default opentoad, AGENT_OVERRIDE, invalid override)
- new: models.test.ts (19 tests — parseModel, resolution, registry invariants)
- update models.dev snapshot

Made-with: Cursor

* fix changed-agents.sh to filter legacy agent files from CI matrix

legacy agent files (claude.ts, codex.ts, etc.) are @ts-nocheck and not
exported from index.ts. changed-agents.sh now reads index.ts imports to
build the active agent set and treats changes to inactive files as
non-agent changes (opentoad canary only).

Made-with: Cursor

* remove MCP file tools, old agent harnesses, and obsolete security tests

ASKPASS-based git auth makes the old MCP file tool security layer unnecessary:
- token never in subprocess env, so symlink/gitattributes/hook attacks can't exfiltrate it
- agents now use native file tools (OpenCode builtin read/edit)

deleted:
- action/mcp/file.ts (file_read, file_write, file_edit, file_delete, list_directory)
- action/mcp/index.ts (dead re-export)
- agent harnesses: claude.ts, codex.ts, cursor.ts, gemini.ts, opencode.ts
- opencode-runner.ts (inlined into opentoad.ts)
- security tests that validated MCP file tool restrictions
- commented-out three-step review flow (~300 lines)
- sanitizeSchema/wrapSchema dead code from mcp/shared.ts
- OPENCODE_MODEL_MINI/MAX env vars (effort-level model overrides removed)

updated test prompts to use generic file ops instead of MCP tool names.
restored pkg-json-scripts + requirements-txt-attack (test --ignore-scripts defense).

Made-with: Cursor

* bump actions/checkout v4 → v6 (node 24)

node 20 actions deprecated june 2, 2026.

Made-with: Cursor

* temporarily disable fail-fast on agnostic tests to debug checkout@v6

Made-with: Cursor

* re-enable fail-fast on agnostic tests

Made-with: Cursor

* fix test token mismatch: mint OIDC tokens scoped to target repo

CI tests override GITHUB_REPOSITORY to pullfrog/test-repo but inherit
the runner's GITHUB_TOKEN (scoped to pullfrog/app), causing 401s on
every run-context fetch. Clear GITHUB_TOKEN in the test subprocess so
ensureGitHubToken() mints a properly scoped token via OIDC.

Also centralizes the default GITHUB_REPOSITORY in runAgentStreaming
instead of repeating it in every test file, and fixes preview-cleanup
to remove workers from all queues (not just name-matching ones).

Made-with: Cursor

* fix ensureGitHubToken to try OIDC when app credentials are absent

ensureGitHubToken only attempted token minting when GITHUB_APP_ID and
GITHUB_PRIVATE_KEY were set. In CI, OIDC is available but app creds
aren't exposed — so the guard prevented minting entirely.

Made-with: Cursor

* dead code cleanup: remove remnants of deleted agents, file tools, effort system

remove unused @anthropic-ai/claude-agent-sdk and @openai/codex-sdk deps,
orphaned file-tool security tests, dead GEMINI_MODEL passthrough, stale
opencode-runner wiki refs, deleted test file references, and MCP file tool
docs. rename docs/effort → docs/models. fix vitest setup: move dotenv to
globalSetup (runs once before forks instead of per-file, 19s → 200ms).

Made-with: Cursor

* address review feedback: remove dead code, update stale references

- remove AGENT_OVERRIDE (only opentoad exists)
- remove shellToolName plumbing (always restricted shell)
- bump action version to 0.0.179
- remove CURSOR_API_KEY from all workflows/configs
- remove OPENCODE_MODEL_MINI/MAX from workflows/docs
- delete wiki/effort.md, rewrite docs/effort.mdx as "Models"
- rewrite wiki/modes.md: orchestrator/subagent → single agent
- simplify flag system: drop builtin flag extraction (debug, effort,
  timeout, agent), keep custom flag replacement only
- reserve all legacy flag names to prevent custom flag conflicts

Made-with: Cursor

* regenerate lockfile after removing claude-agent-sdk and codex-sdk

Made-with: Cursor

* fix import ordering, add lockfile check to pre-push hook

Made-with: Cursor

* remove dead debug payload field, stale packageExtensions

Made-with: Cursor

* merge proc-sandbox and token-exfil into a single test

proc-sandbox and token-exfil were duplicative — both tested that
SANDBOX_TEST_TOKEN couldn't be exfiltrated. consolidated into
token-exfil with shell:restricted (which actually exercises filterEnv)
and the /proc attack vector hints from proc-sandbox.

Made-with: Cursor

* fix wiki adversarial.md to match actual tokenExfil validator

Made-with: Cursor
2026-03-12 05:22:51 +00:00
Colin McDonnell b472aa1ba9 fix(opencode): add effort-aware model overrides and OpenRouter guidance (#354)
* chore: create empty commit for PR

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(action): trigger preview repo creation workflow

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): merge repo opencode config and log provider key presence

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): add effort-aware model override precedence

Add OPENCODE_MODEL_MINI and OPENCODE_MODEL_MAX support with fallback to OPENCODE_MODEL and auto-selection, and keep provider resolution aligned via inline OpenCode config when overrides are used. Update workflow env wiring, test allowlists, and docs (including OpenRouter setup guidance and sidebar ordering) to document the new behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(effort): clarify OpenCode precedence and add section link

Add a concise OpenCode precedence callout in the summary area and link directly to the OpenCode section for full details and examples.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restructure dash (#372)

* Restructure dash

* WIP

* WIP

* refactor trigger UI: extract PR summary card, add mentions section, rename labels

Co-authored-by: Cursor <cursoragent@cursor.com>

* clean up console UI: remove info icons from section descriptions, rename mentions trigger

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix review feedback: layout, terminology, form scope

- extract console sidebar sections to module-level constant
- align three-column layout breakpoints to xl (match sidebar visibility)
- fix mixed shell/bash terminology in beta page
- scope FormProvider to trigger sections only, restore autoComplete="off"

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bump

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: create empty commit for PR

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): merge repo opencode config and log provider key presence

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): add effort-aware model override precedence

Add OPENCODE_MODEL_MINI and OPENCODE_MODEL_MAX support with fallback to OPENCODE_MODEL and auto-selection, and keep provider resolution aligned via inline OpenCode config when overrides are used. Update workflow env wiring, test allowlists, and docs (including OpenRouter setup guidance and sidebar ordering) to document the new behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: update agent rules and OpenCode config docs

Align AGENTS guidance with current preferences and apply review-driven wording updates in OpenCode-related files.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:05:26 +00:00
Mateusz Burzyński 185ca7a832 Avoid using --ignore-workspace (#353) 2026-02-19 14:41:21 +00:00
Colin McDonnell 0e1f1ccbb7 pass GEMINI_MODEL and OPENCODE_MODEL vars through CI and Docker
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 02:33:00 +00:00
Colin McDonnell 8a64742ddf sync action workflow fail-fast to match root workflow
the root workflow was updated to fail-fast: true but the action
workflow wasn't updated to match. the ci consistency test enforces
they stay in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 02:10:18 +00:00
Colin McDonnell 6f108237d4 Deployment protection bypass (#298)
* test preview bypass 2

Co-authored-by: Cursor <cursoragent@cursor.com>

* add apiFetch wrapper with Vercel bypass via query param + header

the template workflow was missing VERCEL_AUTOMATION_BYPASS_SECRET,
so all action API calls to preview deployments hit Vercel's
deployment protection without bypass. this also consolidates the
bypass logic into a single fetch wrapper that applies the secret
as both a query parameter (matching server-side forwarding) and
a header for belt-and-suspenders reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>

* security hardening for Vercel bypass

- redact bypass token from webhook forwarder logs and response body
- remove dead x-preview-api-forward header
- refactor getAllSecrets() to use SENSITIVE_PATTERNS instead of hardcoded list
- enforce https:// on API_URL (localhost exempt for local dev)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 20:01:48 +00:00
David Blass 9071c0ae6c refactor mode selection into delegate tool that spawns subagents (#265) 2026-02-12 19:34:47 +00:00
David Blass bc28c658f2 harden sandbox escape vectors for bash disabled/restricted modes (#257)
* harden sandbox escape vectors for bash disabled/restricted modes

block git config injection (-c flag as subcommand), dangerous subcommands
(config, submodule, rebase, bisect), code-executing arg flags (--exec,
--extcmd), .gitattributes/.gitmodules writes, and package lifecycle scripts.
add retry logic to test runner for transient failures. add security unit
tests and adhoc attack tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* only filter subcommands in nobash, remove nobash from ui

* use regex matching

* iterate on tests

* simplify githooks

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 02:02:41 +00:00
David Blass 19df8372cd add file_read/file_write tools, sandbox tests, CI improvements (#239)
* migrate to flags

* init

* iterate on file write lockdown tests

* improve ci

* fix lockfile

* fix typecheck

* fix lint

* improve pushRestricted

* ok

* fix more

* ok

* remove process.env spreading rule

Co-authored-by: Cursor <cursoragent@cursor.com>

* enhanced fs rw tools

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
2026-02-10 06:35:47 +00:00
Colin McDonnell 102417f442 Add post hooks for cleanup (#193)
* Add post hooks for cleanup

* Switch to signal-based cleanup

* Better exit handling
2026-01-28 01:59:15 +00:00
pullfrog[bot] 90945a9481 chore: update pullfrog.yml workflow 2026-01-28 00:51:37 +00:00
pullfrog[bot] a200d07370 feat: Immediate Leaping into action (#146)
* feat: post "Leaping..." comment immediately without polling GitHub API

This change makes the initial comment response much faster by avoiding
the expensive GitHub API polling that was waiting for workflows to
dequeue (up to 12s+ in some cases).

New architecture:
1. Create WorkflowRun record BEFORE dispatching (no runId yet)
2. Post "Leaping into action..." comment with shortlink URL immediately
3. Dispatch workflow and return
4. workflow_run.requested webhook fills in runId when GitHub dequeues

Shortlink redirect at /api/workflow-run/[id]/logs:
- If runId available: redirects to GitHub workflow run
- If runId null: shows polling page that checks DB every 1.5s

Changes:
- Make `runId` optional on WorkflowRun model (filled in via webhook)
- Add `workflow_run` to expected webhook events
- Add handler for workflow_run.requested to update DB with runId
- Create /api/workflow-run/[id]/logs shortlink redirect route
- Refactor triggerWorkflow.ts to use eager comment pattern
- Update trigger page to use new pattern

Closes #141

* chore: add migration for nullable runId in WorkflowRun

* fix: rm dead code.

* fix: Adjusting the issueNumber prop usage comment.

* fix: shortening and JSDoc for createWorkflowRunRecord.

* fix: Reducing diff, reducing confusion on naming the id.

* Revert "fix: Adjusting the issueNumber prop usage comment."

This reverts commit 34d87c2f8bc58782a53ce5eb14a40935232a4924.

* refactor: reuse buildShortlinkUrl in trigger page

* fix: Reducing confusion on param naming.

* fix: shorter JSDoc.

* Apply suggestion from @RobinTail

* chore: remove unnecessary JSDoc comment from buildShortlinkUrl

* Revert "chore: remove unnecessary JSDoc comment from buildShortlinkUrl"

This reverts commit 491ba6ba3f31c874c9f391871739efa50adb0446.

* fix: confusing naming of var.

* fix: redundant 'let'.

* refactor: move route from `/api/workflow-run/[id]/logs` to `/api/workflow-run-logs/[id]`

Avoids confusion with existing `/api/workflow-run/[runId]` route which uses
GitHub's runId, whereas this new route uses the internal WorkflowRun record id.

* chore: remove old route directory

* refactor: reuse `buildShortlinkUrl()` with `shouldPoll` param

* refactor: add script prop to generateLeapingLoaderHtml

Instead of string-replacing to inject scripts, the function now
accepts an optional script prop that gets wrapped in <script> tags.

* mv script into new LeapingLoaderHtmlProps.

* refactor: extract `buildGithubUrl` helper to avoid repetition

* fix: shorening.

* fix: More clear subtitle.

* refactor: reuse `WORKFLOW_FILENAME` from `app/globals.ts`

* fix: shortening.

* feat: add integrity_id for reliable workflow matching

Pass WorkflowRun record id as integrity_id when dispatching workflows.
The webhook handler parses integrity_id from display_title (via run-name)
for reliable matching, with fallback to repo/owner lookup when missing.

Note: workflow template changes (.github/workflows/pullfrog.yml) need to be
applied manually as the GitHub App lacks workflows permission.

* Revert "feat: add integrity_id for reliable workflow matching"

This reverts commit dbc601233a0dd85ac5f0d608a221e7015e265aaa.

* Add todo for consideration later.

* docs: add plan for action-initiated workflow run correlation

Addresses review feedback requesting research into secure alternatives
to exposing HOOKDECK_API_KEY. Proposes leveraging existing OIDC token
exchange to pass WorkflowRun record ID and correlate with run_id.

* Revert "docs: add plan for action-initiated workflow run correlation"

This reverts commit b9279d0e99db4d85e4144675634afa941858333a.

* FEAT: Add optional integrity_id input, used by run-name, set with partial record id, read by handler for lookup.

* Add integrity_id to app/trigger/[owner]/[repo]/[number]/page.tsx

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>

* feat: Extracting INTEGRITY_ID_LENGTH.

* Add integrity_id to the workflow files of the repo.

* fix: Use Vercel Preview deployment URL into account in buildShortlinkUrl().

* fix: add missing `.rest` prefix in Octokit API call

* revert: remove unnecessary escaping of backticks in comment

* fix: add polling timeout and wrap DB update in try-catch

- Add 3-minute timeout to polling page to prevent indefinite polling
- Wrap updateWorkflowRunComment in try-catch to prevent orphaned comments

* feat: restore job-level deep linking for workflow runs

Adds jobId to WorkflowRun model and captures the first job ID via
listJobsForWorkflowRun() when the workflow_run_in_progress event fires.
The shortlink redirect now appends /job/{jobId} when available, providing
a direct link to the job rather than just the workflow run.

* fix: handle only `workflow_run.in_progress` to avoid race condition

Combine the handling of `runId` and `jobId` into a single update when
`workflow_run.in_progress` fires, avoiding the race condition where
`in_progress` could arrive before `requested` was processed.

* Renae integrity_id -> name

* Clean up

* Clean up

* Shorter timeout

* Add fallback

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Robin Tail <robin_tail@me.com>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
2026-01-28 00:40:56 +00:00
David Blass 9a8db3e07c add restricted tests, refactor test infrastructure (#150) 2026-01-22 21:06:19 +00:00
Colin McDonnell c6dfe4fa10 Update workflows 2026-01-21 03:27:12 +00:00
Colin McDonnell df4e7a9a4a Update workflows 2026-01-21 03:25:32 +00:00
Colin McDonnell 6af0c721ba Update workflows 2026-01-21 03:24:35 +00:00
David Blass 4e19178c81 fix CI (#111) 2026-01-20 17:25:06 +00:00
David Blass 410b11db71 test CI 2026-01-16 20:21:54 +00:00
David Blass 02a498e0cb update workflows 2026-01-16 16:00:27 +00:00
David Blass e4b086938e iterate on CI 2026-01-16 15:52:37 +00:00
Mateusz Burzyński 332ef73b87 Remove invalid working-directory setting (#105) 2026-01-16 10:56:28 +00:00
Mateusz Burzyński cd16ba67a6 Get rid of incorrect cache-dependency-path in an /action workflow (#104) 2026-01-16 10:47:52 +00:00
Anna Bocharova 9432a5b737 Revert 4c5cf44 2026-01-16 11:31:38 +01:00
Anna Bocharova 4c5cf444a2 Fix cache-dependency-path in test workflow 2026-01-16 11:28:45 +01:00
David Blass f34379415e add per-agent smoke tests (#100) 2026-01-16 08:00:16 +00:00
Colin McDonnell 5c60791b34 Update workflow 2026-01-15 23:47:40 +00:00
Colin McDonnell 3fa309853b Fix repo slug 2026-01-14 01:44:30 +00:00
Mateusz Burzyński e839fbeacd Perform repository dispatch using the builtin CLI (#65) 2026-01-13 19:54:57 +00:00
David Blass b3e1cf6de3 Update pullfrog.yml to new template with env-based API keys 2026-01-13 11:29:53 -05:00