8c6cd2bda2
* cancel + restart workflow run when @pullfrog mention is edited - add `WorkflowRun.triggeringCommentId` (BigInt?, indexed) so the webhook handler can find the run that was fired by a given comment - thread `triggeringCommentId` through `reserveRun` / `triggerWorkflow` - factor `dispatchMentionRun` out of `issue_comment_created` so the same shape is reused on edit - replace the `issue_comment_edited` stub: re-evaluates the trigger gate, cancels prior runs (`octokit.rest.actions.cancelWorkflowRun` + DB status='cancelled'), then re-dispatches with a `previousRunsNote` appended to `eventInstructions` so the agent acknowledges the prior run/PR/artifacts in its summary - if the edit removes `@pullfrog`, cancel only (no restart) Co-authored-by: Cursor <cursoragent@cursor.com> * thread previousRunsNote via dedicated payload field user prompt has precedence over eventInstructions, so stuffing the prior-runs note into eventInstructions made it vanish whenever the trigger comment contained an @pullfrog mention (which is always for the edit path). pass it as its own payload field and render it alongside the user's task so the agent actually sees it. * delete cancelled run's progress comment on edit-restart so the issue thread doesn't accumulate "This run was cancelled" stubs on every edit. only deletes for runs we actively cancel; runs that were already terminal (e.g. completed before the edit) keep their summary comment in the thread, and `previousRunsNote` links to it so the new agent can reference prior work. post-cleanup is race-safe: the action's `validateStuckProgressComment` swallows the 404 from the deleted comment and exits cleanly, so the old run's post step cannot clobber the new run's leaping comment. Co-authored-by: Cursor <cursoragent@cursor.com> * also cancel + delete progress comment when triggering comment is deleted mirrors the edit-removes-@pullfrog path: when an @pullfrog comment that fired a run is hard-deleted, look up any prior runs by triggeringCommentId, GH-cancel running ones, and delete their leaping progress comments. skips trigger-gate re-eval (we're tearing down a run, not firing one) and performs no restart. reuses the existing cancelRunsForTriggeringComment helper; the returned previousRunsNote is discarded since no dispatch follows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: move cancellation before trigger gate in issue_comment_edited cancelRunsForTriggeringComment now runs before the triggerEnabled check, so edits that remove @pullfrog still cancel in-flight runs even when the repo mention trigger is currently disabled (e.g. for non-collaborators). * anneal: scope cancel updates per-row + simplify edit gate - replace blanket updateMany on (triggeringCommentId, repoId) with per-row, status-guarded updates so a parallel handler's freshly-reserved run cannot be clobbered into cancelled by a racing edit delivery. - drop wasMention/isMention early-break in issue_comment_edited; always run cancelRunsForTriggeringComment (DB is the canonical "did this comment ever trigger a run" source). closes the missing-changes.body.from edge and lets us tear down a still-running prior run even if the admin disabled the mention trigger mid-flight. - buildPreviousRunsNote returns undefined (not "") when no link lines materialize. - doc cleanups + wiki/modes.md addendum noting issue_comment_edited / _deleted now drive cancel + restart. Co-authored-by: Cursor <cursoragent@cursor.com> * address review feedback on cancel/restart semantics - guard workflow_run.completed update against status='cancelled' so a successful-but-uncancellable GH Actions job can't resurrect a cancelled row (and re-bill it) via the completed webhook. - bucket only status='completed' runs into `preserved` in cancelRunsForTriggeringComment; cancelled/failed prior runs have stubs as their progress comment, not summaries worth referencing. - emit previousRunsNote for the runId-null cancel case so the restarted agent always knows when it's superseding a prior dispatch. - drop the agent-forbidden `gh pr list` hint and soften 'was cancelled' to 'was signalled to cancel' in the note body. - post a fallback comment when the edit-path dispatch fails (prior run already torn down and progress comment already deleted). - symmetrize the delete-handler's pullfrog guard with the edit handler (key off hook.comment.user, not hook.sender). - trim misleading comments on the per-row DB update guard. --------- 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>
297 lines
9.0 KiB
TypeScript
297 lines
9.0 KiB
TypeScript
/**
|
|
* ⚠️ LIMITED IMPORTS - this file is imported by Next.js and must avoid pulling in backend code.
|
|
* All shared constants, types, and data used by both the Next.js app and the action runtime live here.
|
|
* Other files in action/ re-export from this file for backward compatibility.
|
|
*/
|
|
|
|
// mcp name constant
|
|
export const pullfrogMcpName = "pullfrog";
|
|
|
|
/** @see {@link file://./agents/shared.ts} Agent interface that uses this type */
|
|
export type AgentId = "claude" | "opencode";
|
|
|
|
/**
|
|
* format a tool name the way each agent's MCP client presents it to the model.
|
|
* claude code: mcp__pullfrog__select_mode
|
|
* opencode: pullfrog_select_mode
|
|
*/
|
|
export function formatMcpToolRef(agentId: AgentId, toolName: string): string {
|
|
switch (agentId) {
|
|
case "claude":
|
|
return `mcp__${pullfrogMcpName}__${toolName}`;
|
|
case "opencode":
|
|
return `${pullfrogMcpName}_${toolName}`;
|
|
default:
|
|
return agentId satisfies never;
|
|
}
|
|
}
|
|
|
|
// model alias registry lives in models.ts — re-exported here for shared access
|
|
export type { ModelAlias, ModelProvider, ProviderConfig } from "./models.ts";
|
|
export {
|
|
getModelEnvVars,
|
|
getModelProvider,
|
|
getProviderDisplayName,
|
|
modelAliases,
|
|
parseModel,
|
|
providers,
|
|
resolveCliModel,
|
|
resolveDisplayAlias,
|
|
resolveModelSlug,
|
|
resolveOpenRouterModel,
|
|
} from "./models.ts";
|
|
|
|
// tool permission types shared with server dispatch
|
|
export type ToolPermission = "disabled" | "enabled";
|
|
export type ShellPermission = "disabled" | "restricted" | "enabled";
|
|
export type PushPermission = "disabled" | "restricted" | "enabled";
|
|
|
|
// workflow yml permissions for GITHUB_TOKEN
|
|
export type WorkflowPermissionValue = "read" | "write" | "none";
|
|
export type WorkflowIdTokenPermissionValue = "write" | "none";
|
|
|
|
export interface WorkflowPermissions {
|
|
actions?: WorkflowPermissionValue;
|
|
attestations?: WorkflowPermissionValue;
|
|
checks?: WorkflowPermissionValue;
|
|
contents?: WorkflowPermissionValue;
|
|
deployments?: WorkflowPermissionValue;
|
|
discussions?: WorkflowPermissionValue;
|
|
"id-token"?: WorkflowIdTokenPermissionValue;
|
|
issues?: WorkflowPermissionValue;
|
|
models?: WorkflowPermissionValue;
|
|
packages?: WorkflowPermissionValue;
|
|
pages?: WorkflowPermissionValue;
|
|
"pull-requests"?: WorkflowPermissionValue;
|
|
"repository-projects"?: WorkflowPermissionValue;
|
|
"security-events"?: WorkflowPermissionValue;
|
|
statuses?: WorkflowPermissionValue;
|
|
}
|
|
|
|
// permission level for the author who triggered the event
|
|
// matches GitHub's permission levels: admin > write > maintain > triage > read > none
|
|
export type AuthorPermission = "admin" | "maintain" | "write" | "triage" | "read" | "none";
|
|
|
|
// base interface for common payload event fields
|
|
interface BasePayloadEvent {
|
|
issue_number?: number;
|
|
is_pr?: boolean;
|
|
branch?: string;
|
|
/** title of the issue/PR (or contextual title for comments) */
|
|
title?: string;
|
|
/** primary content for this trigger (issue body, PR body, comment body, review body, etc.) */
|
|
body?: string | null;
|
|
comment_id?: number;
|
|
review_id?: number;
|
|
review_state?: string;
|
|
thread?: any;
|
|
pull_request?: any;
|
|
check_suite?: {
|
|
id: number;
|
|
head_sha: string;
|
|
head_branch: string | null;
|
|
status: string | null;
|
|
conclusion: string | null;
|
|
url: string;
|
|
};
|
|
comment_ids?: number[] | "all";
|
|
/** permission level of the user who triggered this event */
|
|
authorPermission?: AuthorPermission;
|
|
/** when true, runs silently without progress comments (e.g., auto-labeling) */
|
|
silent?: boolean;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface PullRequestOpenedEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_opened";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
body: string | null;
|
|
branch: string;
|
|
}
|
|
|
|
interface PullRequestReadyForReviewEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_ready_for_review";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
body: string | null;
|
|
branch: string;
|
|
}
|
|
|
|
interface PullRequestReviewRequestedEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_review_requested";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
body: string | null;
|
|
branch: string;
|
|
}
|
|
|
|
interface PullRequestReviewSubmittedEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_review_submitted";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
review_id: number;
|
|
/** review body is the primary content */
|
|
body: string | null;
|
|
review_state: string;
|
|
branch: string;
|
|
}
|
|
|
|
interface PullRequestReviewCommentCreatedEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_review_comment_created";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
comment_id: number;
|
|
/** comment body is the primary content (null if already in prompt) */
|
|
body: string | null;
|
|
thread?: any;
|
|
branch: string;
|
|
}
|
|
|
|
interface IssuesOpenedEvent extends BasePayloadEvent {
|
|
trigger: "issues_opened";
|
|
issue_number: number;
|
|
title: string;
|
|
body: string | null;
|
|
}
|
|
|
|
interface IssuesAssignedEvent extends BasePayloadEvent {
|
|
trigger: "issues_assigned";
|
|
issue_number: number;
|
|
title: string;
|
|
body: string | null;
|
|
}
|
|
|
|
interface IssuesLabeledEvent extends BasePayloadEvent {
|
|
trigger: "issues_labeled";
|
|
issue_number: number;
|
|
title: string;
|
|
body: string | null;
|
|
}
|
|
|
|
interface IssueCommentCreatedEvent extends BasePayloadEvent {
|
|
trigger: "issue_comment_created";
|
|
comment_id: number;
|
|
/** distinguishes this from PR review comments (which use pull_request_review_comment_created) */
|
|
comment_type: "issue";
|
|
/** comment body is the primary content (null if already in prompt) */
|
|
body: string | null;
|
|
issue_number: number;
|
|
// PR-specific fields (only present when is_pr is true)
|
|
is_pr?: true;
|
|
branch?: string;
|
|
title?: string;
|
|
}
|
|
|
|
interface CheckSuiteCompletedEvent extends BasePayloadEvent {
|
|
trigger: "check_suite_completed";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
body: string | null;
|
|
pull_request: any;
|
|
branch: string;
|
|
check_suite: {
|
|
id: number;
|
|
head_sha: string;
|
|
head_branch: string | null;
|
|
status: string | null;
|
|
conclusion: string | null;
|
|
url: string;
|
|
};
|
|
}
|
|
|
|
interface WorkflowDispatchEvent extends BasePayloadEvent {
|
|
trigger: "workflow_dispatch";
|
|
}
|
|
|
|
interface FixReviewEvent extends BasePayloadEvent {
|
|
trigger: "fix_review";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
review_id: number;
|
|
/** when true, only address comments the triggerer approved with 👍 (vs all comments) */
|
|
approved_only?: boolean | undefined;
|
|
}
|
|
|
|
interface ImplementPlanEvent extends BasePayloadEvent {
|
|
trigger: "implement_plan";
|
|
issue_number: number;
|
|
plan_comment_id: number;
|
|
/** plan content is the primary content (null if already in prompt) */
|
|
body: string | null;
|
|
}
|
|
|
|
interface PullRequestSynchronizeEvent extends BasePayloadEvent {
|
|
trigger: "pull_request_synchronize";
|
|
issue_number: number;
|
|
is_pr: true;
|
|
title: string;
|
|
body: string | null;
|
|
branch: string;
|
|
/** SHA before the push -- used to compute incremental range-diff between PR versions */
|
|
before_sha: string;
|
|
}
|
|
|
|
interface UnknownEvent extends BasePayloadEvent {
|
|
trigger: "unknown";
|
|
}
|
|
|
|
// discriminated union for payload event based on trigger
|
|
// note: all events use issue_number for consistency (PRs are issues in GitHub's API)
|
|
export type PayloadEvent =
|
|
| PullRequestOpenedEvent
|
|
| PullRequestReadyForReviewEvent
|
|
| PullRequestSynchronizeEvent
|
|
| PullRequestReviewRequestedEvent
|
|
| PullRequestReviewSubmittedEvent
|
|
| PullRequestReviewCommentCreatedEvent
|
|
| IssuesOpenedEvent
|
|
| IssuesAssignedEvent
|
|
| IssuesLabeledEvent
|
|
| IssueCommentCreatedEvent
|
|
| CheckSuiteCompletedEvent
|
|
| WorkflowDispatchEvent
|
|
| FixReviewEvent
|
|
| ImplementPlanEvent
|
|
| UnknownEvent;
|
|
|
|
// writeable payload type for building payloads
|
|
export interface WriteablePayload {
|
|
"~pullfrog": true;
|
|
/** semantic version of the payload to ensure compatibility */
|
|
version: string;
|
|
/** provider/model slug (e.g. "anthropic/claude-opus") */
|
|
model?: string | undefined;
|
|
/** the user's actual request (body if @pullfrog tagged) */
|
|
prompt: string;
|
|
/** github username of the human who triggered this workflow run */
|
|
triggerer?: string | undefined;
|
|
/** event-level instructions for this trigger type (flag-expanded server-side) */
|
|
eventInstructions?: string | undefined;
|
|
/**
|
|
* system-injected note about prior superseded runs (e.g. when the
|
|
* triggering @pullfrog comment is edited). rendered alongside the user's
|
|
* prompt rather than via eventInstructions so it survives user-prompt
|
|
* precedence.
|
|
*/
|
|
previousRunsNote?: string | undefined;
|
|
/** event data from webhook payload - discriminated union based on trigger field */
|
|
event: PayloadEvent;
|
|
/** timeout for agent run (e.g., "10m", "1h30m") - defaults to "1h" */
|
|
timeout?: string | undefined;
|
|
/** working directory for the agent */
|
|
cwd?: string | undefined;
|
|
/** pre-created progress comment (ID + type) for updating status */
|
|
progressComment?: { id: string; type: "issue" | "review" } | undefined;
|
|
/** when true, seed the PR summary tmpfile + persist edits at run end */
|
|
generateSummary?: boolean | undefined;
|
|
}
|
|
|
|
// immutable payload type for agent execution
|
|
export type Payload = Readonly<WriteablePayload>;
|