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
2 changes: 2 additions & 0 deletions src/api/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const projectsRouter = router({
workItemBudgetUsd: z.string().nullish(),
agentEngine: z.string().nullish(),
engineSettings: EngineSettingsSchema.nullish(),
runLinksEnabled: z.boolean().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
Expand All @@ -113,6 +114,7 @@ export const projectsRouter = router({
workItemBudgetUsd: z.string().nullish(),
agentEngine: z.string().nullish(),
engineSettings: EngineSettingsSchema.nullish(),
runLinksEnabled: z.boolean().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
Expand Down
53 changes: 51 additions & 2 deletions src/backends/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { withGitHubToken } from '../github/client.js';
import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js';
import { logger } from '../utils/logging.js';
import { getDashboardUrl } from '../utils/runLink.js';
import { readCompletionEvidence } from './completion.js';
import { createNativeToolRuntimeArtifacts } from './nativeToolRuntime.js';
import { postProcessResult } from './postProcess.js';
Expand Down Expand Up @@ -325,13 +326,27 @@ async function hydratePrSidecar(sidecarPath: string): Promise<{
* Build progress-monitor config from pipeline inputs.
*/
function buildProgressMonitorConfig(
input: AgentInput & { config: CascadeConfig },
input: AgentInput & { config: CascadeConfig; project: ProjectConfig },
agentType: string,
logWriter: LogWriter,
repoDir: string | null,
isGitHubAck: boolean,
engineId: string,
model: string,
) {
const { workItemId } = input;

// Build run link config when the project has run links enabled and dashboard URL is set
const runLink =
input.project.runLinksEnabled && getDashboardUrl()
? {
engineLabel: engineId,
model,
projectId: input.project.id,
workItemId: workItemId ?? undefined,
}
: undefined;

return {
logWriter,
agentType,
Expand All @@ -342,6 +357,7 @@ function buildProgressMonitorConfig(
repoDir: repoDir ?? undefined,
trello: workItemId ? { workItemId } : undefined,
preSeededCommentId: isGitHubAck ? undefined : (input.ackCommentId as string | undefined),
runLink,
...(input.prNumber && input.repoFullName
? {
github: {
Expand Down Expand Up @@ -442,6 +458,7 @@ export async function executeWithEngine(
outputSummary: outcome.outputSummary,
}),

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: webhook pipeline with sequential guard checks
execute: async (ctx: PipelineContext) => {
const { repoDir, fileLogger, logWriter, setRunId } = ctx;
const log = createAgentLogger(fileLogger);
Expand Down Expand Up @@ -480,9 +497,41 @@ export async function executeWithEngine(
}

const monitor = createProgressMonitor(
buildProgressMonitorConfig(input, agentType, logWriter, repoDir, isGitHubAck),
buildProgressMonitorConfig(
input,
agentType,
logWriter,
repoDir,
isGitHubAck,
engine.definition.id,
partialInput.model ?? '',
),
);

// Inject the runId into the progress monitor so links point to the specific run
if (runId && monitor) {
monitor.setRunId(runId);
}

// Inject run link env vars into project secrets for subprocess agents (claude-code/codex)
if (input.project.runLinksEnabled) {
partialInput.projectSecrets ??= {};
const dashboardUrl = getDashboardUrl();
if (dashboardUrl) {
partialInput.projectSecrets.CASCADE_RUN_LINKS_ENABLED = 'true';
partialInput.projectSecrets.CASCADE_DASHBOARD_URL = dashboardUrl;
partialInput.projectSecrets.CASCADE_ENGINE_LABEL = engine.definition.id;
partialInput.projectSecrets.CASCADE_MODEL = partialInput.model ?? '';
partialInput.projectSecrets.CASCADE_PROJECT_ID = input.project.id;
if (workItemId) {
partialInput.projectSecrets.CASCADE_WORK_ITEM_ID = workItemId;
}
if (runId) {
partialInput.projectSecrets.CASCADE_RUN_ID = runId;
}
}
}

const executionPlan: AgentExecutionPlan = {
...partialInput,
progressReporter: monitor ?? {
Expand Down
9 changes: 9 additions & 0 deletions src/backends/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ export interface ProgressMonitorOptions {
github?: { owner: string; repo: string };
/** Pre-seeded comment ID from router ack — skip initial comment posting */
preSeededCommentId?: string;
/** Run link config — when set, appends a dashboard link to progress comments */
runLink?: {
runId?: string;
engineLabel: string;
model: string;
projectId: string;
workItemId?: string;
};
}

/**
Expand All @@ -41,6 +49,7 @@ export function createProgressMonitor(options: ProgressMonitorOptions): Progress
trello: options.trello,
github: options.github,
preSeededCommentId: options.preSeededCommentId,
runLink: options.runLink,
};

return new ProgressMonitor(config);
Expand Down
50 changes: 50 additions & 0 deletions src/backends/progressMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { ModelSpec } from 'llmist';
import { syncCompletedTodosToChecklist } from '../agents/utils/checklistSync.js';
import { formatStatusMessage } from '../config/statusUpdateConfig.js';
import { captureException } from '../sentry.js';
import { buildRunLink, buildWorkItemRunsLink, getDashboardUrl } from '../utils/runLink.js';
import { callProgressModel } from './progressModel.js';
import { clearProgressCommentId, writeProgressCommentId } from './progressState.js';
import { ProgressAccumulator } from './progressState/accumulator.js';
Expand Down Expand Up @@ -49,6 +50,14 @@ export interface ProgressMonitorConfig {
* Defaults to DEFAULT_SCHEDULE_MINUTES = [1, 3, 5].
*/
scheduleMinutes?: number[];
/** Run link config — when set, appends a dashboard link to progress comments */
runLink?: {
runId?: string;
engineLabel: string;
model: string;
projectId: string;
workItemId?: string;
};
}

const PROGRESS_MODEL_TIMEOUT_MS = 20_000;
Expand Down Expand Up @@ -92,6 +101,13 @@ export class ProgressMonitor implements ProgressReporter {
return this.pmPoster?.getCommentId() ?? null;
}

/** Update the run ID (available after the run record is created). */
setRunId(runId: string): void {
if (this.config.runLink) {
this.config.runLink.runId = runId;
}
}

// ── ProgressReporter interface (accumulate only, no posting) ──

async onIteration(iteration: number, maxIterations: number): Promise<void> {
Expand Down Expand Up @@ -152,6 +168,36 @@ export class ProgressMonitor implements ProgressReporter {

// ── Internal ──

private buildRunLinkFooter(): string {
const { runLink } = this.config;
if (!runLink) return '';

const dashboardUrl = getDashboardUrl();
if (!dashboardUrl) return '';

if (runLink.runId) {
return buildRunLink({
dashboardUrl,
runId: runLink.runId,
engineLabel: runLink.engineLabel,
model: runLink.model,
});
}

if (runLink.workItemId) {
return buildWorkItemRunsLink({
dashboardUrl,
projectId: runLink.projectId,
workItemId: runLink.workItemId,
engineLabel: runLink.engineLabel,
model: runLink.model,
});
}

return '';
}

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: progress reporting with multiple posting targets
private async tick(): Promise<void> {
// Wait for initial comment to complete before proceeding so the first
// tick updates the same comment instead of creating a duplicate
Expand Down Expand Up @@ -192,6 +238,10 @@ export class ProgressMonitor implements ProgressReporter {
summary = formatStatusMessage(this.config.agentType);
}

// Append run link footer if configured
const runLinkFooter = this.buildRunLinkFooter();
if (runLinkFooter) summary += runLinkFooter;

// Post to PM provider (Trello/JIRA)
if (this.pmPoster) {
try {
Expand Down
7 changes: 7 additions & 0 deletions src/cli/dashboard/projects/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export default class ProjectsUpdate extends DashboardCommand {
model: Flags.string({ description: 'Default model' }),
'work-item-budget': Flags.string({ description: 'Per-work-item budget in USD' }),
'agent-engine': Flags.string({ description: 'Agent engine' }),
'run-links-enabled': Flags.boolean({
description: 'Enable run links in agent comments (requires CASCADE_DASHBOARD_URL env var)',
allowNo: true,
}),
};

async run(): Promise<void> {
Expand All @@ -32,6 +36,9 @@ export default class ProjectsUpdate extends DashboardCommand {
model: flags.model,
workItemBudgetUsd: flags['work-item-budget'],
agentEngine: flags['agent-engine'],
...(flags['run-links-enabled'] !== undefined
? { runLinksEnabled: flags['run-links-enabled'] }
: {}),
});

if (flags.json) {
Expand Down
1 change: 1 addition & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const ProjectConfigSchema = z.object({
agentEngine: AgentEngineConfigSchema.optional(),
engineSettings: EngineSettingsSchema.optional(),
squintDbUrl: z.string().url().optional(),
runLinksEnabled: z.boolean().default(false),
});

export const CascadeConfigSchema = z.object({
Expand Down
4 changes: 4 additions & 0 deletions src/db/migrations/0037_add_run_links.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Migration 0037: Add run_links_enabled to projects
-- Enables per-project opt-in for including dashboard run links in agent comments.

ALTER TABLE projects ADD COLUMN IF NOT EXISTS run_links_enabled BOOLEAN NOT NULL DEFAULT false;
7 changes: 7 additions & 0 deletions src/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@
"when": 1771000000000,
"tag": "0036_project_only_agent_configs",
"breakpoints": false
},
{
"idx": 37,
"version": "7",
"when": 1772000000000,
"tag": "0037_add_run_links",
"breakpoints": false
}
]
}
3 changes: 3 additions & 0 deletions src/db/repositories/configMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export interface ProjectConfigRaw {
workItemBudgetUsd?: number;
squintDbUrl?: string;
engineSettings?: EngineSettings;
runLinksEnabled?: boolean;
trello?: {
boardId: string;
lists: Record<string, string>;
Expand Down Expand Up @@ -126,6 +127,7 @@ type ProjectRow = {
squintDbUrl: string | null;
agentEngine: string | null;
agentEngineSettings: EngineSettings | null;
runLinksEnabled: boolean;
};

export function buildAgentMaps(configs: AgentConfigRow[]): {
Expand Down Expand Up @@ -238,6 +240,7 @@ export function mapProjectRow({
workItemBudgetUsd: row.workItemBudgetUsd ? Number(row.workItemBudgetUsd) : undefined,
engineSettings: row.agentEngineSettings ?? undefined,
squintDbUrl: row.squintDbUrl ?? undefined,
runLinksEnabled: row.runLinksEnabled ?? false,
};

if (trelloConfig) {
Expand Down
3 changes: 3 additions & 0 deletions src/db/repositories/projectsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export async function createProject(
workItemBudgetUsd?: string | null;
agentEngine?: string | null;
engineSettings?: EngineSettings | null;
runLinksEnabled?: boolean;
},
) {
const db = getDb();
Expand All @@ -54,6 +55,7 @@ export async function createProject(
model: rest.model,
workItemBudgetUsd: rest.workItemBudgetUsd,
agentEngine: rest.agentEngine,
runLinksEnabled: rest.runLinksEnabled ?? false,
...(engineSettings !== undefined
? { agentEngineSettings: normalizeEngineSettings(engineSettings) }
: {}),
Expand All @@ -74,6 +76,7 @@ export async function updateProject(
workItemBudgetUsd?: string | null;
agentEngine?: string | null;
engineSettings?: EngineSettings | null;
runLinksEnabled?: boolean;
},
) {
const db = getDb();
Expand Down
3 changes: 2 additions & 1 deletion src/db/schema/projects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { jsonb, numeric, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { boolean, jsonb, numeric, pgTable, text, timestamp } from 'drizzle-orm/pg-core';

import type { EngineSettings } from '../../config/engineSettings.js';
import { organizations } from './organizations.js';
Expand All @@ -20,6 +20,7 @@ export const projects = pgTable(
agentEngine: text('agent_engine'),
agentEngineSettings: jsonb('agent_engine_settings').$type<EngineSettings>(),
squintDbUrl: text('squint_db_url'),
runLinksEnabled: boolean('run_links_enabled').default(false).notNull(),

createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at')
Expand Down
6 changes: 5 additions & 1 deletion src/gadgets/github/core/createPR.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { githubClient } from '../../../github/client.js';
import { runCommand } from '../../../utils/repo.js';
import { buildRunLinkFooterFromEnv } from '../../../utils/runLink.js';

export interface CreatePRParams {
title: string;
Expand Down Expand Up @@ -100,10 +101,13 @@ export async function createPR(params: CreatePRParams): Promise<CreatePRResult>
);
}

const runLinkFooter = buildRunLinkFooterFromEnv();
const prBody = runLinkFooter ? params.body + runLinkFooter : params.body;

try {
const pr = await githubClient.createPR(owner, repo, {
title: params.title,
body: params.body,
body: prBody,
head: params.head,
base: params.base,
draft: params.draft,
Expand Down
6 changes: 5 additions & 1 deletion src/gadgets/github/core/createPRReview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { githubClient } from '../../../github/client.js';
import { buildRunLinkFooterFromEnv } from '../../../utils/runLink.js';

export interface CreatePRReviewParams {
owner: string;
Expand All @@ -15,12 +16,15 @@ export interface CreatePRReviewResult {
}

export async function createPRReview(params: CreatePRReviewParams): Promise<CreatePRReviewResult> {
const runLinkFooter = buildRunLinkFooterFromEnv();
const body = runLinkFooter ? params.body + runLinkFooter : params.body;

const review = await githubClient.createPRReview(
params.owner,
params.repo,
params.prNumber,
params.event,
params.body,
body,
params.comments,
);
return { reviewUrl: review.htmlUrl, event: params.event };
Expand Down
5 changes: 4 additions & 1 deletion src/gadgets/github/core/postPRComment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { githubClient } from '../../../github/client.js';
import { buildRunLinkFooterFromEnv } from '../../../utils/runLink.js';

export async function postPRComment(
owner: string,
Expand All @@ -7,7 +8,9 @@ export async function postPRComment(
body: string,
): Promise<string> {
try {
const result = await githubClient.createPRComment(owner, repo, prNumber, body);
const runLinkFooter = buildRunLinkFooterFromEnv();
const fullBody = runLinkFooter ? body + runLinkFooter : body;
const result = await githubClient.createPRComment(owner, repo, prNumber, fullBody);
return `Comment posted (id: ${result.id}): ${result.htmlUrl}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
Loading
Loading