Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/agents/definitions/debug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ prompts:
Analyze and process the work item with ID: <%= it.cardId %>. The work item data has been pre-loaded.

backend:
enableStopHooks: true
needsGitHubToken: false


Expand Down
1 change: 0 additions & 1 deletion src/agents/definitions/email-joke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ prompts:
<% } %>

backend:
enableStopHooks: false
needsGitHubToken: false


Expand Down
6 changes: 4 additions & 2 deletions src/agents/definitions/implementation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ prompts:
Analyze and process the work item with ID: <%= it.cardId %>. The work item data has been pre-loaded.

backend:
enableStopHooks: true
needsGitHubToken: true
requiresPR: true
hooks:
scm:
enableStopHooks: true
requiresPR: true
postConfigure: sequentialGadgetExecution


Expand Down
1 change: 0 additions & 1 deletion src/agents/definitions/planning.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ prompts:
Analyze and process the work item with ID: <%= it.cardId %>. The work item data has been pre-loaded.

backend:
enableStopHooks: false
needsGitHubToken: false


Expand Down
42 changes: 39 additions & 3 deletions src/agents/definitions/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
AgentCapabilities,
AgentDefinition,
ContextStepName,
ScmHooks,
SupportedTrigger,
} from './schema.js';
import { CONTEXT_STEP_REGISTRY, PRE_EXECUTE_REGISTRY } from './strategies.js';
Expand All @@ -49,6 +50,10 @@ export interface AgentProfile {
blockGitPush?: boolean;
/** Whether the agent must create a PR for success (e.g., implementation) */
requiresPR?: boolean;
/** Whether the agent must submit a review before finishing (e.g., review) */
requiresReview?: boolean;
/** Whether the agent must have pushed changes before finishing */
requiresPushedChanges?: boolean;
/** Fetch context injections for this agent type */
fetchContext(params: FetchContextParams): Promise<ContextInjection[]>;
/** Build the task prompt for this agent type */
Expand All @@ -72,6 +77,32 @@ export interface AgentProfile {
// Helpers
// ============================================================================

/**
* Resolve the effective SCM hooks configuration from an agent definition.
*
* Merges legacy flat flags with the new `hooks.scm` format.
* The new `hooks.scm` format wins when both are present.
*
* Legacy flat flags (deprecated):
* - `backend.enableStopHooks` → `hooks.scm.enableStopHooks`
* - `backend.blockGitPush` → `hooks.scm.blockGitPush`
* - `backend.requiresPR` → `hooks.scm.requiresPR`
*
* New fields only available via `hooks.scm`:
* - `hooks.scm.requiresReview`
* - `hooks.scm.requiresPushedChanges`
*/
export function resolveScmHooks(backend: AgentDefinition['backend']): ScmHooks {
const legacy: ScmHooks = {
...(backend.enableStopHooks !== undefined && { enableStopHooks: backend.enableStopHooks }),
...(backend.blockGitPush !== undefined && { blockGitPush: backend.blockGitPush }),
...(backend.requiresPR !== undefined && { requiresPR: backend.requiresPR }),
};
const newHooks = backend.hooks?.scm ?? {};
// New hooks.scm wins over legacy flat flags
return { ...legacy, ...newHooks };
}

function resolveRegistry<T>(registry: Record<string, T>, key: string, label: string): T {
const value = registry[key];
if (!value) throw new Error(`${label} '${key}' not found in registry`);
Expand Down Expand Up @@ -146,17 +177,22 @@ function buildProfileFromDefinition(def: AgentDefinition, agentType: string): Ag
throw new Error(`Agent '${agentType}' has invalid taskPrompt: ${validationResult.error}`);
}

// Resolve SCM hooks (merges legacy flat flags with new hooks.scm format)
const scmHooks = resolveScmHooks(def.backend);

const profile: AgentProfile = {
filterTools: (allTools: ToolManifest[]) => {
// Filter tools by the gadget names derived from capabilities
const nameSet = new Set(gadgetNames);
return allTools.filter((t) => nameSet.has(t.name));
},
sdkTools,
enableStopHooks: def.backend.enableStopHooks,
enableStopHooks: scmHooks.enableStopHooks ?? false,
needsGitHubToken: def.backend.needsGitHubToken,
...(def.backend.blockGitPush !== undefined && { blockGitPush: def.backend.blockGitPush }),
...(def.backend.requiresPR && { requiresPR: true }),
...(scmHooks.blockGitPush !== undefined && { blockGitPush: scmHooks.blockGitPush }),
...(scmHooks.requiresPR && { requiresPR: true }),
...(scmHooks.requiresReview && { requiresReview: true }),
...(scmHooks.requiresPushedChanges && { requiresPushedChanges: true }),
fetchContext: async (params) => {
// Resolve context pipeline from the trigger (empty array if no trigger or trigger has no pipeline)
const contextPipeline = resolveContextPipeline(triggers, params.input.triggerType);
Expand Down
7 changes: 5 additions & 2 deletions src/agents/definitions/respond-to-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ prompts:
CI checks have failed. Analyze the failures and fix them.

backend:
enableStopHooks: true
needsGitHubToken: true
blockGitPush: false
hooks:
scm:
enableStopHooks: true
blockGitPush: false
requiresPushedChanges: true
preExecute: postInitialPRComment


Expand Down
1 change: 0 additions & 1 deletion src/agents/definitions/respond-to-planning-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ prompts:
Read the user's comment carefully and classify it: if they ask a question or request clarification, reply with a thorough answer via PostComment (do not modify the plan). If they request plan changes, make surgical, targeted updates. If the comment contains both a question and a change request, do both. Default to plan updates when intent is ambiguous.

backend:
enableStopHooks: false
needsGitHubToken: false


Expand Down
7 changes: 5 additions & 2 deletions src/agents/definitions/respond-to-pr-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ prompts:
Read the comment carefully and respond accordingly. If they ask for code changes, make the changes, commit, and push. If they ask a question, reply with a PR comment. Default to surgical, targeted changes unless they clearly ask for something broader.

backend:
enableStopHooks: true
needsGitHubToken: true
blockGitPush: false
hooks:
scm:
enableStopHooks: true
blockGitPush: false
requiresPushedChanges: true


hint: Complete the current task efficiently before moving to the next.
Expand Down
7 changes: 5 additions & 2 deletions src/agents/definitions/respond-to-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ prompts:
Carefully read each review comment and make the requested changes. Commit and push your changes when done. Use the ReplyToReviewComment tool to respond to individual review comments as you address them. Focus on surgical, targeted fixes unless the reviewer clearly asks for broader changes.

backend:
enableStopHooks: true
needsGitHubToken: true
blockGitPush: false
hooks:
scm:
enableStopHooks: true
blockGitPush: false
requiresPushedChanges: true


hint: Address the current review comment fully before moving to the next. Batch related file edits together.
Expand Down
5 changes: 4 additions & 1 deletion src/agents/definitions/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ prompts:
Examine the code changes carefully and submit your review using CreatePRReview.

backend:
enableStopHooks: false
needsGitHubToken: true
hooks:
scm:
enableStopHooks: false
requiresReview: true
preExecute: postInitialPRComment


Expand Down
48 changes: 47 additions & 1 deletion src/agents/definitions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,51 @@ const StrategiesSchema = z.object({
gadgetOptions: GadgetOptionsSchema,
});

/**
* SCM-specific hook configuration.
* Controls stop-hook behavior and push/PR requirements for SCM-integrated agents.
*/
export const ScmHooksSchema = z.object({
/** Whether to enable stop hooks that check for uncommitted/unpushed changes */
enableStopHooks: z.boolean().optional(),
/** Whether to block git push in hooks (set false for agents working on existing PR branches) */
blockGitPush: z.boolean().optional(),
/** Whether the agent must create a PR before finishing */
requiresPR: z.boolean().optional(),
/** Whether the agent must submit a review before finishing */
requiresReview: z.boolean().optional(),
/** Whether the agent must have pushed changes before finishing */
requiresPushedChanges: z.boolean().optional(),
});

/**
* Category-scoped hook configuration.
* Extensible for future categories (e.g., hooks.email, hooks.pm).
*/
export const HooksSchema = z.object({
/** SCM (source control) hook configuration */
scm: ScmHooksSchema.optional(),
});

const BackendSchema = z.object({
enableStopHooks: z.boolean(),
/**
* @deprecated Use hooks.scm.enableStopHooks instead.
* Kept for backward compatibility — new format wins when both are present.
*/
enableStopHooks: z.boolean().optional(),
needsGitHubToken: z.boolean(),
/**
* @deprecated Use hooks.scm.blockGitPush instead.
* Kept for backward compatibility — new format wins when both are present.
*/
blockGitPush: z.boolean().optional(),
/**
* @deprecated Use hooks.scm.requiresPR instead.
* Kept for backward compatibility — new format wins when both are present.
*/
requiresPR: z.boolean().optional(),
/** Category-scoped hook configuration */
hooks: HooksSchema.optional(),
preExecute: z.enum(['postInitialPRComment']).optional(),
postConfigure: z.enum(['sequentialGadgetExecution']).optional(),
});
Expand Down Expand Up @@ -302,3 +342,9 @@ export type IntegrationRequirements = z.infer<typeof IntegrationRequirementsSche

/** Known provider (trello, jira, github, etc.) */
export type KnownProvider = z.infer<typeof KnownProviderSchema>;

/** SCM hook configuration */
export type ScmHooks = z.infer<typeof ScmHooksSchema>;

/** Category-scoped hook configuration */
export type Hooks = z.infer<typeof HooksSchema>;
1 change: 0 additions & 1 deletion src/agents/definitions/splitting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ prompts:
Analyze and process the work item with ID: <%= it.cardId %>. The work item data has been pre-loaded.

backend:
enableStopHooks: false
needsGitHubToken: false


Expand Down
12 changes: 10 additions & 2 deletions src/agents/shared/builderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getCompactionConfig } from '../../config/compactionConfig.js';
import { getIterationTrailingMessage } from '../../config/hintConfig.js';
import { getRateLimitForModel } from '../../config/rateLimits.js';
import { getRetryConfig } from '../../config/retryConfig.js';
import { initSessionState } from '../../gadgets/sessionState.js';
import { type SessionHooks, initSessionState } from '../../gadgets/sessionState.js';
import type { LLMCallLogger } from '../../utils/llmLogging.js';
import { resolveSquintDbPath } from '../../utils/squintDb.js';
import type { IProgressMonitor } from '../contracts/index.js';
Expand Down Expand Up @@ -48,6 +48,8 @@ export interface CreateBuilderOptions {
projectId?: string;
/** Work item (card) ID for PR ↔ work item linking. Passed to session state. */
cardId?: string;
/** Resolved SCM hook flags for finish validation (requiresPR, requiresReview, etc.) */
hooks?: SessionHooks;
}

const MAX_GADGETS_PER_RESPONSE = 25;
Expand Down Expand Up @@ -76,7 +78,13 @@ export async function createConfiguredBuilder(options: CreateBuilderOptions): Pr

// Initialize session state for gadgets (e.g., Finish checks PR requirement for implementation)
if (!skipSessionState) {
initSessionState(agentType, options.baseBranch, options.projectId, options.cardId);
initSessionState(
agentType,
options.baseBranch,
options.projectId,
options.cardId,
options.hooks,
);
}

// Resolve config values before building
Expand Down
6 changes: 6 additions & 0 deletions src/backends/llmist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export class LlmistBackend implements AgentBackend {
baseBranch: input.project.baseBranch,
projectId: input.project.id,
cardId: agentInput.cardId,
// Pass resolved hook flags for finish validation (hook-driven instead of agent-type checks)
hooks: {
requiresPR: profile.requiresPR,
requiresReview: profile.requiresReview,
requiresPushedChanges: profile.requiresPushedChanges,
},
// Pass the progress monitor from the adapter so createObserverHooks can call
// onIteration/onToolCall/onText — enables progress updates to Trello/GitHub
progressMonitor: progressReporter as Parameters<
Expand Down
1 change: 1 addition & 0 deletions src/cli/session/finish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class Finish extends Command {
agentType: flags['agent-type'] ?? process.env.CASCADE_AGENT_TYPE ?? null,
prCreated: flags['pr-created'],
reviewSubmitted: flags['review-submitted'],
hooks: {},
});

if (!result.valid) {
Expand Down
1 change: 1 addition & 0 deletions src/gadgets/Finish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class Finish extends Gadget({
agentType: state.agentType,
prCreated: state.prCreated,
reviewSubmitted: state.reviewSubmitted,
hooks: state.hooks,
});

if (!result.valid) {
Expand Down
20 changes: 13 additions & 7 deletions src/gadgets/session/core/finish.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execSync } from 'node:child_process';
import { githubClient } from '../../../github/client.js';
import type { SessionHooks } from '../../sessionState.js';

export function hasUncommittedChanges(): boolean {
try {
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface SessionState {
agentType: string | null;
prCreated: boolean;
reviewSubmitted: boolean;
hooks: SessionHooks;
}

export interface FinishValidationError {
Expand All @@ -61,38 +63,42 @@ export interface FinishValidationSuccess {
export type FinishValidationResult = FinishValidationError | FinishValidationSuccess;

export async function validateFinish(state: SessionState): Promise<FinishValidationResult> {
if (state.agentType === 'implementation' && !state.prCreated) {
const hooks = state.hooks ?? {};

if (hooks.requiresPR && !state.prCreated) {
const prUrl = await findPRForCurrentBranch();
if (!prUrl) {
return {
valid: false,
error:
'Cannot finish implementation session without creating a PR. ' +
'Cannot finish session without creating a PR. ' +
'You must call CreatePR to submit your changes before calling Finish.',
};
}
}

if (state.agentType === 'review' && !state.reviewSubmitted) {
if (hooks.requiresReview && !state.reviewSubmitted) {
return {
valid: false,
error:
'Cannot finish review session without submitting a review. ' +
'Cannot finish session without submitting a review. ' +
'You must call CreatePRReview to submit your review before calling Finish.',
};
}

if (state.agentType === 'respond-to-review' || state.agentType === 'respond-to-ci') {
if (hooks.requiresPushedChanges) {
if (hasUncommittedChanges()) {
return {
valid: false,
error: `Cannot finish ${state.agentType} session with uncommitted changes. You must commit your changes (git add && git commit) before calling Finish.`,
error:
'Cannot finish session with uncommitted changes. You must commit your changes (git add && git commit) before calling Finish.',
};
}
if (hasUnpushedCommits()) {
return {
valid: false,
error: `Cannot finish ${state.agentType} session without pushing changes. You must push your commits (git push) before calling Finish.`,
error:
'Cannot finish session without pushing changes. You must push your commits (git push) before calling Finish.',
};
}
}
Expand Down
Loading