Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/infra/prompt-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ Requirements:
3. Summary must explain the user problem and the intended fix.
4. Changes, validation, and risks must be flat string lists.
5. Validation must mention the provided test commands and whether they passed or are still pending.
6. Follow the repository contribution rules and PR template when they are provided.

Output schema:
{
Expand All @@ -412,6 +413,9 @@ Patch Draft:

Validation Context:
{{validationContext}}

Repository Contribution Rules:
{{contributionRulesContext}}
`;

export const REPOSITORY_ANALYSIS_PROMPT = `You are OpenMeta, an autonomous open source contribution agent.
Expand Down
53 changes: 53 additions & 0 deletions src/orchestration/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3063,6 +3063,35 @@ export class AgentOrchestrator {
};
}

const unsatisfiedContributionRules = this.evaluateUnsatisfiedContributionRules(
input.workspace,
hasBlockingValidationFailures,
);
if (unsatisfiedContributionRules.length > 0) {
const message = `Skipping real draft PR creation because repository contribution requirements are not satisfied: ${unsatisfiedContributionRules.join('; ')}`;
logger.warn(message);
input.workspace.validationWarnings = [...input.workspace.validationWarnings, ...unsatisfiedContributionRules];
ui.callout({
label: 'OpenMeta Agent',
title: 'Repository constraints require review',
subtitle: 'OpenMeta kept this run in artifact-only mode to avoid opening a process-invalid PR.',
lines: unsatisfiedContributionRules,
tone: 'warning',
});
return {
changedFiles: input.changedFiles,
validationResults: input.validationResults,
};
}

if (input.headless && hasBlockingValidationFailures) {
logger.warn('Skipping real draft PR creation because validation failed in headless mode.');
return {
changedFiles: input.changedFiles,
validationResults: input.validationResults,
};
}

if (!input.headless) {
ui.callout({
label: 'OpenMeta Agent',
Expand Down Expand Up @@ -3102,6 +3131,7 @@ export class AgentOrchestrator {
prDraft: input.prDraft,
workspacePath: input.workspace.workspacePath,
changedFiles: input.changedFiles,
contributionRules: input.workspace.contributionRules,
});

ui.card({
Expand Down Expand Up @@ -3261,6 +3291,29 @@ export class AgentOrchestrator {
return results.some((result) => !result.passed && !this.isInfrastructureValidationFailure(result));
}

private evaluateUnsatisfiedContributionRules(
workspace: RepoWorkspaceContext,
hasBlockingValidationFailures: boolean,
): string[] {
const rules = workspace.contributionRules;
if (!rules) {
return [];
}

const missing: string[] = [];
if (rules.requiresPriorDiscussion) {
missing.push(
'Repository guidance indicates prior maintainer discussion or approval is required before opening a PR.',
);
}

if (rules.requiresPassingValidation && hasBlockingValidationFailures) {
missing.push('Repository guidance requires passing validation before opening a PR.');
}

return missing;
}

private resolveMachineExecutionOutcome(input: {
draftOnly: boolean;
localArtifactsOnly: boolean;
Expand Down
14 changes: 14 additions & 0 deletions src/services/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,20 @@ export class ContentService {
`- Tech Requirements: ${issue.analysis.techRequirements.join(', ') || 'n/a'}`,
`- Estimated Workload: ${issue.analysis.estimatedWorkload || 'n/a'}`,
'',
'## Repository Contribution Rules',
'',
`- Detected Rule Files: ${workspace.contributionRules?.detectedFiles.join(', ') || 'none'}`,
`- PR Template: ${workspace.contributionRules?.prTemplatePath || 'not detected'}`,
`- Required Checklist Items: ${workspace.contributionRules?.requiredChecklistItems.join(' | ') || 'none'}`,
`- PR Title Rules: ${workspace.contributionRules?.prTitleRules.join(' | ') || 'none'}`,
`- Commit Message Rules: ${workspace.contributionRules?.commitMessageRules.join(' | ') || 'none'}`,
`- Branch Naming Rules: ${workspace.contributionRules?.branchNamingRules.join(' | ') || 'none'}`,
`- Validation Rules: ${workspace.contributionRules?.requiredValidationRules.join(' | ') || 'none'}`,
`- Issue Linking Rules: ${workspace.contributionRules?.issueLinkingRules.join(' | ') || 'none'}`,
`- Release Note Rules: ${workspace.contributionRules?.releaseNoteRules.join(' | ') || 'none'}`,
`- Requires Prior Discussion: ${workspace.contributionRules?.requiresPriorDiscussion ? 'yes' : 'no'}`,
`- Requires Passing Validation: ${workspace.contributionRules?.requiresPassingValidation ? 'yes' : 'no'}`,
'',
'## Detected Test Commands',
'',
...(workspace.testCommands.length > 0
Expand Down
67 changes: 57 additions & 10 deletions src/services/contribution-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import type { PullRequestDraft } from '../contracts/index.js';
import { logger } from '../infra/index.js';
import type { RankedIssue } from '../types/index.js';
import type { RankedIssue, RepositoryContributionRules } from '../types/index.js';
import { contentService } from './content.js';
import { githubService } from './github.js';

Expand All @@ -24,6 +24,7 @@ export interface ContributionPrSubmissionInput {
prDraft: PullRequestDraft;
workspacePath: string;
changedFiles: string[];
contributionRules?: RepositoryContributionRules;
}

export interface ContributionPrSubmissionResult {
Expand All @@ -42,9 +43,9 @@ export class ContributionPrService {
async submitDraftPullRequest(input: ContributionPrSubmissionInput): Promise<ContributionPrSubmissionResult> {
const upstreamRepo = await this.getUpstreamRepositoryContext(input.issue);
const forkRepo = await this.ensureForkRepository(upstreamRepo);
const branchName = this.buildPublishBranchName(input.issue);
const draftPullRequest = this.buildDraftPullRequest(input.prDraft);
const commitMessage = this.buildContributionCommitMessage(input.issue);
const branchName = this.buildPublishBranchName(input.issue, input.contributionRules);
const draftPullRequest = this.buildDraftPullRequest(input.prDraft, input.issue, input.contributionRules);
const commitMessage = this.buildContributionCommitMessage(input.issue, input.contributionRules);

await this.createCommitOnFork({
forkRepo,
Expand All @@ -68,25 +69,71 @@ export class ContributionPrService {
};
}

buildDraftPullRequest(prDraft: PullRequestDraft): DraftPullRequest {
buildDraftPullRequest(
prDraft: PullRequestDraft,
issue?: RankedIssue,
contributionRules?: RepositoryContributionRules,
): DraftPullRequest {
const generatedBody = contentService.formatPullRequestDraftBody(prDraft);
const templateBody = contributionRules?.prTemplate?.trim();
const issueLinkLine = issue ? `Closes ${issue.repoFullName}#${issue.number}` : undefined;
const releaseNoteLine = contributionRules?.requiresReleaseNotes
? 'Release Notes: include a brief summary of the user-visible change.'
: undefined;

const body = templateBody
? [templateBody, '', '---', '', issueLinkLine, releaseNoteLine, generatedBody].filter(Boolean).join('\n')
: [issueLinkLine, releaseNoteLine, generatedBody].filter(Boolean).join('\n\n');

return {
title: prDraft.title,
body: contentService.formatPullRequestDraftBody(prDraft),
body,
};
}

buildPublishBranchName(issue: RankedIssue): string {
buildPublishBranchName(issue: RankedIssue, contributionRules?: RepositoryContributionRules): string {
const prefix = this.resolveBranchPrefix(contributionRules);
const slug = issue.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 32);

return `openmeta/agent-${issue.number}-${slug || 'issue'}-${Date.now()}`;
return `${prefix}${issue.number}-${slug || 'issue'}-${Date.now()}`;
}

buildContributionCommitMessage(issue: RankedIssue, contributionRules?: RepositoryContributionRules): string {
if (this.requiresConventionalCommit(contributionRules)) {
return `feat: address ${issue.repoFullName}#${issue.number} ${issue.title}`.slice(0, 120);
}

return `address ${issue.repoFullName}#${issue.number}: ${issue.title}`.slice(0, 120);
}

buildContributionCommitMessage(issue: RankedIssue): string {
return `feat: address ${issue.repoFullName}#${issue.number} ${issue.title}`.slice(0, 120);
private resolveBranchPrefix(contributionRules?: RepositoryContributionRules): string {
const rules = contributionRules?.branchNamingRules.join(' ').toLowerCase() || '';
if (rules.includes('feature/')) {
return 'feature/';
}

if (rules.includes('bugfix/')) {
return 'bugfix/';
}

if (rules.includes('fix/')) {
return 'fix/';
}

return 'openmeta/agent-';
}

private requiresConventionalCommit(contributionRules?: RepositoryContributionRules): boolean {
const rules = contributionRules?.commitMessageRules.join(' ').toLowerCase() || '';
if (!rules) {
return true;
}

return /conventional|type\(scope\):|feat:|fix:|chore:|docs:/.test(rules);
}

private async getUpstreamRepositoryContext(issue: RankedIssue): Promise<ContributionRepositoryContext> {
Expand Down
33 changes: 33 additions & 0 deletions src/services/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
RankedIssue,
RepoFileSnippet,
RepoMemory,
RepositoryContributionRules,
RepoWorkspaceContext,
TestResult,
UserProfile,
Expand Down Expand Up @@ -275,6 +276,7 @@ Repo Stars: ${i.repoStars}`,
issueContext: this.formatRankedIssue(issue),
patchDraft: JSON.stringify(patchDraft, null, 2),
validationContext: contextAssemblerService.buildValidationContext(workspace),
contributionRulesContext: this.formatContributionRules(workspace.contributionRules),
});

return this.generateStructuredOutput({
Expand Down Expand Up @@ -575,6 +577,37 @@ Repo Stars: ${i.repoStars}`,
].join('\n');
}

private formatContributionRules(rules?: RepositoryContributionRules): string {
if (!rules) {
return 'No repository-specific contribution rules were detected.';
}

const checklist =
rules.requiredChecklistItems.length > 0
? rules.requiredChecklistItems.map((item) => `- ${item}`).join('\n')
: '- none';
const template = rules.prTemplate
? `\nPR Template (${rules.prTemplatePath || 'detected'}):\n${rules.prTemplate}`
: '';

return [
`Detected Rule Files: ${rules.detectedFiles.join(', ') || 'none'}`,
`PR Title Rules: ${rules.prTitleRules.join(' | ') || 'none'}`,
`Commit Message Rules: ${rules.commitMessageRules.join(' | ') || 'none'}`,
`Branch Naming Rules: ${rules.branchNamingRules.join(' | ') || 'none'}`,
`Validation Rules: ${rules.requiredValidationRules.join(' | ') || 'none'}`,
`Issue Linking Rules: ${rules.issueLinkingRules.join(' | ') || 'none'}`,
`Release Note Rules: ${rules.releaseNoteRules.join(' | ') || 'none'}`,
`Checklist Items:\n${checklist}`,
`Prior Discussion Required: ${rules.requiresPriorDiscussion ? 'yes' : 'no'}`,
`Issue Linking Required: ${rules.requiresIssueLinking ? 'yes' : 'no'}`,
`Release Notes Required: ${rules.requiresReleaseNotes ? 'yes' : 'no'}`,
`Passing Validation Required: ${rules.requiresPassingValidation ? 'yes' : 'no'}`,
template,
]
.filter(Boolean)
.join('\n\n');
}
private formatEnvironment(environment: EnvironmentInfo): string {
const availableTools = environment.tools
.filter((tool) => tool.available)
Expand Down
Loading
Loading