From b748355cbe053240df7e351e3275e82372e42d7b Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 14 Apr 2026 20:37:20 +0000 Subject: [PATCH] homepage copy refresh + fix skills CLI installation (#539) * add wiki/betterstack.md documenting log querying, request-ID grouping, and MCP usage Made-with: Cursor * fix webhook race conditions: separate runId assignment from data updates the workflow_run webhook handler had a race where concurrent handlers assigned the same runId to different pending records. the loser's P2002 silently dropped data updates (jobId, status, completedAt). fix by splitting into two steps: assignRunId() handles the race-safe unique assignment, then data updates always target where: { runId } so they hit the correct record regardless of who won. also downgrade R2 ObjectLockedByBucketPolicy errors from error to warn level since duplicate webhook deliveries writing the same key is expected under load. Made-with: Cursor * homepage copy refresh + fix skills CLI installation - update hero to "Agent x GitHub" with new subtagline - rewrite intro paragraphs: workflow, harness capabilities, billing - add feature sections: bash isolation, headless browser, MCP tools - update FAQ answers, footer attribution, free-for-oss copy - update APP_DESCRIPTION for SEO - fix skills install: use npx from tmpdir instead of local binary (the bundled action has no node_modules; running npx from tmpdir avoids project .npmrc with pnpm settings breaking binary resolution) - instruct agents to use markdown image syntax in upload_file tool - start dependency installation eagerly from main.ts - include event title in task instructions Made-with: Cursor --- main.ts | 3 +++ mcp/dependencies.ts | 5 +++-- mcp/upload.ts | 2 +- utils/buildPullfrogFooter.ts | 4 ++-- utils/instructions.ts | 3 ++- utils/skills.ts | 34 +++++++++++++++++++++++++++++----- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/main.ts b/main.ts index ede1253..479ab0e 100644 --- a/main.ts +++ b/main.ts @@ -2,6 +2,7 @@ import * as core from "@actions/core"; import { deleteProgressComment, reportProgress } from "./mcp/comment.ts"; +import { startInstallation } from "./mcp/dependencies.ts"; import { initToolState, startMcpHttpServer, @@ -307,6 +308,8 @@ export async function main(): Promise { log.info(`» MCP server started at ${mcpHttpServer.url}`); timer.checkpoint("mcpServer"); + startInstallation(toolContext); + if (payload.model) log.info(`» model: ${payload.model}`); if (payload.timeout) log.info(`» timeout: ${payload.timeout}`); log.info(`» push: ${payload.push}`); diff --git a/mcp/dependencies.ts b/mcp/dependencies.ts index 9b5c37a..93c351f 100644 --- a/mcp/dependencies.ts +++ b/mcp/dependencies.ts @@ -67,9 +67,10 @@ Inspect the repository structure to determine how dependencies should be install } /** - * start dependency installation in the background (non-blocking, idempotent) + * start dependency installation in the background (non-blocking, idempotent). + * called eagerly from main.ts at startup and also available via MCP tools. */ -function startInstallation(ctx: ToolContext): void { +export function startInstallation(ctx: ToolContext): void { // already started or completed - do nothing if (ctx.toolState.dependencyInstallation) { return; diff --git a/mcp/upload.ts b/mcp/upload.ts index 4b1c94c..36bac81 100644 --- a/mcp/upload.ts +++ b/mcp/upload.ts @@ -14,7 +14,7 @@ export function UploadFileTool(ctx: ToolContext) { return tool({ name: "upload_file", description: - "upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed.", + "upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed. when embedding uploaded images in comments or PR bodies, always use markdown image syntax: ![description](url)", parameters: UploadFileParams, execute: execute(async (params) => { // read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded diff --git a/utils/buildPullfrogFooter.ts b/utils/buildPullfrogFooter.ts index 37de638..9c25b57 100644 --- a/utils/buildPullfrogFooter.ts +++ b/utils/buildPullfrogFooter.ts @@ -13,7 +13,7 @@ export interface WorkflowRunFooterInfo { } export interface BuildPullfrogFooterParams { - /** add "Triggered by Pullfrog" link */ + /** add "via Pullfrog" link */ triggeredBy?: boolean; /** add "View workflow run" link */ workflowRun?: WorkflowRunFooterInfo | undefined; @@ -52,7 +52,7 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string { } if (params.triggeredBy) { - parts.push("Triggered by [Pullfrog](https://pullfrog.com)"); + parts.push("via [Pullfrog](https://pullfrog.com)"); } if (params.model) { diff --git a/utils/instructions.ts b/utils/instructions.ts index 507617a..05c59b2 100644 --- a/utils/instructions.ts +++ b/utils/instructions.ts @@ -153,9 +153,10 @@ ${ctx.userQuoted}`; const eventInstructions = ctx.payload.eventInstructions ?? ""; if (eventInstructions) { + const parts = [ctx.eventTitle, eventInstructions].filter(Boolean); return `************* YOUR TASK ************* -${eventInstructions}`; +${parts.join("\n\n")}`; } return ""; diff --git a/utils/skills.ts b/utils/skills.ts index a54580a..e1a2a70 100644 --- a/utils/skills.ts +++ b/utils/skills.ts @@ -1,9 +1,19 @@ import { spawnSync } from "node:child_process"; -import { resolve } from "node:path"; +import { tmpdir } from "node:os"; import { log } from "./cli.ts"; +import { getDevDependencyVersion } from "./version.ts"; -const skillsBin = resolve(import.meta.dirname, "../node_modules/.bin/skills"); +const skillsVersion = getDevDependencyVersion("skills"); +/** + * install a skill globally via the `skills` CLI. + * + * runs `npx skills add --skill -g` with `cwd` set to os tmpdir + * so npm doesn't walk up and find a project-level `.npmrc` with pnpm-specific + * settings (e.g. `public-hoist-pattern`) that break npx binary resolution. + * the `-g` flag writes to `$HOME/.agents/skills/` which is controlled by + * `params.env.HOME` (the fake HOME), so cwd has no effect on install location. + */ export function addSkill(params: { ref: string; skill: string; @@ -11,9 +21,21 @@ export function addSkill(params: { agent: string; }): void { const result = spawnSync( - skillsBin, - ["add", params.ref, "--skill", params.skill, "-g", "-a", params.agent, "-y"], + "npx", + [ + "-y", + `skills@${skillsVersion}`, + "add", + params.ref, + "--skill", + params.skill, + "-g", + "-a", + params.agent, + "-y", + ], { + cwd: tmpdir(), env: { ...process.env, ...params.env }, stdio: "pipe", timeout: 30_000, @@ -22,6 +44,8 @@ export function addSkill(params: { if (result.status === 0) { log.info(`installed ${params.skill} skill (${params.agent})`); } else { - log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`); + const stderr = (result.stderr?.toString() || "").trim(); + const errorMsg = result.error ? result.error.message : stderr; + log.info(`${params.skill} skill install failed: ${errorMsg}`); } }