97937f46f7
* console UI improvements and cleanup - add verify workflow button and API endpoint for manual installation check - move env var check into PromptBox as blocking overlay (hoisted to RepoConsole) - extract FlagsCheatSheet modal, replace verbose flag hints everywhere - add info popovers for repo setup / post-checkout script descriptions - remove unused prAutoFixCiFailures schema fields and migration - default mentionAllowNonCollaborator to disabled for safety on public repos - update docs for triggers and getting started Co-authored-by: Cursor <cursoragent@cursor.com> * add diagnostic logging for push_branch bug investigation temporary [push-debug] logs to trace why getPushDestination falls back to origin/<localBranch> instead of using the correct remote branch name for same-repo PRs. Co-authored-by: Cursor <cursoragent@cursor.com> * add git config diagnostic to verify original bug cause Co-authored-by: Cursor <cursoragent@cursor.com> * temporarily disable StoredPushDest to test git config path Co-authored-by: Cursor <cursoragent@cursor.com> * remove diagnostic logging for push_branch investigation verified that StoredPushDest fix works correctly on preview repo. both the stored dest path and the git config fallback resolve to the correct remote branch in the GitHub Actions environment. Co-authored-by: Cursor <cursoragent@cursor.com> * fix formatting in AgentSettings and TriggersSettings Co-authored-by: Cursor <cursoragent@cursor.com> * pass derived env var state to PromptBox instead of raw secrets data eliminates duplicated derivation logic between RepoConsole and PromptBox by passing envVarMissing, envVarChecking, and agentKeyNames as props. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: prevent duplicate comment after PR review deletes progress comment progressCommentId now uses three states: undefined (no comment yet), number (active), null (deliberately deleted). After create_pull_request_review deletes the progress comment, subsequent report_progress calls skip instead of creating a new comment. Co-authored-by: Cursor <cursoragent@cursor.com> * effort descriptions, test ordering, husky, docs images, typo fix - rewrote delegation effort level descriptions to per-level breakdown - action-agents now waits for action-agnostic; action-agnostic waits for root - added husky + lint-staged (biome check --write on staged files) - updated triggers docs images and triggers.mdx content - fixed "figured" → "figures" typo on landing page - updated pnpm-lock.yaml Co-authored-by: Cursor <cursoragent@cursor.com> * Commit --------- Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.9 KiB
TypeScript
129 lines
4.9 KiB
TypeScript
import { type } from "arktype";
|
|
import { Effort } from "../external.ts";
|
|
import type { Mode } from "../modes.ts";
|
|
import { markActivity } from "../utils/activity.ts";
|
|
import { log } from "../utils/cli.ts";
|
|
import { resolveSubagentInstructions } from "../utils/instructions.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
import { execute, tool } from "./shared.ts";
|
|
|
|
export const DelegateParams = type({
|
|
mode: type.string.describe(
|
|
"the name of the mode to delegate to (e.g., 'Build', 'Plan', 'Review', 'Fix', 'AddressReviews')"
|
|
),
|
|
"effort?": Effort.describe(
|
|
'effort level for the subagent: "mini" (low-effort and fast, only for simple tasks), "auto" (medium-effort, good for typical tasks that don\'t require significant reasoning), or "max" (high-effort, good for PR reviews and complex coding tasks)'
|
|
),
|
|
"instructions?": type.string.describe(
|
|
"optional additional context or instructions for the subagent — use this to pass results from earlier delegations or narrow the subagent's focus"
|
|
),
|
|
});
|
|
|
|
// exported for unit testing
|
|
export function resolveMode(modes: Mode[], modeName: string): Mode | null {
|
|
return modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
|
|
}
|
|
|
|
// cap subagent output to avoid bloating the orchestrator's context window.
|
|
// the orchestrator needs enough to understand what happened, not the full NDJSON stream.
|
|
const MAX_OUTPUT_CHARS = 20_000;
|
|
|
|
// exported for unit testing
|
|
export function truncateOutput(output: string | undefined): string | undefined {
|
|
if (!output || output.length <= MAX_OUTPUT_CHARS) return output;
|
|
const truncated = output.slice(-MAX_OUTPUT_CHARS);
|
|
return `[truncated — showing last ${MAX_OUTPUT_CHARS} chars]\n${truncated}`;
|
|
}
|
|
|
|
export function DelegateTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "delegate",
|
|
description:
|
|
"Delegate a task to a subagent with a specific mode and effort level. The subagent runs as a separate process with the mode's step-by-step instructions.",
|
|
parameters: DelegateParams,
|
|
execute: execute(async (params) => {
|
|
// guard: prevent subagent recursion
|
|
if (ctx.toolState.delegationActive) {
|
|
return {
|
|
error: "delegation is not available inside a delegated subagent",
|
|
};
|
|
}
|
|
|
|
// resolve mode
|
|
const selectedMode = resolveMode(ctx.modes, params.mode);
|
|
if (!selectedMode) {
|
|
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
|
return {
|
|
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
|
|
availableModes: ctx.modes.map((m) => ({
|
|
name: m.name,
|
|
description: m.description,
|
|
})),
|
|
};
|
|
}
|
|
|
|
const effort = params.effort ?? "auto";
|
|
|
|
// track state
|
|
ctx.toolState.selectedMode = selectedMode.name;
|
|
ctx.toolState.delegationActive = true;
|
|
|
|
log.info(
|
|
`» delegating to ${selectedMode.name} mode (effort=${effort})${params.instructions ? " with orchestrator instructions" : ""}`
|
|
);
|
|
|
|
// keep the process-level activity timeout alive while the subagent runs.
|
|
// agent CLIs can have long silent thinking phases (>60s) with no stdout,
|
|
// which would trigger the activity timeout. the overall run timeout (default 1h)
|
|
// is the real safety net for stalled agents.
|
|
const keepAliveInterval = setInterval(markActivity, 30_000);
|
|
|
|
try {
|
|
// build subagent payload with effort override
|
|
const subagentPayload = { ...ctx.payload, effort };
|
|
|
|
// build subagent instructions with mode prompt baked in
|
|
const subagentInstructions = resolveSubagentInstructions({
|
|
payload: subagentPayload,
|
|
repo: ctx.repo,
|
|
modes: ctx.modes,
|
|
mode: selectedMode,
|
|
orchestratorInstructions: params.instructions,
|
|
});
|
|
|
|
// spawn subagent — reuses same MCP server, same toolState
|
|
const result = await ctx.agent.run({
|
|
payload: subagentPayload,
|
|
mcpServerUrl: ctx.mcpServerUrl,
|
|
tmpdir: ctx.tmpdir,
|
|
instructions: subagentInstructions,
|
|
});
|
|
|
|
log.info(`» delegation to ${selectedMode.name} completed (success=${result.success})`);
|
|
|
|
return {
|
|
success: result.success,
|
|
mode: selectedMode.name,
|
|
effort,
|
|
output: truncateOutput(result.output),
|
|
error: result.error,
|
|
};
|
|
} catch (err) {
|
|
// normalize agent crashes into the same return shape as clean failures
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
log.error(`» delegation to ${selectedMode.name} crashed: ${errorMessage}`);
|
|
return {
|
|
success: false,
|
|
mode: selectedMode.name,
|
|
effort,
|
|
error: errorMessage,
|
|
};
|
|
} finally {
|
|
clearInterval(keepAliveInterval);
|
|
// always release the lock so the orchestrator can delegate again
|
|
ctx.toolState.delegationActive = false;
|
|
}
|
|
}),
|
|
});
|
|
}
|