Commit Graph

14 Commits

Author SHA1 Message Date
wolfy 2aca1a3aa3 feat: adapt pullfrog for gitea + ollama 2026-05-31 03:16:29 -05:00
Colin McDonnell f662b1a0c8 unify per-run token + cost accounting + persist to WorkflowRun (#547)
* unify per-run token + cost accounting across agents

every agent harness now logs the same 5-column (or 6 with cost) table and
populates the same AgentUsage contract, regardless of agent or upstream
provider. previously OpenCode and the Claude fallback path emitted a 3-col
table whose "Input Tokens" was actually only the non-cached delta, silently
dropping cache read/write — real runs were being reported at ~0.4% of their
true input (e.g. one baseline showed Input=30 while step_finish events
summed to cache_read=724,753).

changes:
- add logTokenTable helper in action/agents/shared.ts with stable columns:
  Input | Cache Read | Cache Write | Output | Total | Cost ($). cost
  column renders only when a value is known.
- action/agents/opencode.ts: accumulate step_finish.part.tokens AND
  step_finish.part.cost (sourced from models.dev inside opencode —
  confirmed working across Anthropic, OpenAI, Google, xAI, DeepSeek,
  Moonshot, and OpenRouter). drop the event.stats.total_tokens fallback
  since that payload has no cache breakdown.
- action/agents/claude.ts: success-path now treats input_tokens as the
  non-cached field (matching OpenCode semantics), carries
  cache_read_input_tokens / cache_creation_input_tokens separately, and
  captures total_cost_usd from the final result event. the per-message
  fallback accumulator now captures cache fields too so it's no longer
  lossy when the result event never fires.
- formatUsageSummary gains a Cost ($) column that matches the stdout
  table row-for-row; missing values render as "—".
- scripts/token-usage.ts parses all three historical formats (new 5-col,
  legacy 4-col Claude success, legacy 3-col lossy) and explicitly flags
  the lossy runs instead of averaging misleading values.

validation (pnpm play --local, identical "say hello" prompt):

  agent+model                           Input  CacheR  CacheW  Output  Total   Cost
  OpenCode + Anthropic Sonnet 4.6           4  41,177  20,735     129  62,045  $0.0921
  Claude CLI  + Anthropic Sonnet 4.6        9  80,133  11,611     389  92,142  $0.0766
  OpenCode + OpenAI codex-mini         10,893  46,976       0     606  58,475  $0.0059
  OpenCode + Google Gemini 3 Flash         —       —       —       —       —  $0.0114
  OpenCode + xAI Grok 4 Fast                —       —       —       —       —  $0.0035
  OpenCode + DeepSeek Chat             18,854       0       0       1  18,855  $0.0053
  OpenCode + Moonshot Kimi K2.5             —       —       —       —       —  $0.0106
  OpenCode + OpenRouter→Anthropic           —       —       —       —       —  $0.0617
  OpenCode + OpenRouter→OpenAI              —       —       —       —       —  $0.0038

* isolate play.ts from developer gitconfig

play.ts is a CI-emulator but inherits the developer's user- and system-scope
gitconfig. a common local convenience — url."git@github.com:".insteadOf
"https://github.com/" to force SSH auth — gets applied at read time on every
git call inside the temp repo, causing `git remote get-url --push origin`
to return an SSH URL instead of the stored HTTPS one. pullfrog_push_branch's
validatePushDestination (correctly) treats that as tampering and blocks the
push. the agent then burns the full MAX_COMMIT_RETRIES budget trying
workarounds that can't beat a user-scope insteadOf rule, turning a trivial
"say hello" run into a 1.35M-token session.

point GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM at /dev/null inside run() so
the play process and its spawned agent see the same empty gitconfig that
a real CI runner would. CI has no rewrites, so this is a no-op there; dev
machines get CI-identical git state. SSH client config (~/.ssh/config and
keys) is separate from gitconfig and is unaffected, so setupTestRepo's SSH
clone still works locally. setupGit only writes --local scope, so nothing
downstream depends on user-scope values.

verification: with the scratch repo cleaned up and this isolation in place,
OpenCode + Anthropic on the same "say hello" prompt goes from 1,349,654
tokens / $2.00+ to 62,045 tokens / $0.0921 — no retry loop, no push blocks.

* persist aggregated token + cost usage to WorkflowRun

AgentUsage has been memory-only — rendered into the GitHub step summary
and then discarded when the runner tears down. that made questions like
"avg cost per customer per day" require log-spelunking. persist it:

- add Int? columns for inputTokens / outputTokens / cacheReadTokens /
  cacheWriteTokens and a Decimal? costUsd column on workflow_runs.
  Int4's 2.1B ceiling is ~200x larger than any realistic run so BigInt
  would be overkill. costUsd uses the same default Decimal precision
  as existing money columns (accounts.usageUsd, proxy_keys.hwmUsage).

- extend PATCH /api/workflow-run/[runId] to accept the new numeric
  fields alongside the existing artifact strings. per-field type
  validation ensures the allowlist stays scalar-safe and rejects
  negative / non-finite values.

- generalize patchWorkflowRunFields in the action so it accepts a
  mixed string/number payload, and add an aggregateUsage(entries)
  helper that sums per-agent AgentUsage records into a single patch.

- call the reporter from main.ts's outer finally block, gated on
  toolContext. this is the shared cleanup path that every agent
  implementation flows through — claude.ts, opencode.ts, and any
  future harness all push their AgentUsage into toolState.usageEntries
  via the same line 468, so one finally-block call covers them all.
  running in finally also means partial usage gets persisted even
  when the agent errored out mid-run.

* anneal token + cost accounting

follow-up polish from a review pass:

- aggregate usage across commit-retry iterations inside each agent harness.
  previously runClaude / runOpenCode returned only the final retry's usage,
  so any run that hit the dirty-tree retry loop under-counted tokens and
  cost in both the stdout table and the WorkflowRun row. added a shared
  mergeAgentUsage helper in agents/shared.ts; both harnesses now fold each
  iteration's usage into a running total and return the sum.

- scripts/token-usage.ts now handles the unified format with or without
  the Cost ($) column. previously the int-only number regex rejected
  decimals and the 5-cell length check rejected 6-cell rows, so logs
  from post-cost-tracking runs fell through to "no token table". the
  parser now accepts both 5- and 6-cell unified rows, splits int vs
  decimal cells, and averages reported Cost alongside the tokens.

- PATCH /api/workflow-run/[runId] now rejects INT field values above
  INT4_MAX (2_147_483_647) so a malformed payload gets a clean 400
  instead of propagating a Prisma error. also defends against a
  compromised runner sending a deliberately huge value.

- clarifying comments: opencode.ts documents that step_finish.part.cost
  is a per-step delta (empirically verified), main.ts explains that
  toolState.usageEntries already carries merged per-retry usage so
  aggregateUsage just sums entries (one per agent.run()).

- tests for aggregateUsage and mergeAgentUsage — 12 new cases covering
  empty / partial / multi-agent inputs and the "keep undefined" semantic
  that prevents spurious zeros from being persisted.

- drop `as number` cast in logTokenTable — narrow via const instead.

* anneal: clamp INT overflow + guarantee mergeAgentUsage immutability

second review pass surfaced two defensive gaps:

- a single token field exceeding INT4_MAX would pass the client but be
  rejected by the server's per-field validator, writing a partial row
  with some NULLs where sums belonged. clamp in aggregateUsage so the
  wire payload is always self-consistent across all numeric columns,
  with a loud warning so the clamp doesn't silently swallow weirdness.

- mergeAgentUsage's single-sided branches returned the input reference.
  callers treat AgentUsage as immutable but future callers might not;
  always return a fresh shallow copy instead. two new tests guarantee
  the no-mutation-leak property.

no behavior change in the happy path — INT4_MAX is ~200x the largest
realistic per-run token count.

* anneal: resilient usage persistence + cross-platform null device

third review pass surfaced three small issues:

- main.ts finally block: writeGitHubUsageSummaryToFile throwing would
  skip the WorkflowRun usage PATCH. both are independent best-effort
  cleanup tasks — wrap the former in catch so a filesystem failure
  doesn't block DB persistence.

- AgentUsage.inputTokens had no jsdoc explaining that it's the full
  billable input (cached + non-cached). the same word "Input" means
  "non-cached only" in the stdout/markdown tables (derived by
  subtraction). document the semantic so dashboards querying
  WorkflowRun.inputTokens don't misinterpret it.

- play.ts gitconfig isolation was hard-coded to "/dev/null" which
  doesn't exist on Windows. use `os.devNull` for cross-platform
  parity (resolves to `\\.\nul` on win32). the project is Linux-only
  in CI so this only helps local Windows contributors, but it's a
  zero-cost swap.

also updated the finally-block caveat comment: usage is only pushed
to toolState.usageEntries when agent.run() returns an AgentResult,
not when the timeout race rejects — so timed-out runs don't
persist partial usage. documented instead of trying to thread state
through Promise.race.

* anneal: NaN-guard cost accumulators + clarify inputTokens docs

final polish from review round 4:

- guard both cost accumulators (opencode step_finish.part.cost and claude
  result.total_cost_usd) with Number.isFinite. `typeof x === "number"`
  accepts NaN, and one NaN `+=` would poison the running total for the
  whole session.

- reword prisma schema comment on WorkflowRun usage fields to call out
  that cacheReadTokens / cacheWriteTokens are SUB-totals within
  inputTokens (not additional tokens on top). prevents future dashboards
  from double-counting by ~2x when summing "total tokens used".
2026-04-20 21:27:54 +00:00
Colin McDonnell f1400ffb7c remove cost logging from agent runs; extract secrets into own tab
- stop capturing/displaying total_cost_usd from Claude CLI (theoretical cost is misleading for subscription users)
- remove Cost column from action logs table and GitHub Job Summary
- extract SecretsCard into its own sidebar tab with KeyRound icon
- remove children prop from AgentSettingsSection

Made-with: Cursor
2026-03-31 06:14:04 +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 1ed3da8273 add magenta log prefixes for delegated subagents (#387)
When delegate() runs multiple subagents in parallel, their logs
interleave without visual distinction. Use AsyncLocalStorage to
automatically prefix every log line inside runSubagent() with the
task label in magenta (e.g. [frontend-review]).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 05:53:18 +00:00
David Blass cfd38d82fc refactor delegation system, add PR summary comments, and improve code quality (#334)
* refactor delegation system and add PR summary comments

Delegation system:
- replace mode-based delegation with select_mode → delegate two-step flow
- orchestrator crafts self-contained subagent prompts (clean context — no system/repo/event instructions leak)
- add role-based tool filtering via FastMCP authenticate hook (?role=subagent hides orchestrator-only tools)
- add select_mode tool for orchestrator guidance per mode
- add ask_question tool for lightweight research subagents
- extract shared subagent lifecycle into subagent.ts (create, complete, stdout, instructions)
- route set_output to per-subagent state when activeSubagentId is set
- track per-subagent state (SubagentState Map) replacing boolean delegationActive flag
- capture and aggregate AgentUsage across all agents (claude, codex, gemini, opencode)
- write usage summary table to GitHub job summary
- block built-in subagent spawning (Task for Claude, Task(*) for Cursor)
- increase activity timeout from 60s to 300s (subagent thinking phases)
- fix gh CLI misguidance in system prompt — explicitly forbid usage

PR summary comments:
- add prSummaryComment trigger (DB schema + migrations + Zod + UI toggle)
- dispatch mini-effort summary job alongside PR review on pr.created
- add update_pull_request_body MCP tool
- add defaultEffort option to webhook dispatch

Hardening:
- rewrite delegate/selectMode tests with simulated state management
- add toolFiltering.test.ts for role extraction, canAccess, set_output routing
- remove non-null assertions for PULLFROG_TEMP_DIR (proper error throws)
- use fetchWithRetry for direct tarball downloads
- DRY fix for rate limit check in test runner

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

* fix: add type keyword to Effort import in handleWebhook.ts

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

* clean up delegation system, improve code quality across the codebase

- simplify delegate tool to instructions + effort params with subagent lifecycle in subagent.ts
- add select_mode and ask_question orchestrator-only tools with canAccess filtering
- replace delegate.test.ts/selectMode.test.ts with toolFiltering.test.ts (live MCP integration)
- add set_output routing for subagent context and AgentUsage tracking across all agents
- add PR summary comment trigger (schema, UI, webhook dispatch with silent flag)
- add update_pull_request_body MCP tool
- fix changed-agents.sh to always include claude canary for non-agent action changes
- fix cursor pagination bug in getSelectedInstallationReposPage
- remove destructuring patterns, inline type definitions, and unsafe type casts
- replace non-null assertions with explicit checks in install.ts
- convert multi-param functions to single param objects (postCleanup, runActionLocal, etc.)
- use isHttpError helper in API routes instead of catch-any patterns
- add adhoc test fixtures for delegation scenarios (context isolation, error handling, synthesis, etc.)

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

* no subagent mutation, one mcp per subagent

* address review feedback: parallel-safe usage tracking, subagent isolation, minor improvements

* fix subagent state isolation: replace Object.freeze with shallow copy

Object.freeze throws TypeErrors when subagent tools (checkout_pr,
report_progress) write scalar properties to toolState. A shallow copy
achieves the same isolation for scalar fields while allowing tools to
work normally. Shared references (subagents Map, usageEntries array)
remain shared for coordination.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-02-22 14:12:43 +00:00
pullfrog[bot] 70f1c47a28 Audit core.warning/core.error usage (#269)
* Stop using command-based logs for warnings and errors

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

* revert

* tweak

* de-noise

* Remove redundant ts() timestamp prefix from log calls

* Restore timestamped logging and refine debug output routing.

Bring back timestamp prefixes for standard logs and make log.debug emit via core.debug when runner debug is enabled, while still surfacing debug lines for --debug runs.

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

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
2026-02-19 23:11:47 +00:00
Colin McDonnell 78cf05f111 Clean up url resolution 2026-02-13 14:24:46 +00:00
Mateusz Burzyński 267a4586ae Use a stable NEXT_PUBLIC_VERCEL_BRANCH_URL for short links (#287)
* Use a stable `NEXT_PUBLIC_VERCEL_BRANCH_URL` for short links

* Update JSDoc reference to NEXT_PUBLIC_VERCEL_BRANCH_URL

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-02-13 12:27:27 +00:00
pullfrog[bot] fb80343ffd feat(agents): add thinking time logging between tool calls (#244)
* feat(agents): add thinking time logging between tool calls

Adds a ThinkingTimer utility that tracks the gap between tool results and
the next tool call. When the gap exceeds 3 seconds, it logs the duration
with a stopwatch emoji (⏱️ 4.2s).

Uses performance.now() for high-resolution timing and Intl.NumberFormat
for rendering duration in seconds with optional fraction digits.

Integrated across all 5 agents: Claude, Codex, Cursor, Gemini, OpenCode.

Closes #127

* fix: adjusting tests for mocking performance.now.

* fix: reducing diff for claude.

* fix: rm unused args for claude.

* rm unused args for codex.

* fix: rm unused args for gemini.

* fix: rm unused args for opencode.

* mv THINKING_THRESHOLD.

* rev: I decided to pospone node:perf_hooks integration since it requires more comprehensive refactoring.

* fix: using Intl unit formatting.

* tests for ThinkingTimer.

* fix: narrow unit.

* fix: making durationFormatter a class instance property since using one agent per run.

* fix: inverting condition in markToolCall.

* thinking timer improvements and fix actions/checkout v6 auth

- thinking timer: use » chevron and "thought for X seconds" format
- thinking timer: add debug timestamps for sanity checking
- demote PID namespace isolation logs to debug
- remove redundant "setting up git authentication" log
- fix duplicate Authorization header with actions/checkout v6: clean up
  includeIf credential entries that v6 persists via external config files

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

* standardize tool call log prefix to » double chevron

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

* update timer tests for new thinking log format

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

---------

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>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 06:17:43 +00:00
Colin McDonnell f77fecc2a0 Update 2026-01-28 07:47:52 +00:00
David Blass 04cc24bf64 improve nobash tests, fix cursor, reenable CI (#138) 2026-01-21 01:37:56 +00:00
Colin McDonnell 9e019d89d2 Clean up actions and payloads (#98)
* Clean up actions and payloads

* Clean up action

* Cleanup
2026-01-16 07:16:25 +00:00
Colin McDonnell 2d2d31adfa Code style (#97)
* Cleanup

* fix: populate deny array before assigning to config, add CursorCliConfig type

* Fix deny array ordering and add CursorCliConfig type

Move deny array population before config declaration to avoid
relying on reference semantics. Add proper type interface for
the CLI config object.

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-01-15 22:06:53 +00:00