* 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>
* review prompt: friendly green callouts + per-section severity emojis
- Replace `[!NOTE]` informational tier and the no-callout minor-suggestions
tier with friendly green blockquotes (`> ✅` / `> 💡`). The two loud
tiers (`[!CAUTION]` / `[!IMPORTANT]`) keep their GitHub admonitions.
- Add a per-`##`-section severity-emoji rule (🚨/⚠️/💡/ℹ️) for
cross-cutting review concerns that don't anchor to a line and would
otherwise be buried in summary content.
- Drop the `<br/>` between summary sections — heading + blank line
carries enough visual spacing.
- Skip the post-run learnings-reflection turn for `IncrementalReview`.
It's the lowest-novelty mode (delta review against existing PR with
prior summary already loaded) and almost never produces durable
learnings — reflection there costs ~$0.50-0.80/run for nothing.
- Surface real error info on `agent-browser` skill install failures
(exit code + stdout + stderr + spawn error). The skills CLI uses a
TUI that prints errors to stdout, so the prior stderr-only logging
silently swallowed every failure.
* review prompt: per-bullet severity emoji + bullets-only sections
Section headings are plain again (no leading severity emoji). Severity
moves to individual bullets so a section that mixes a 🚨 and a 💡 isn't
mislabeled by either. Section bodies are now bullets only — paragraph
prose under a heading is harder to scan and tends to bury the
actionable point.
Bullets can carry indented continuation content (sub-bullets, code
fences, blockquotes) by indenting two spaces under the parent.
* review prompt: cap section length + identifier discipline
Bound each summary section to at most 4 bullets at most 2 lines each,
and explicitly call out identifier-heavy prose as an anti-pattern. The
reader is often a manager or non-author; identifier-dense paragraphs
('foo calls bar.fetch which dispatches to baz via qux...') are
unreadable for them. Default to plain-language behavior descriptions,
name an identifier only when it's the subject of an actionable concern
or a public surface a reader would recognize, target 2-3 backtick
tokens per bullet.
Move the deep-explanation pattern from open blockquote to a default-
collapsed details/summary so depth doesn't dominate the visible body.
* review prompt: hard cap on bullet identifier density + worked rewrite example
Soft 'aim for 2-3 tokens' guidance was ignored — first big-PR e2e
showed 12 of 19 actionable bullets exceeded the target (avg 4.8 tokens,
several over 8). Promote to a hard cap of 3 backticked tokens per
bullet and pair with a concrete bad/good rewrite the agent can pattern-
match against. Also tighten the per-bullet length cap from ~240 to
~200 chars and explicitly call it 'hard cap, not target'.
* review prompt: tighten bullet length cap to 160 chars, dramatize the worked example
V2 e2e test: token discipline improved (4.8 -> 3.3 avg, 12/19 -> 6/14
violations) but length got worse (235 -> 286 chars, 13/14 over the 200
cap). The agent compensated for fewer identifiers with more prose.
Two changes: (1) tighten the cap from ~200 chars to 160 chars / 1
visual line and call out wrap-to-multiple-lines as the failure mode;
(2) rewrite the worked example so the good version is genuinely half
the length of the bad one, not just lower token count. The example was
the thing the agent pattern-matches against; making the good version
~130 chars vs the bad version's ~290 chars sets the right shape.
* review prompt: drop fixed bullet-count cap, keep length + identifier caps
Per user feedback — section length should be governed by content, not
an arbitrary count. Soft guidance ('past ~6, ask whether to split') is
fine; the hard '≤ 4 bullets per section' rule was the wrong shape.
Length cap (160c) and identifier cap (3 backtick tokens) stay; those
target the actual scanability problem.
* review prompt: drop ## subsystem sections, flat 'Issues found' list
Per-section structure forced every concern into a subsystem frame and
made the body read like a series of mini-essays. Replace with two
parts: (1) TL;DR + Key changes as the dispassionate overview, (2) flat
'### Issues found' list ordered by severity, intermixed across files
and subsystems. Per-bullet rules (≤160c, ≤3 backtick tokens, severity
emoji prefix, optional indented continuation) carry over unchanged.
* review prompt: full v6 structure — preamble + cross-cutting H3s + nitpicks
Replaces the flat 'Issues found' bullet list with the iterated v6 shape:
- Preamble is a bolded inline 'Reviewed changes' lead-in plus bullets
plus a collapsed 'Review metadata' block (mode/files/commits/refs/
reviewed commits list/prior pullfrog review/staleness note).
- Each cross-cutting concern gets a '### emoji Title' section. The
visible problem write-up is human-friendly and DESCRIBES THE PROBLEM
ONLY — no asks, no suggested fixes, no 'the right thing to do is'.
- Each section carries a collapsed 'Technical details' block wrapped
in a 4-backtick markdown fence (so it can hold its own 3-tick code
fences cleanly, agent-readable, one-click copyable). Standard four
inner sections: Affected sites, Required outcome, optional Suggested
approach, optional Open questions for the human.
- '### ℹ️ Nitpicks' at the bottom for body-only nits that don't
inline; simple bullets, no technical-details collapse.
- Anti-paragraph-wall rule: never two successive plain paragraphs in
visible '### ' sections; alternate prose with structure.
- Inline-vs-body discipline: anything that anchors to a single line
goes inline, body is for cross-cutting only.
- Drops legacy '### Key changes', '### Issues found', '<b>TL;DR</b>',
and the '<sub>Summary</sub>' line.
* model effort: bump Gemini + GPT to high effort; drop Gemini Pro→Flash subagent
E2E review eval against a substantive billing-module diff surfaced two
related quality gaps:
1. Gemini Pro at thinkingLevel=medium (#663's CI-timeout fix) reviewed
the diff only, took the 0-lens path, and missed a catastrophic
camelCase/snake_case service-vs-schema mismatch. Bumping back to
high — review work is exactly the wrong shape for the medium/high
tradeoff #663 was optimizing for; the per-turn TTFT cost is worth
paying when reasoning IS the value.
2. GPT had no reasoningEffort override, defaulting to upstream medium.
Same diff, similar shallow result vs Claude. Adding reasoningEffort:
high for the curated direct-OpenAI slugs, mirroring the Gemini
pattern (Anthropic separately uses --effort high via the Claude
Code CLI flag in claude.ts).
3. Gemini Pro's subagentModel was 'gemini-flash' — but Google has no
in-between tier between Pro and Flash, and Flash is a meaningful
capability cliff for review work. Dropping the override so subagents
inherit Pro. Cost stays reasonable since Gemini Pro is already the
cheapest of the flagship trio.
Other providers unchanged: Anthropic opus→sonnet and OpenAI gpt→gpt-5.4
remain (each is a one-tier drop to a still-capable sibling).
* model effort: revert orchestrator override, set explicit high on reviewfrog subagent
Reshape the effort design after eval:
- Drop the explicit Gemini and GPT model-level overrides — orchestrators
now run at upstream defaults (Gemini high, GPT-5.x medium). Gemini's
upstream IS high, so this is a no-op there; GPT goes back to upstream
medium for orchestrator-level routing work.
- Add explicit 'high' on the reviewfrog subagent via agent.options.
OpenCode merge order is base ← model.options ← agent.options ← variant
per session/llm.ts:141, so the subagent always runs at high regardless
of which orchestrator dispatched it. Both thinkingConfig.thinkingLevel
(Gemini) and reasoningEffort (GPT) keys included; irrelevant keys are
ignored per provider.
- Bump providers-live timeouts (12min job / 10min step, from 8/6) to
budget for Gemini's TTFT variance at high effort. #663's 4min timeout
was sized for the medium-effort override that's now removed.
* model effort: restore Gemini explicit high override (no-override path breaks)
Bare 'rely on upstream default' for Gemini failed in e2e — removing the
model-level provider config produced 'Function call is missing a
thought_signature' API errors on every gemini-pro run. Even though
upstream opencode's options() returns the same thinkingLevel: high we
were explicitly setting, opencode's resolution path differs subtly
between the two cases. v2's explicit override worked; v3's removal
broke. Reproducible across two consecutive runs.
Restoring the explicit Gemini override (back to v2 design). GPT
orchestrator stays UN-overridden — at upstream default (medium) — since
removing that override didn't trigger the same failure pattern and the
reviewfrog subagent agent.options high override compensates for the
extra depth GPT loses at medium.
* diag: remove reviewfrog agent.options to isolate Gemini thought_signature failure
v3 (no Gemini orch override) failed with thought_signature error. v4
(restored Gemini orch override at v2-equivalent) ALSO failed, even
though the orchestrator config matches v2. The variable between v2
(working) and v4 (failing) is the new reviewfrog agent.options block.
Removing it to confirm — if Gemini works again, the agent.options
addition is the culprit and we need a different shape for it.
* opencode-ai: bump 1.1.56 → 1.15.0 + clean up gemini effort config
opencode-ai@1.1.56 was published 2026-02-10 (3 months old). The Google
API tightened thought_signature validation 24-48h ago (per
https://discuss.ai.google.dev/t/gemini-thought-signature-patch/122555),
and the bug class hits opencode's session→prompt serializer for MCP
tool-call parts (anomalyco/opencode#4832, #8321). Latest stable bumps
us through ~3 months of fixes; needed for Gemini-direct to stop dying
with 'thought_signature is missing' on every multi-turn run.
Companion cleanup: the gemini provider override in opencode.ts had
30-line block of comments, four unused constants, and a 6-line
Object.fromEntries map for two entries. Replaced with one source-of-
truth helper that loops modelAliases, filters provider==='google',
strips the 'google/' prefix, and returns the override map. Adding any
future Google alias to the registry now flows through automatically.
Test added: action/agents/opencode.test.ts asserts the helper covers
every direct-Google alias, strips the prefix correctly, and pins every
entry to thinkingLevel high — catches drift in helper logic without
hardcoding the API ids the test would have to update in lockstep
with the registry.
* fix(workflow): tolerate listJobsForWorkflowRun 404 in resolveRun
PR #750 (docker testing rewrite) replaced the per-call env allowlist
with full process.env passthrough into the test container. That now
leaks GITHUB_RUN_ID + GITHUB_JOB into runs whose MCP token is scoped
to a DIFFERENT repo (e.g. providers-live smoke runs the action against
pullfrog/test-repo with pullfrog/app's run ID). The unconditional
listJobsForWorkflowRun call 404s and crashes the entire run, breaking
every providers-live job on main since #750 landed.
jobId is purely cosmetic (deep-links 'View workflow run' footer to a
specific job vs the run-level URL). Wrapping the API call in try/catch
so a 404 logs a debug message and falls through to undefined jobId is
the right fix — the failure mode is exactly what graceful degradation
is for, and the alternative (filter the env vars at the docker boundary)
re-introduces the kind of allowlist #750 was getting rid of.
* opencode-ai: pin 1.14.51 instead of 1.15.0 (effect refactor breaks JSON output)
opencode 1.15.0 (May 15) ships a major architectural refactor onto
@effect — the run command boots an in-process server via
@opencode-ai/sdk/v2 and the JSON event emission path through that SDK
client doesn't surface on stdout the way our parser expects (CI run
on 1.15.0 produced 0 stdout events but the agent still completed).
Local invocation also hangs at the in-process server boot.
The Gemini thought_signature fixes (the original reason for bumping)
landed earlier in the 1.14.x line, so 1.14.51 (May 14) gets us the
upstream fix without the Effect rewrite. Defer the 1.15.x bump until
we're ready to rewire our parser/spawn around the new SDK.
* opencode-ai: revert to 1.1.56; gha: filter outer-CI workflow-run vars at the docker boundary
Two related changes for the docker testing harness's ergonomics:
1. Revert opencode-ai 1.14.51 → 1.1.56. The 1.14+ line ships an Effect
refactor (the SDK-v2 client + in-process server architecture) that
our --format json parser doesn't speak — even the 1.14.51 release,
pre-dating the 1.15.0 Effect rename, produced 0 stdout events on
our skill-invoke smoke. There's no clean pre-Effect version that
ships the Gemini thought_signature fix; that fix needs a separate
workstream once we're ready to rewire the parser onto SDK v2.
2. Filter outer-CI workflow-run identifiers (GITHUB_RUN_ID, GITHUB_JOB,
GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_REF, GITHUB_SHA, etc.) from
gha.ts's --env-file passthrough. PR #750's full-process.env design
leaks pullfrog/app's CI run identifiers into runs that act against
a different repo (e.g. pullfrog/test-repo); any code path inside
the action that uses them as keys (most notably resolveRun's
listJobsForWorkflowRun lookup) 404s. Filtering them here means
the action sees undefined and skips the lookup, complementing the
defensive try/catch in resolveRun (commit addc76d4). GITHUB_REPOSITORY
and GITHUB_TOKEN are NOT filtered — those are genuinely needed.
Companion to addc76d4 (resolveRun 404 tolerance). The two together
make this class of bug 'either fix would have caught it' rather than
'silently breaks the entire test matrix'.
* fix(deps): sync pnpm-lock.yaml with opencode-ai 1.1.56 manifest revert
Forgot to refresh the lockfile after reverting the manifest in 02c6d8c1.
CI's frozen-lockfile install was failing with 'lockfile: 1.14.51,
manifest: 1.1.56' mismatch.
* docker testing rewrite: bake the image, drop the allowlist, kill the quoting
- new `pnpm gha <script>` wrapper. one entry point for running any node
script in the GHA-like container; replaces the runtime apt-get +
useradd + chown ceremony in `action/utils/docker.ts`.
- `action/Dockerfile` bakes ubuntu:24.04 + node 24 + gh + jq + sudo +
testuser at uid 1000. `action/docker-entrypoint.sh` remaps to the host
uid/gid and `exec`s the requested command — no `bash -c` nesting, no
`escapeForDoubleQuotes`.
- env passthrough: full `process.env` (+ `.env` via dotenv) flows through
`--env-file`, multi-line values via `-e` fallback. drops
`EnvFilterMode` / `testEnvAllowList`.
- image rebuild is content-hash gated on Dockerfile + entrypoint; volume
is versioned by hash so a stale `node_modules` cache from an old image
can't poison a new one.
- `action/play.ts` slimmed to a CLI; `run()` extracted to
`action/utils/runFixture.ts`. drops the `--local` / `PLAY_LOCAL` dual
mode in favor of explicit `play:local` / `runtest:local` scripts.
- `action/test/run.ts` no longer self-relaunches into docker — that's
`gha`'s job now.
- `action/test/coverage.ts` `ALWAYS_RUN_ALL` updated to track the new
files.
- `wiki/docker.md` rewritten (243 → 105 lines). `wiki/action-tests.md`,
`wiki/billing.md`, `wiki/adversarial.md`, `README.md`, `AGENTS.md` all
updated to drop `--local` / `PLAY_LOCAL` references.
verified end-to-end: `pnpm play` runs the default fixture against
pullfrog/scratch, exit 0; `sudo unshare --pid` still works inside the
container; `pnpm runtest` boots through the wrapper.
* gha: address review feedback + 3 related issues found locally
review-flagged:
- bare `pnpm gha --build` now builds the image and exits 0 (was
printing help and exiting 1 — docs claimed it was a valid standalone)
- `initVolumeOwnership` skipped when the named volume already exists;
saves the ~240ms `docker run … chown` on every warm invocation
- `GIT_SSH_COMMAND` gate widened to any `id_*` private key (was hard-
coded to `id_rsa`, leaving ed25519-only linux contributors with the
default ssh config). dropped `-i` so ssh picks whichever key exists
- new `action/.dockerignore` — partial mitigation noted: BuildKit
(default since docker 23) only sends files referenced by the
Dockerfile (~42B in practice), so the perf concern is mostly
hypothetical. file is still worth keeping for `DOCKER_BUILDKIT=0`
fallback and as documented intent for future `COPY . .` additions
related issues found while validating locally:
- `parseArgs` now stops flag-parsing at the first positional (or
literal `--`); `pnpm gha test/run.ts --build` previously
intercepted `--build` as a gha flag instead of forwarding to
`test/run.ts`
- new `pnpm gha --clean` command prunes orphan `pullfrog-gha:*`
images and `pullfrog-gha-node-modules-*` volumes whose hash
doesn't match the current Dockerfile (each Dockerfile/entrypoint
edit creates a fresh hash and orphans the prior pair, ~600MB +
~200MB each — without a cleaner they accumulate silently)
- `--shell` without a TTY now fails fast with an actionable message
before docker is invoked, instead of producing the confusing
`the input device is not a TTY` from docker run
wiki updated: documents `--clean`, the parseArgs passthrough rule,
and a new "Reclaiming disk" section.
* gha: fidelity, flexibility, and signal-safety improvements
investigated local fidelity vs the real GHA ubuntu-24.04 runner and
addressed the gaps that have actually bitten contributors or could.
fidelity (image now matches GHA closer):
- bake build-essential, wget, xz-utils, file alongside the existing
toolset. gh, jq, git, python3, sudo, ssh, build-essential, wget,
xz, file, unzip, curl all present. native module builds (node-gyp,
any package missing arm64 prebuilts) now work; common agent shell
calls don't hit ENOENT
- `host.docker.internal:host-gateway` flag wires the host into the
container's DNS on linux (macOS Docker Desktop bakes it in). lets
scripts that hit a local dev server use `API_URL=http://host.docker.
internal:3100` and work identically on both platforms
- `--init` makes tini PID 1, fixing signal forwarding during the
pre-exec warmup window (Ctrl-C was previously taking up to 10s to
tear down because bash-as-PID-1 swallowed the signal)
- pnpm version is correctly pinned via the workspace's
`packageManager` field — corepack resolves it at install time;
verified via the new `--doctor` command
flexibility (new affordances):
- `pnpm gha --doctor` runs an inside-the-container fidelity audit:
os + arch + node/pnpm/python versions, version snapshots of every
baked tool, env vars (CI, HOME, TMPDIR), uid/gid, and the
host.docker.internal resolution. useful for "works in CI fails
locally" or vice versa
- `pnpm gha --build --no-cache` busts the docker layer cache when
an apt mirror, base image, or external download has changed
upstream
- entrypoint's `pnpm install` warmup is now wrapped in a `flock` on
a file in the shared node_modules volume — concurrent `pnpm gha`
invocations (e.g. play in one terminal, runtest in another)
serialize their install instead of racing
docs:
- new "Gaps (known)" section in wiki/docker.md explicitly calling
out the things this system can't do yet, including the missing
`uses: ./action` semantics gap that
`.github/workflows/action-gha-e2e-adhoc.yml` currently fills via
GHA only (designing a local `pnpm gha-action <fixture>` is on the
roadmap), service containers, parallel-run sharing, and arch
differences (arm64 vs amd64)
* docs: audit + corrections after testing fronts
self-audit pass for stale references and incomplete pointers:
- wiki/browser.md: `Docker (node:24)` → `pnpm gha container (ubuntu:24.04)`.
the substance was right (chrome not preinstalled) but the base image
reference was stale.
- wiki/docker.md: the "Permission errors" troubleshooting line claimed
the node_modules volume is chowned on every run; now correctly says
"owned by the host uid on first creation; warm runs skip the chown"
to match the actual behavior after the initVolumeOwnership fix.
- wiki/action-tests.md: `API_URL` env-var doc now mentions BOTH paths
(`localhost:` from play:local, `host.docker.internal:` from inside
the container). Proxy/router recipe now shows both invocations
side-by-side instead of saying "must use play:local".
- wiki/billing.md: same dual-recipe update for the loop-including-the-
action proxy walkthrough.
- gha.ts header: expanded the usage block to include --clean / --doctor /
--no-cache / --shell-TTY, added the host.docker.internal note, and
pointed at wiki/docker.md for design rationale.
self-document check: a future agent landing on this code can answer
"how do I run a fixture / debug in shell / add a tool / diagnose
fidelity / reach a local dev server" purely from gha.ts header +
wiki/docker.md without spelunking through the entrypoint or git
history.
* PR summary as agent-edited tmpfile snapshot
Replaces the comment-based PR summary path (and the in-progress
update_pr_summary tool from #534) with a snapshot file the agent edits in
place during Review / IncrementalReview / pr-summary Task runs.
The server seeds the tmpfile with the previous snapshot (incremental) or a
stable scaffold (first run), exposes the path via select_mode, and reads it
back at end-of-run to persist to WorkflowRun.summarySnapshot and (when the
prSummaryComment toggle is on) splice into the PR description body.
Why a tmpfile rather than a tool call: incremental snapshot edits are
output-token-cheap when the agent uses native file-editing tools, and
range-diff cleanly across runs because section headings are stable. The
agent never has to regurgitate the full snapshot to update it.
Gating: snapshot generation is opt-in via either prSummaryComment="enabled"
(splice into PR body) or prReReview="enabled" (snapshot feeds future
incremental review runs as context). Users who disable both pay nothing
end-to-end — no seeding, DB write, or body splice.
Behavior changes:
- Drop the Summarize mode and the Summary comment type entirely; the
rolling summary is no longer a separate run shape.
- pull_request_synchronize with re-review off and summary on still
dispatches a silent pr-summary Task, but it edits the snapshot file
instead of posting a fresh comment.
- /api/repo/.../pr/.../summary-comment now returns
{ snapshot: string | null } from the DB instead of fetching a comment via
GraphQL. URL kept stable so deployed older actions degrade gracefully.
- summaryCommentNodeId is retained on WorkflowRun for legacy data and a
future backfill of pre-snapshot comment-based summaries.
Supersedes #534. The commit-tool/sub-agent direction in that PR is
abandoned in favor of this file-based shape.
* address review pass #1: synchronize fallback, splice idempotency, docs
* address review pass #2: in-flight skip should not race summary fallback
* address review pass #3: signal-handler flush, doc clarifications
* address review pass #4: in-flight persist promise + bounded body-splice timeout
* address review pass #5: defensive catch on persist worker, doc nit
* add summary-stale post-run gate
When generateSummary is set, we capture the bytes of the seeded snapshot
file and pass them to the agent's post-run loop alongside the file path.
After each agent attempt, the loop diffs the current file against the
seed; if they're byte-identical the agent never touched it, and we nudge
once via a resume turn (similar to the dirty-tree gate, but soft and
fire-once so smaller models that legitimately decide no edit is warranted
don't burn the retry budget).
Mostly defends against forgetful smaller models on the Review path —
their mode prompt asks them to edit the snapshot file, but the
multi-step instruction can fall through when the diff is large.
* trigger: retry vercel preview build
* fix(action): drop unused re-export that pulled node:fs/promises into next bundle
action/internal/index.ts was re-exporting DEFAULT_PR_SUMMARY_INSTRUCTIONS
from action/utils/prSummary.ts, but nothing in the next.js app imports
it. prSummary.ts uses node:fs/promises, and pullfrog/internal is aliased
into the next bundle by next.config.ts, which made turbopack try to
resolve node:fs/promises in client chunks and fail with:
the chunking context (unknown) does not support external modules
(request: node:fs/promises)
drop the re-export — selectMode.ts (the only real consumer) already
imports it directly from action/utils/prSummary.ts.
* firewall PR summary snapshot from user instructions; resurrect rich format for Review
The agent-internal snapshot (the markdown file the agent edits in place across
runs) is exclusively durable context for future agent runs — user-supplied
summarization instructions warp it and degrade that context. Drop the
prSummaryCommentInstructions read path end-to-end:
- handleWebhook: stop reading prSummaryCommentInstructions, stop passing
prSummaryInstructions through dispatch options
- action payload + ToolState + selectMode addendum: drop the instructions
appendix; the snapshot prompt is fixed, not user-shaped
- TriggersSettings: drop the InstructionsEditor for prSummaryCommentInstructions
- prSummary.ts: reframe DEFAULT_PR_SUMMARY_INSTRUCTIONS as agent-targeted
(durable context, not human-facing prose)
Prisma columns (prSummaryComment, prSummaryCommentInstructions) and the
matching zod schema entry stay for graceful retreat.
Separately, resurrect PR_SUMMARY_FORMAT (deleted along with the Summarize mode
in the original PR) and wire it into Review mode only. Initial PR reviews now
include a structured summary section in the review body using the rich format
(TL;DR, key changes, ## sections with before/after, file-link trails).
IncrementalReview keeps its existing terser bullet-list shape since re-review
bodies are deltas, not introductions. The user-facing review summary and the
agent-internal snapshot are deliberately separate artifacts with separate
prompts and zero shared content.
* address review comments: prompt self-consistency + stale-doc cleanup
PR 568 self-review (4232488109) flagged a self-contradiction the firewall
commit introduced and three stale doc references that survived.
- action/modes.ts: Review-mode step 2's trivial-PR shortcut said `submit
"Reviewed — no issues found." per step 5`, but step 5's rewrite removed
exactly that preamble. Aligned both: trivial PRs and no-actionable-issues
PRs now produce a body that opens with "No new issues found." followed by
the PR summary, so the user gets the headline up front and still sees what
was reviewed.
- docs/pr-reviews.mdx: dropped the "customize the summary style with Summary
instructions in the console" sentence (the editor was removed in the
firewall commit). Replaced with a note that the snapshot uses Pullfrog's
built-in format and is not user-customizable.
- wiki/prompt.md, wiki/modes.md: rewrote the snapshot-prompt entries to
reflect the firewall — DEFAULT_PR_SUMMARY_INSTRUCTIONS is the entire
prompt, prSummaryCommentInstructions is no longer wired in.
* drop orphaned prSummaryCommentInstructions column
Prod audit (455 repos): 5 non-null rows on a single account, all containing the
literal placeholder text from the InstructionsEditor we removed in the firewall
commit. No account has an intentional preference set, so silent-ignore (the
keep-for-retreat option) costs us nothing meaningful while leaving an orphan
column in the schema. Drop it.
- prisma/schema.prisma: remove the column
- prisma/migrations/20260506000000_drop_pr_summary_comment_instructions:
ALTER TABLE ... DROP COLUMN
- utils/schemas/triggers.ts: drop the matching zod entry
* drop body splicing; snapshot is internal-only
User-visible PR summarization continues to ship in Review and IncrementalReview
review bodies (which already render PR_SUMMARY_FORMAT and "Reviewed changes"
respectively). The snapshot tmpfile is now purely durable cross-run agent
context — seed, edit-in-place, save to DB, feed the next run. Massive
simplification: the body splice mechanics, the two-toggle gating matrix, the
summaryHandlingCovered race tracking, and the synchronize summary-only Task
fallback all go away.
Code:
- prSummary.ts: drop splice/strip/marker code (`splicePrSummary`,
`stripExistingSummaryBlock`, `buildSummaryBlock`, `extractPrSummary`,
PULLFROG_SUMMARY_START/END). keep scaffold, instructions, seed/read.
- main.ts: rename persistAndPostSummary -> persistSummary; collapse to a
single DB PATCH. drop pulls.get/pulls.update, drop AbortSignal timeout,
drop in-flight promise machinery, drop prSummaryToBody plumbing.
- ToolState: add summarySeed (replaces local var in main.ts so persist can
compare). drop prSummaryToBody and summaryPersistInFlight.
- persistSummary now compares against the seed and skips the DB write
with a warning when unchanged — saving the seed verbatim is either a
no-op or persists the placeholder scaffold, neither useful.
- postRun.ts: when summary-stale is the only failing gate and the resume
turn itself fails, restore the pre-resume successful result and break.
symmetric with the existing reflection-failure preservation. summary-stale
can no longer flip a successful run to failed.
Webhook:
- pull_request_opened: generateSummary follows prReReview only (the snapshot
has no consumer when re-review is off).
- pull_request_synchronize: collapses to "if prReReview enabled, dispatch
IncrementalReview". the summaryHandlingCovered flag, the same-SHA/in-flight
coordination it was protecting, and the summary-only Task fallback all
delete cleanly.
UI / config:
- drop SummarizePRsTrigger (the toggle gated body splice; with that gone
it has no behavior). drop sidebar entry, console import, Text icon import.
- drop prSummaryComment from triggers zod schema, prisma schema, preview
settings script.
Migration: squash the two existing migrations into one timestamped
20260507000000_pr_summary_snapshot covering all three column changes
(add summarySnapshot on workflow_runs, drop prSummaryCommentInstructions
and prSummaryComment on repos). repo convention is one migration per PR.
Action: bump 0.0.203 -> 0.0.205 (payload contract changed: prSummaryToBody
removed; main is at 0.0.204).
Out-of-diff cleanup:
- review.ts:190 + review.test.ts:651 — "Reviewed — no issues found." ->
"No new issues found." to match the canonical body in modes.ts.
Verified: pnpm typecheck clean, pnpm lint clean, postRun + review tests
pass, dev DB reset against production and the squashed migration applied
cleanly (summarySnapshot present, prSummaryComment / prSummaryCommentInstructions
both gone).
* re-orient snapshot toward functional summary; drop prior-review-feedback section
Empirical audit on preview-568 PR #5 showed the snapshot IS load-bearing
for the orchestrator: lens-dispatch prompts on incremental runs carried
forward context from the snapshot's risk register (e.g. "the JSDoc
explicitly scopes to code points — do not flag grapheme-cluster issues"
on the surrogate-pair fix run, "consistency with native padStart" on the
padStart-added run). The orchestrator was reading the snapshot, reasoning
about it, and using it to anti-prime / focus subagents — exactly the
high-leverage path. My earlier "snapshot is write-only" claim was wrong.
The shape, however, was steering it toward review-history-log instead of
functional summary. This commit re-orients:
- prSummary.ts: replace the four-section scaffold (~580 chars of placeholder
italics under "What this PR does / Key changes / Risk / Reviewed in prior
runs") with a minimal seed (~150 chars: just a header + a one-line
comment about what the file is for). different PRs warrant different
organization; forcing a refactor and a feature into the same template
is procrustean. minimal seed also makes the unchanged-from-seed gate
in persistSummary more sensitive.
- selectMode.ts addendum: rewrite around three principles. (1) the snapshot
is a FUNCTIONAL summary of what the PR does and the risks it carries,
not a chronological review log — commit history can already be
reconstructed from list_pull_request_reviews. (2) the orchestrator should
USE the snapshot during triage and dispatch — concrete example given of
carrying snapshot context into subagent lens prompts. (3) structure is
the agent's call; stable headings make snapshots range-diff cleanly when
they fit, but riff when they don't.
- modes.ts IncrementalReview: drop the "Prior review feedback" checklist
from the user-facing review body (step 6b gone, step 7 ELSE IFs cleaned
up). It duplicated content that's already covered by the Reviewed-changes
bullets and tracked durably in the snapshot for the next agent run; in
the user-facing body it was noise. step 3 still fetches prior reviews
but its role is now just filtering aggregation in step 5, not rendering.
- AGENTS.md: codify "no follow-ups" rule. when an issue is identified
during code review, fix it in this PR — PR scope does not constrain
quality. follow-up TODOs are forbidden as a substitute for doing the
work now.
Empirical evidence supporting the re-orientation:
- Run 25568912293 (PR#5 incr1, surrogate-pair fix): orchestrator's
correctness lens dispatch said "Do NOT flag grapheme-cluster issues
— the JSDoc scopes to code points." The grapheme-cluster framing was
not in the diff; it was downstream of the snapshot's prior risk-section
framing of truncate's contract. Snapshot influencing dispatch.
- Run 25569054779 (PR#5 incr2, padStart added): orchestrator's correctness
lens dispatch enumerated edge cases including "consistency with native
String.prototype.padStart contract" and "fill = multi-code-point string
(e.g. emoji)". Both threads carried over from the snapshot's prior
truncate code-point-vs-code-unit discussion. Snapshot informing the
shape of what was looked for.
The cost of maintaining the snapshot (~800 tokens, ~$0.005/run) is
trivially affordable when it materially improves orchestrator triage
on the 1-5 lenses dispatched per review.
* refactor progress comments into a single bundled type + helper module
introduce ProgressComment ({ id, type: "issue" | "review" }) as the canonical handle for
the GitHub comment a run uses to report progress, and route every read/update/delete/create
through a single helper module (action/utils/progressComment.ts). previously every site that
touched the progress comment hardcoded octokit.rest.issues.*Comment, which made adding a
second comment type (review-thread replies) require duplicating the same branch in 6+ places
— the same shape that bit pullfrog/app#445.
new capability: when the address-reviews trigger fires for a one-off review comment, the
"Leaping into action" comment is now posted as a reply in that review thread instead of as
a top-level PR timeline comment. the helper handles failure (e.g. parent comment deleted)
by silently falling back to a top-level issue comment, so the run never loses its progress
surface.
changes:
- action/utils/progressComment.ts (new) — ProgressComment type + getProgressComment,
updateProgressComment, deleteProgressCommentApi, createLeapingProgressComment. uses a
structural Octokit interface to bridge the @octokit/rest version mismatch between the
action package (v22) and the root project (v21).
- action/internal/index.ts — re-export the new types and helpers for cross-boundary use.
- action/external.ts, action/utils/payload.ts — replace progressCommentId: string with
progressComment: { id: string, type: "issue" | "review" } in WriteablePayload + JsonPayload.
wire-format breaking, no legacy fallback (in-flight runs across the deploy lose their
progress comment, fine).
- action/mcp/server.ts — ToolState.progressCommentId becomes
progressComment: ProgressComment | null | undefined (same tristate semantics).
- action/main.ts, action/mcp/comment.ts, action/utils/errorReport.ts,
action/utils/postCleanup.ts — every issues.*Comment call against the progress comment
routes through the helper module. zero hardcoded API branching outside the helper.
- utils/github/triggerWorkflow.ts — drop createLeapingComment + updateCommentToLeaping;
dispatchAndTrackWorkflow gains a resolution chain (existingComment → replyToReviewComment
→ triggeringIssue → none) and an existingComment: ProgressComment param plus
replyToReviewComment: { pullNumber, commentId }.
- utils/webhooks/handleWebhook.ts — dispatch closure threads replyToReviewComment through;
the one-off review comment branch passes it and skips the now-redundant eyes reaction
on the comment we're about to reply to.
- app/trigger/[owner]/[repo]/[number]/page.tsx, utils/github/runActionLocal.ts,
app/api/cli/dispatch/route.ts, app/api/dispatch-workflow/route.ts — call sites updated to
new shape.
no schema or DB column changes. the existing WorkflowRun.progressCommentId column is still
written by id only; type lives only on the in-flight payload, which is sufficient for
runtime since it's the only thing that needs to know which API to call.
* anneal pass 1: fallback visibility + stale doc/comment updates
- progressComment.ts: when reviewReply→issue fallback fires, prepend a [!NOTE] callout
with a permalink back to the original review comment. without this, the parent comment
showed no eyes reaction (deliberately skipped) and no reply, leaving the user with no
signal that anything happened.
- wiki/post-cleanup.md: update progressCommentId references to progressComment, document
the new helper-based dispatch by type.
- wiki/main.md: update initToolState({ progressCommentId }) → ({ progressComment })
in the resolver-chain diagram.
- action/main.ts, action/mcp/review.ts: update two stale comments that referenced the
old field name.
* anneal pass 2: post-cleanup detection through fallback notice + log cleanup
- isLeapingIntoActionCommentBody: strip a leading GFM blockquote/alert before
testing the leaping prefix. without this, the [!NOTE] callout that the
reviewReply→issue fallback prepends would prevent post-cleanup from
recognizing the stuck "Leaping into action..." comment, leaving it permanently
on the PR timeline if the workflow died before any progress update.
- progressComment helper: switch from log.warning (action-flavored, emits a
::warning:: GitHub Actions annotation) to console.warn so the helper doesn't
pollute Vercel logs when invoked from the webhook context.
- triggerWorkflow.ts: drop the duplicate caller-side log on review-reply
failure — the helper already speaks loudly. Reword the catch-branch log to
reflect that it now only fires when both the reply AND the helper's internal
fallback failed.
- progressComment.ts: document that the [!NOTE] fallback notice is overwritten
on the first report_progress call, and explain the trade-off vs persisting
it through the action payload + ToolState.
* debloat: drop the [!NOTE] fallback callout
Reverting two pieces from the prior anneal pass:
- progressComment.ts: drop the [!NOTE] callout that the reviewReply→issue fallback
prepended to the leaping body. It disappeared on the agent's first report_progress
call, which made it half-committed to visibility — worse than either properly
persisting it (real engineering) or leaving the fallback silent (current choice).
The console.warn diagnostic and the workflow-run footer link in the leaping
comment itself give us enough signal for the rare case where both API endpoints
fail at once.
- isLeapingIntoActionCommentBody: revert the leading-blockquote stripping; only
needed to compensate for the [!NOTE] callout.
Keeping: the console.warn-vs-log.warning fix (real cross-runtime concern), the
duplicate-log drop in triggerWorkflow.ts, the wiki updates, and the two stale
source-comment fixes.
* fix: prevent stranded task list overwriting post-cleanup message
When a run is cancelled, the action's todoTracker may have an HTTP write in
flight to GitHub when SIGTERM lands. The action process dies, but the request
data has already left the socket — GitHub processes it and updates the comment
body to the (stale) task list. Meanwhile post-cleanup, running in a separate
process, writes the "This run was cancelled 🛑" message. If the tracker's
in-flight write happens to land *after* post-cleanup's write, the user never
sees the cancellation message.
Two-layer fix:
- Action side: cancel the tracker in the SIGTERM signal handler so no new
debounced writes get scheduled. This shrinks the race window but can't
un-send a request already on the wire.
- Post-cleanup side: after writing, verify the body landed and re-issue if
another write clobbered ours. Loops up to 3× with a 3s settle delay so
delayed in-flight writes from the dying action have time to arrive before
our read-back check decides whether to retry.
* lint: import createLeapingProgressComment from pullfrog/internal in test script
* address bot review findings: reply-target root, version bump, GET error handling
Three real findings from the bot reviews on #567 plus a small DRY pass:
1. handleWebhook reply-target: `newComments[0]` may be a reply, not a
top-level review comment. `getReviewCommentsWithReplies` returns root +
replies for any thread the review touched, and `pull_request_review_id`
filtering only narrows by *which review submitted*, not *root vs reply*.
When a user submits a single reply as their entire review (e.g. replying
to someone else's comment to ping @pullfrog), the reply ID flowed through
to `createReplyForReviewComment`, which 422s on replies-to-replies and
degraded to a top-level issue comment — exactly the polluted-PR-timeline
behavior this PR was built to remove. Walk up `in_reply_to` from the
already-fetched thread data to find the root and reply there instead.
2. action/package.json: bumped 0.0.202 → 0.0.204. main is at 0.0.203 and
our wire format changed; without a bump validateCompatibility can't
surface the mismatch on the deploy boundary, and the merge would have
gone backwards.
3. postCleanup writeAndVerify: distinguish a thrown verify-GET from a
"body got overwritten" mismatch. Treating a transient 5xx/rate-limit GET
the same as a clobber wasted PUT attempts and printed a misleading
"in-flight writes kept clobbering us" warning. We trust our PUT (which
returned 200) and exit instead of amplifying writes against a flaky API.
4. Small DRY: extracted parseProgressComment for the
`{ id: string; type } -> ProgressComment` parse that had drifted across
server.ts and postCleanup.ts.
releases the Review/IncrementalReview no-progress carve-out in
action/utils/run.ts (71dff24c) that has been sitting unpublished in
main since May 4. fixes the long-standing false-failure where Review
runs would error with "agent completed without reporting progress"
even after successfully submitting a review (issue #569).
* Fix Node 24 action bootstrap fallback
Resolve the published CLI launcher through PATH so runners missing a sibling Node 24 npx can still start, and make post cleanup recognize prefixed leaping comments.
* Bump Pullfrog action package version
Ensure the Node 24 bootstrap and post-cleanup fixes publish to npm and move the v0 action tag.
* Walk PATH for corepack and npx in action bootstrap
ensureActionDependencies and runPackageCli now resolve corepack/npx through
PATH the same way as the npx-via-PATH fix, so Node 24 runner pools missing
either sibling can still bootstrap. Also adds a Zod-mirror settings helper
for the preview-556 repo and documents the per-PR settings workflow.
* log when corepack PATH fallback is used
PR CI kept breaking on upstream catalog drift (new model ships on models.dev,
OpenRouter renames an id, etc.) — failures unrelated to the PR's contents.
split the model-alias test suite so PRs only see pure-logic checks, and push
the external-state drift + end-to-end coverage to main.
test organization:
- action/test/models.test.ts keeps pure invariants: openRouterResolve
completeness and fallback-chain resolution. runs on every PR.
- action/test/models-catalog.main.test.ts gets the 4 network-dependent
describes (models.dev validity x2, OpenRouter API validity, latest-model
snapshot). runs only on main push via a dedicated vitest config
(vitest.main.config.ts + `pnpm test:catalog`).
new CI jobs in .github/workflows/test.yml:
- models-catalog: `pnpm test:catalog` on every main push. detects upstream
catalog drift so we can react at the next convenient window.
- models-live: 38-entry matrix that invokes the agent harness end-to-end
against the real provider for each alias in models.ts. generated from
action/test/list-aliases.ts. runs only on main push AND only when
resolution-affecting files changed (action/models.ts, action/package.json,
action/agents/**) — the exact shape of the opus 4.7 incident.
test/run.ts: PULLFROG_MODEL now flows through from process.env so the live
matrix can pin an alias per job without the per-agent default clobbering it.
Made-with: Cursor
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
npx was running with cwd set to the action's own directory, which has
package.json with "name": "pullfrog". npm treats the local package as
satisfying the request and skips the registry fetch, then fails to find
the binary (sh: 1: pullfrog: not found). use GITHUB_WORKSPACE instead.
Made-with: Cursor
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
action/.husky prepare script was overriding root husky config, so the
pre-push hook (lint + typecheck + test) never ran. merged the lockfile
sync pre-commit into root .husky/pre-commit and removed action/.husky.
also auto-fixed biome format/import-sort errors from last commit.
Made-with: Cursor
simplify installation-not-found flow by removing ownerHasInstallation
field and collapsing the "selected repos" vs "no access" branches into
a single message with a confirm prompt. improve spinner/log copy
throughout init (secrets, model, workflow, test run).
backfill missing jobId on workflow-run redirect page by querying the
GitHub API for the pullfrog job when jobId is null. add 600ms delay
in handleWorkflowRunInProgress before fetching jobs to avoid racing
the job assignment.
Made-with: Cursor
the postinstall script referenced scripts/generate-proxies.ts which isn't
included in the published npm package, silently breaking every npx install.
replaced the proxy stub approach with turbopack resolveAlias and webpack
conditionNames so both bundlers resolve pullfrog imports to TypeScript
source directly — matching what tsc already does via customConditions.
also moves PR summary format from handleWebhook into modes.ts so the
summarize mode prompt includes it directly.
Made-with: Cursor
PRs created as draft (or by automation tokens that suppress workflow
triggers) never ran preview-create because the workflow only listened
for opened/synchronize. Adding ready_for_review as a trigger ensures
the preview repo gets created when a draft PR is marked ready.
Also makes preview-create.ts idempotent by catching 422 (repo already
exists) so it's safe if both opened and ready_for_review fire.
Made-with: Cursor
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
models can now be marked `deprecated: true` with a `fallback` slug
pointing to a replacement. `resolveCliModel` follows the chain
recursively (with cycle detection) until it finds a non-deprecated
model. this keeps deprecated models in the registry for backward
compatibility instead of removing them.
marks opencode/mimo-v2-pro-free as deprecated with fallback to
opencode/nemotron-3-super-free.
Made-with: Cursor
* Update waitlist, run ralph experiments
* fix PR files pagination: use octokit.paginate() for >100 files
* fix garbled FAQ answer on landing page
* track cache read/write tokens in OpenCode agent usage
* wrap dispatch() calls in try/catch to prevent webhook retries on transient failures
* replace raw error messages with generic responses in API routes
* guard request.json() calls with try-catch returning 400 on malformed bodies
* log warning when GraphQL review thread/comment counts hit pagination limits
* reduce review comment cache TTL from 24 hours to 10 minutes
* use select instead of include for proxyKey in workflow run queries
* align Claude agent activity timeout to 5 minutes to match OpenCode agent
* add in-memory dedup for PR close webhooks to prevent duplicate indexing
* extract isPullfrogLogin() helper for shared Pullfrog detection logic
* check response.ok on log fetch in checkSuite.ts
* add 10s timeouts to checkSuite API calls and log fetch
* parallelize proxy key usage API calls with Promise.allSettled
* fix three typos on landing page: colleage, dectects, reponse
* move MAX_STDERR_LINES constant to shared.ts
* add indexes on Repo.accountId and PFUser.accountId FK columns
* remove unused Permission enum from Prisma schema
* populate author and keywords in action/package.json
* use crypto.timingSafeEqual for all secret comparisons
* add missing env vars to globals.ts: R2, webhook, and API secrets
* remove commented-out UserRepo model from Prisma schema
* replace console.log/error with log utility in production API routes
* replace catch(error: any) with proper type guards in getUserRole
* remove stale TODO comment on console page
* handle repository_transferred webhook to update owner
* show toast.error instead of console.error on mode/workflow mutation failures
* add Space key handler for keyboard navigation on workflow run links
* replace role=link spans with button elements for proper accessibility
* add root 404 page with Pullfrog branding
* update ISSUES.md: mark completed items
* mark remaining low-priority UX items as addressed
* add error logging alongside toasts, add check script, update ralph commands
* address review feedback: squash migrations, fix try/catch scope, wire up globals consumers
- squash drop_permission_enum migration into add_indexes migration (one migration per PR)
- move getPullRequest() outside try/catch in mention handler so errors aren't mislogged as "dispatch failed"
- restore key ID in proxyKeys.ts Promise.allSettled error log
- remove accidental asdf.txt and ralph.md files
- wire up globals.ts exports to consumers (r2-uploads, r2-private, verifyHookdeckSignature, sync-usage, forwardPreviewWebhook, dispatch-workflow)
Made-with: Cursor
* update model snapshot (qwen3.6-plus-preview renamed to qwen3.6-plus)
Made-with: Cursor