b6658ddbc1
* add workflows permission to git token and waitlist improvements - add `workflows` to `InstallationTokenPermissions` type in both action and API token routes - include `workflows: write` in the git token so agents can push workflow file changes - add `githubFollowers` field to WaitlistSignup schema with migration - add script to populate waitlist followers from GitHub API - add frog-green-square-border logo asset Co-authored-by: Cursor <cursoragent@cursor.com> * improve CI, agent logging, token permissions, and delegation guardrails - add format check and build step to root CI job - standardize agent model/effort log lines across all agents - fix GitHub App permissions types to match OpenAPI schema (workflows is write-only) - improve delegation error message to prevent subagent recursion - demote noisy OpenCode stderr to debug level - add subagent delegation rules to resolved instructions Co-authored-by: Cursor <cursoragent@cursor.com> * fix graphql partial error handling, update delegation message, add workflow_run fixtures Co-authored-by: Cursor <cursoragent@cursor.com> * remove module-level env var throws that break CI build Co-authored-by: Cursor <cursoragent@cursor.com> * fix logging bug and type hole from PR review - use batch-local notFound counter so per-batch log doesn't undercount - add workflows to WorkflowTokenPermissions so wire type matches what action sends Co-authored-by: Cursor <cursoragent@cursor.com> * lazy-init appOctokit to fix next build without env vars Co-authored-by: Cursor <cursoragent@cursor.com> * drop pnpm build from CI test workflow Co-authored-by: Cursor <cursoragent@cursor.com> * fix delegate-effort test regex to match actual log format, disable fail-fast for agnostic tests the test was matching `running \w+ with effort=auto` but the actual log line from shared.ts is `» effort: auto`. also temporarily set fail-fast: false on action-agnostic so all failures surface at once. Co-authored-by: Cursor <cursoragent@cursor.com> * disable fail-fast in action workflow too, relax ci.test.ts to match both workflow files now use fail-fast: false for agnostic tests so all matrix jobs run to completion. the ci consistency test now checks that the two workflows agree rather than requiring true. Co-authored-by: Cursor <cursoragent@cursor.com> * restore fail-fast: true now that all agnostic tests pass Co-authored-by: Cursor <cursoragent@cursor.com> * skip agent tests in CI when agent harness file didn't change adds action/test/changed-agents.sh which reads the PR diff (via dorny/paths-filter) and outputs only agents whose harness file was modified. the action-agents matrix now uses this dynamic list instead of a hardcoded array, so e.g. a PR touching only cursor.ts runs 6 jobs instead of 30. Co-authored-by: Cursor <cursoragent@cursor.com> * update ci.test.ts to validate dynamic agent matrix the test now checks that the matrix references the changes job output and that changed-agents.sh correctly discovers all agents. Co-authored-by: Cursor <cursoragent@cursor.com> * parallelize action jobs and use claude canary fallback for shared changes runs action-agents in parallel with action-agnostic after root/changes, and updates changed-agents logic so shared or non-harness action runtime changes run only claude while harness-specific edits run only those changed agents. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
5.2 KiB
TypeScript
130 lines
5.2 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 subagent. you are already running as a delegated subagent. complete the task directly using the available tools. do not attempt to delegate further. your last agent message will be passed to your supervisor and it will decide what to do next.",
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}),
|
|
});
|
|
}
|