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>
This commit is contained in:
committed by
pullfrog[bot]
parent
a90743e9fe
commit
cfd38d82fc
+37
-44
@@ -7,22 +7,19 @@ import { getJobToken } from "./token.ts";
|
||||
|
||||
type JsonPromptInput = Extract<ResolvedPromptInput, object>; // not string
|
||||
|
||||
/**
|
||||
* Controls whether the script should check the reason for the workflow termination.
|
||||
* It can be either canceled or failed.
|
||||
* YAML file cannot supply it (not in ENV), so an extra request is required to check it.
|
||||
* */
|
||||
// controls whether the script should check the reason for the workflow termination.
|
||||
// it can be either canceled or failed.
|
||||
// YAML file cannot supply it (not in ENV), so an extra request is required to check it.
|
||||
const SHOULD_CHECK_REASON = true;
|
||||
|
||||
/**
|
||||
* Build error comment body with error message and footer
|
||||
*/
|
||||
function buildErrorCommentBody(params: {
|
||||
type BuildErrorCommentBodyParams = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: string | undefined;
|
||||
isCancellation: boolean;
|
||||
}): string {
|
||||
};
|
||||
|
||||
function buildErrorCommentBody(params: BuildErrorCommentBodyParams): string {
|
||||
const workflowRunLink = params.runId
|
||||
? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})`
|
||||
: "workflow run logs";
|
||||
@@ -38,34 +35,31 @@ function buildErrorCommentBody(params: {
|
||||
return `${errorMessage}${footer}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the progress comment is stuck on "Leaping into action"
|
||||
* Fetches the comment by ID and checks if it starts with LEAPING_INTO_ACTION_PREFIX
|
||||
* Returns the comment ID if stuck, null otherwise
|
||||
*/
|
||||
type ValidateStuckCommentParams = {
|
||||
promptInput: JsonPromptInput | null;
|
||||
octokit: ReturnType<typeof createOctokit>;
|
||||
owner: string;
|
||||
repo: string;
|
||||
};
|
||||
async function validateStuckProgressComment(
|
||||
promptInput: JsonPromptInput | null,
|
||||
octokit: ReturnType<typeof createOctokit>,
|
||||
owner: string,
|
||||
repo: string
|
||||
params: ValidateStuckCommentParams
|
||||
): Promise<number | null> {
|
||||
if (!promptInput?.progressCommentId) {
|
||||
if (!params.promptInput?.progressCommentId) {
|
||||
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
||||
return null;
|
||||
}
|
||||
|
||||
const commentId = parseInt(promptInput.progressCommentId, 10);
|
||||
const commentId = parseInt(params.promptInput.progressCommentId, 10);
|
||||
log.info(`[post] validating progressCommentId from prompt input: ${commentId}`);
|
||||
|
||||
try {
|
||||
const { data: comment } = await octokit.rest.issues.getComment({
|
||||
owner,
|
||||
repo,
|
||||
const commentResult = await params.octokit.rest.issues.getComment({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
|
||||
// check if comment is stuck on "Leaping into action"
|
||||
if (comment.body?.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
||||
if (commentResult.data.body?.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
||||
log.info(`[post] comment ${commentId} is stuck on "Leaping into action"`);
|
||||
return commentId;
|
||||
}
|
||||
@@ -79,31 +73,31 @@ async function validateStuckProgressComment(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the workflow or its steps is cancelled.
|
||||
* While the job is still in_progress, the individual steps may have their conclusions set.
|
||||
*/
|
||||
async function getIsCancelled(params: {
|
||||
type GetIsCancelledParams = {
|
||||
repoContext: ReturnType<typeof parseRepoContext>;
|
||||
octokit: ReturnType<typeof createOctokit>;
|
||||
runIdStr: string | undefined;
|
||||
}): Promise<boolean> {
|
||||
};
|
||||
|
||||
async function getIsCancelled(params: GetIsCancelledParams): Promise<boolean> {
|
||||
if (!params.runIdStr) return false; // can't check without a run ID — assume failure
|
||||
try {
|
||||
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||
const jobsResult = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||
owner: params.repoContext.owner,
|
||||
repo: params.repoContext.name,
|
||||
run_id: Number.parseInt(params.runIdStr, 10),
|
||||
});
|
||||
|
||||
// find current job by matching GITHUB_JOB env var
|
||||
// Note: GITHUB_JOB is the job ID (yaml key), but job.name is the display name
|
||||
// For matrix jobs, the name includes matrix values like "build (ubuntu-latest, node-18)"
|
||||
// So we match jobs that START with the job ID
|
||||
// find current job by matching GITHUB_JOB env var.
|
||||
// GITHUB_JOB is the job ID (yaml key), but job.name is the display name.
|
||||
// for matrix jobs, the name includes matrix values like "build (ubuntu-latest, node-18)"
|
||||
// so we match jobs that START with the job ID
|
||||
const currentJobName = process.env.GITHUB_JOB;
|
||||
const currentJob = currentJobName
|
||||
? jobs.jobs.find((j) => j.name === currentJobName || j.name.startsWith(`${currentJobName} (`))
|
||||
: jobs.jobs[0]; // fallback to first job
|
||||
? jobsResult.data.jobs.find(
|
||||
(j) => j.name === currentJobName || j.name.startsWith(`${currentJobName} (`)
|
||||
)
|
||||
: jobsResult.data.jobs[0]; // fallback to first job
|
||||
|
||||
if (!currentJob) {
|
||||
log.warning("[post] could not find current job");
|
||||
@@ -150,13 +144,12 @@ export async function runPostCleanup(): Promise<void> {
|
||||
const repoContext = parseRepoContext();
|
||||
const octokit = createOctokit(token);
|
||||
|
||||
// validate that progressCommentId from prompt input is stuck on "Leaping into action"
|
||||
const commentId = await validateStuckProgressComment(
|
||||
const commentId = await validateStuckProgressComment({
|
||||
promptInput,
|
||||
octokit,
|
||||
repoContext.owner,
|
||||
repoContext.name
|
||||
);
|
||||
owner: repoContext.owner,
|
||||
repo: repoContext.name,
|
||||
});
|
||||
|
||||
if (!commentId) return log.info("» [post] no stuck progress comment to update, skipping cleanup");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user