Skip to content

Commit 7fe05ca

Browse files
committed
fix: add plan merging state transition and improve PR message generation
- Transition plan status to merging while merge train is active - Add merging→running and merging→paused to valid plan transitions - Improve orchestrator createPR with job status table and real test config - Improve mc_pr tool with PR template lookup and conventional commit titles - Always include Mission Control attribution in PR bodies
1 parent 4d4a576 commit 7fe05ca

4 files changed

Lines changed: 106 additions & 11 deletions

File tree

src/lib/orchestrator.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ export class Orchestrator {
507507
}
508508

509509
if (this.mergeTrain && this.mergeTrain.getQueue().length > 0) {
510+
plan.status = 'merging';
511+
510512
const nextJob = this.mergeTrain.getQueue()[0];
511513
this.showToast('Mission Control', `Merging job "${nextJob.name}"...`, 'info');
512514
this.notify(`⇄ Merging job "${nextJob.name}" into integration branch...`);
@@ -566,6 +568,10 @@ export class Orchestrator {
566568
}
567569
}
568570

571+
if (plan.status === 'merging' && (!this.mergeTrain || this.mergeTrain.getQueue().length === 0)) {
572+
plan.status = 'running';
573+
}
574+
569575
const latestPlan = await loadPlan();
570576
if (!latestPlan) {
571577
return;
@@ -901,8 +907,50 @@ If your work needs human review before it can proceed: mc_report(status: "needs_
901907
}
902908

903909
const defaultBranch = await getDefaultBranch();
904-
const title = plan.name.replace(/"/g, '\\"');
905-
const body = `Automated PR from Mission Control plan: ${plan.name}\n\nJobs:\n${plan.jobs.map((j) => `- ${j.name}`).join('\n')}`;
910+
const title = `feat: ${plan.name}`;
911+
const jobLines = plan.jobs.map((j) => {
912+
const status = j.status === 'merged' ? '✅' : j.status === 'failed' ? '❌' : '⏳';
913+
const mergedAt = j.mergedAt ? new Date(j.mergedAt).toISOString().slice(0, 19).replace('T', ' ') : '—';
914+
return `| ${j.name} | ${status} ${j.status} | ${mergedAt} |`;
915+
}).join('\n');
916+
917+
const mergeTrainConfig = this.getMergeTrainConfig();
918+
const testingLines: string[] = [];
919+
if (mergeTrainConfig.testCommand) {
920+
testingLines.push(`- [x] \`${mergeTrainConfig.testCommand}\` passed after each merge`);
921+
}
922+
if (mergeTrainConfig.setupCommands?.length) {
923+
testingLines.push(`- [x] Setup: \`${mergeTrainConfig.setupCommands.join(' && ')}\``);
924+
}
925+
if (testingLines.length === 0) {
926+
testingLines.push('- No test command configured');
927+
}
928+
929+
const body = [
930+
'## Summary',
931+
'',
932+
`Orchestrated plan **${plan.name}** with ${plan.jobs.length} job(s).`,
933+
'',
934+
'## Jobs',
935+
'',
936+
'| Job | Status | Merged At |',
937+
'|-----|--------|-----------|',
938+
jobLines,
939+
'',
940+
'## Testing',
941+
'',
942+
...testingLines,
943+
'',
944+
'## Notes',
945+
'',
946+
`- Integration branch: \`${plan.integrationBranch}\``,
947+
`- Base commit: \`${plan.baseCommit.slice(0, 8)}\``,
948+
`- Mode: ${plan.mode}`,
949+
'',
950+
'---',
951+
'',
952+
'🚀 *Automated PR from [Mission Control](https://github.com/nigel-dev/opencode-mission-control)*',
953+
].join('\n');
906954
const prResult = await this.runCommand([
907955
'gh',
908956
'pr',

src/lib/plan-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const VALID_PLAN_TRANSITIONS: Record<PlanStatus, PlanStatus[]> = {
6363
pending: ['running', 'failed', 'canceled'],
6464
running: ['paused', 'merging', 'failed', 'canceled'],
6565
paused: ['running', 'failed', 'canceled'],
66-
merging: ['creating_pr', 'failed', 'canceled'],
66+
merging: ['running', 'paused', 'creating_pr', 'failed', 'canceled'],
6767
creating_pr: ['completed', 'failed', 'canceled'],
6868
completed: [],
6969
failed: [],

src/tools/pr.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { tool, type ToolDefinition } from '@opencode-ai/plugin';
22
import { getDefaultBranch } from '../lib/git';
3-
import { getJobByName } from '../lib/job-state';
3+
import { getJobByName, type Job } from '../lib/job-state';
44

55
export async function executeGhCommand(args: string[]): Promise<string> {
66
const proc = Bun.spawn(['gh', 'pr', 'create', ...args], {
@@ -19,6 +19,45 @@ export async function executeGhCommand(args: string[]): Promise<string> {
1919
return stdout.trim();
2020
}
2121

22+
async function loadPrTemplate(cwd?: string): Promise<string | null> {
23+
const candidates = [
24+
'.github/pull_request_template.md',
25+
'.github/PULL_REQUEST_TEMPLATE.md',
26+
'pull_request_template.md',
27+
'PULL_REQUEST_TEMPLATE.md',
28+
'.github/PULL_REQUEST_TEMPLATE/pull_request_template.md',
29+
];
30+
31+
for (const candidate of candidates) {
32+
try {
33+
const fullPath = cwd ? `${cwd}/${candidate}` : candidate;
34+
const file = Bun.file(fullPath);
35+
if (await file.exists()) {
36+
return await file.text();
37+
}
38+
} catch {
39+
continue;
40+
}
41+
}
42+
return null;
43+
}
44+
45+
function buildDefaultBody(job: Job): string {
46+
return [
47+
'## Summary',
48+
'',
49+
job.prompt,
50+
'',
51+
'## Changes',
52+
'',
53+
`Branch: \`${job.branch}\``,
54+
'',
55+
'---',
56+
'',
57+
'🚀 *Created by [Mission Control](https://github.com/nigel-dev/opencode-mission-control)*',
58+
].join('\n');
59+
}
60+
2261
export const mc_pr: ToolDefinition = tool({
2362
description: 'Create a pull request from a job\'s branch',
2463
args: {
@@ -28,11 +67,11 @@ export const mc_pr: ToolDefinition = tool({
2867
title: tool.schema
2968
.string()
3069
.optional()
31-
.describe('PR title (defaults to job prompt)'),
70+
.describe('PR title (defaults to conventional commit format using job name)'),
3271
body: tool.schema
3372
.string()
3473
.optional()
35-
.describe('PR body'),
74+
.describe('PR body (defaults to PR template or generated summary)'),
3675
draft: tool.schema
3776
.boolean()
3877
.optional()
@@ -57,8 +96,8 @@ export const mc_pr: ToolDefinition = tool({
5796
throw new Error(`Failed to push branch "${job.branch}": ${pushStderr}`);
5897
}
5998

60-
// 3. Determine PR title (default to job prompt)
61-
const prTitle = args.title || job.prompt;
99+
// 3. Determine PR title (conventional commit format)
100+
const prTitle = args.title || `feat: ${job.name}`;
62101

63102
// 4. Build gh pr create arguments
64103
const defaultBranch = await getDefaultBranch(job.worktreePath);
@@ -68,9 +107,17 @@ export const mc_pr: ToolDefinition = tool({
68107
'--base', defaultBranch,
69108
];
70109

71-
// 5. Add optional body
110+
// 5. Build PR body — use explicit body, or fall back to default
111+
const mcAttribution = '\n\n---\n\n🚀 *Created by [Mission Control](https://github.com/nigel-dev/opencode-mission-control)*';
72112
if (args.body) {
73-
ghArgs.push('--body', args.body);
113+
ghArgs.push('--body', args.body + mcAttribution);
114+
} else {
115+
const template = await loadPrTemplate(job.worktreePath);
116+
if (template) {
117+
ghArgs.push('--body', template + mcAttribution);
118+
} else {
119+
ghArgs.push('--body', buildDefaultBody(job));
120+
}
74121
}
75122

76123
// 6. Add draft flag if specified

tests/tools/pr.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('mc_pr', () => {
8585
});
8686

8787
describe('argument handling', () => {
88-
it('should use job prompt as default title', async () => {
88+
it('should use conventional commit format as default title', async () => {
8989
const job: Job = {
9090
id: 'job-1',
9191
name: 'feature-auth',

0 commit comments

Comments
 (0)