feat: Auto-branch initiative system with per-project default branches
Planning tasks (research, discuss, plan, detail, refine) now run on the project's defaultBranch instead of hardcoded 'main'. Execution tasks (execute, verify, merge, review) auto-generate an initiative branch (cw/<slug>) on first dispatch. Branch configuration removed from initiative creation — it's now fully automatic. - Add PLANNING_CATEGORIES/EXECUTION_CATEGORIES to branch-naming - Dispatch manager splits logic by task category - ProcessManager uses per-project defaultBranch fallback - Phase dispatch uses project.defaultBranch for ensureBranch base - Remove mergeTarget from createInitiative input - Rename updateInitiativeMergeConfig → updateInitiativeConfig - Add defaultBranch field to registerProject + UI - Rename mergeTarget → branch across all frontend components
This commit is contained in:
@@ -33,7 +33,6 @@ export function CreateInitiativeDialog({
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [projectIds, setProjectIds] = useState<string[]>([]);
|
const [projectIds, setProjectIds] = useState<string[]>([]);
|
||||||
const [executionMode, setExecutionMode] = useState<"yolo" | "review_per_phase">("review_per_phase");
|
const [executionMode, setExecutionMode] = useState<"yolo" | "review_per_phase">("review_per_phase");
|
||||||
const [mergeTarget, setMergeTarget] = useState("");
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
@@ -48,7 +47,7 @@ export function CreateInitiativeDialog({
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
mergeRequiresApproval: true,
|
mergeRequiresApproval: true,
|
||||||
mergeTarget: 'main',
|
branch: null,
|
||||||
projects: [],
|
projects: [],
|
||||||
};
|
};
|
||||||
utils.listInitiatives.setData(undefined, (old = []) => [tempInitiative, ...old]);
|
utils.listInitiatives.setData(undefined, (old = []) => [tempInitiative, ...old]);
|
||||||
@@ -73,7 +72,6 @@ export function CreateInitiativeDialog({
|
|||||||
setName("");
|
setName("");
|
||||||
setProjectIds([]);
|
setProjectIds([]);
|
||||||
setExecutionMode("review_per_phase");
|
setExecutionMode("review_per_phase");
|
||||||
setMergeTarget("");
|
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
@@ -85,7 +83,6 @@ export function CreateInitiativeDialog({
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
executionMode,
|
executionMode,
|
||||||
mergeTarget: mergeTarget.trim() || null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,20 +120,6 @@ export function CreateInitiativeDialog({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="merge-target">
|
|
||||||
Merge Target Branch{" "}
|
|
||||||
<span className="text-muted-foreground font-normal">
|
|
||||||
(optional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="merge-target"
|
|
||||||
placeholder="e.g. feat/auth"
|
|
||||||
value={mergeTarget}
|
|
||||||
onChange={(e) => setMergeTarget(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>
|
||||||
Projects{" "}
|
Projects{" "}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface InitiativeHeaderProps {
|
|||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
executionMode?: string;
|
executionMode?: string;
|
||||||
mergeTarget?: string | null;
|
branch?: string | null;
|
||||||
};
|
};
|
||||||
projects?: Array<{ id: string; name: string; url: string }>;
|
projects?: Array<{ id: string; name: string; url: string }>;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@@ -74,10 +74,10 @@ export function InitiativeHeader({
|
|||||||
{initiative.executionMode === "yolo" ? "YOLO" : "REVIEW"}
|
{initiative.executionMode === "yolo" ? "YOLO" : "REVIEW"}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{initiative.mergeTarget && (
|
{initiative.branch && (
|
||||||
<Badge variant="outline" className="gap-1 text-[10px] font-mono">
|
<Badge variant="outline" className="gap-1 text-[10px] font-mono">
|
||||||
<GitBranch className="h-3 w-3" />
|
<GitBranch className="h-3 w-3" />
|
||||||
{initiative.mergeTarget}
|
{initiative.branch}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!editing && projects && projects.length > 0 && (
|
{!editing && projects && projects.length > 0 && (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function RegisterProjectDialog({
|
|||||||
}: RegisterProjectDialogProps) {
|
}: RegisterProjectDialogProps) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
|
const [defaultBranch, setDefaultBranch] = useState("main");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const registerMutation = trpc.registerProject.useMutation({
|
const registerMutation = trpc.registerProject.useMutation({
|
||||||
@@ -40,6 +41,7 @@ export function RegisterProjectDialog({
|
|||||||
if (open) {
|
if (open) {
|
||||||
setName("");
|
setName("");
|
||||||
setUrl("");
|
setUrl("");
|
||||||
|
setDefaultBranch("main");
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
@@ -50,6 +52,7 @@ export function RegisterProjectDialog({
|
|||||||
registerMutation.mutate({
|
registerMutation.mutate({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
url: url.trim(),
|
url: url.trim(),
|
||||||
|
defaultBranch: defaultBranch.trim() || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,15 @@ export function RegisterProjectDialog({
|
|||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="default-branch">Default Branch</Label>
|
||||||
|
<Input
|
||||||
|
id="default-branch"
|
||||||
|
placeholder="main"
|
||||||
|
value={defaultBranch}
|
||||||
|
onChange={(e) => setDefaultBranch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('writeInputFiles', () => {
|
|||||||
name: 'Test Initiative',
|
name: 'Test Initiative',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
mergeRequiresApproval: true,
|
mergeRequiresApproval: true,
|
||||||
mergeTarget: 'main',
|
branch: 'cw/test-initiative',
|
||||||
executionMode: 'review_per_phase',
|
executionMode: 'review_per_phase',
|
||||||
createdAt: new Date('2026-01-01'),
|
createdAt: new Date('2026-01-01'),
|
||||||
updatedAt: new Date('2026-01-02'),
|
updatedAt: new Date('2026-01-02'),
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
|||||||
name: ini.name,
|
name: ini.name,
|
||||||
status: ini.status,
|
status: ini.status,
|
||||||
mergeRequiresApproval: ini.mergeRequiresApproval,
|
mergeRequiresApproval: ini.mergeRequiresApproval,
|
||||||
mergeTarget: ini.mergeTarget,
|
branch: ini.branch,
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
let agentCwd: string;
|
let agentCwd: string;
|
||||||
if (initiativeId) {
|
if (initiativeId) {
|
||||||
log.debug({ alias, initiativeId, baseBranch, branchName }, 'creating initiative-based worktrees');
|
log.debug({ alias, initiativeId, baseBranch, branchName }, 'creating initiative-based worktrees');
|
||||||
agentCwd = await this.processManager.createProjectWorktrees(alias, initiativeId, baseBranch ?? 'main', branchName);
|
agentCwd = await this.processManager.createProjectWorktrees(alias, initiativeId, baseBranch, branchName);
|
||||||
|
|
||||||
// Log projects linked to the initiative
|
// Log projects linked to the initiative
|
||||||
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
|
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class ProcessManager {
|
|||||||
async createProjectWorktrees(
|
async createProjectWorktrees(
|
||||||
alias: string,
|
alias: string,
|
||||||
initiativeId: string,
|
initiativeId: string,
|
||||||
baseBranch: string = 'main',
|
baseBranch?: string,
|
||||||
branchName?: string,
|
branchName?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
|
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
|
||||||
@@ -78,7 +78,8 @@ export class ProcessManager {
|
|||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
||||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath, undefined, agentWorkdir);
|
const worktreeManager = new SimpleGitWorktreeManager(clonePath, undefined, agentWorkdir);
|
||||||
const worktree = await worktreeManager.create(project.name, branchName ?? `agent/${alias}`, baseBranch);
|
const effectiveBaseBranch = baseBranch ?? project.defaultBranch;
|
||||||
|
const worktree = await worktreeManager.create(project.name, branchName ?? `agent/${alias}`, effectiveBaseBranch);
|
||||||
const worktreePath = worktree.path;
|
const worktreePath = worktree.path;
|
||||||
const pathExists = existsSync(worktreePath);
|
const pathExists = existsSync(worktreePath);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type { InitiativeRepository } from '../db/repositories/initiative-reposit
|
|||||||
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
|
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
|
||||||
import type { Task } from '../db/schema.js';
|
import type { Task } from '../db/schema.js';
|
||||||
import type { DispatchManager, QueuedTask, DispatchResult } from './types.js';
|
import type { DispatchManager, QueuedTask, DispatchResult } from './types.js';
|
||||||
import { initiativeBranchName, phaseBranchName, taskBranchName } from '../git/branch-naming.js';
|
import { phaseBranchName, taskBranchName, isPlanningCategory, generateInitiativeBranch } from '../git/branch-naming.js';
|
||||||
import { createModuleLogger } from '../logger/index.js';
|
import { createModuleLogger } from '../logger/index.js';
|
||||||
|
|
||||||
const log = createModuleLogger('dispatch');
|
const log = createModuleLogger('dispatch');
|
||||||
@@ -327,13 +327,23 @@ export class DefaultDispatchManager implements DispatchManager {
|
|||||||
let baseBranch: string | undefined;
|
let baseBranch: string | undefined;
|
||||||
let branchName: string | undefined;
|
let branchName: string | undefined;
|
||||||
|
|
||||||
if (task.phaseId && task.initiativeId && this.initiativeRepository && this.phaseRepository) {
|
if (task.initiativeId && this.initiativeRepository) {
|
||||||
try {
|
try {
|
||||||
|
if (isPlanningCategory(task.category)) {
|
||||||
|
// Planning tasks run on project default branches — no initiative branch needed.
|
||||||
|
// baseBranch and branchName remain undefined; ProcessManager uses per-project defaults.
|
||||||
|
} else if (task.phaseId && this.phaseRepository) {
|
||||||
|
// Execution task — ensure initiative has a branch
|
||||||
const initiative = await this.initiativeRepository.findById(task.initiativeId);
|
const initiative = await this.initiativeRepository.findById(task.initiativeId);
|
||||||
const phase = await this.phaseRepository.findById(task.phaseId);
|
if (initiative) {
|
||||||
if (initiative?.mergeTarget && phase) {
|
let initBranch = initiative.branch;
|
||||||
const initBranch = initiativeBranchName(initiative.mergeTarget);
|
if (!initBranch) {
|
||||||
|
initBranch = generateInitiativeBranch(initiative.name);
|
||||||
|
await this.initiativeRepository.update(initiative.id, { branch: initBranch });
|
||||||
|
}
|
||||||
|
|
||||||
|
const phase = await this.phaseRepository.findById(task.phaseId);
|
||||||
|
if (phase) {
|
||||||
if (task.category === 'merge') {
|
if (task.category === 'merge') {
|
||||||
// Merge tasks work directly on the phase branch
|
// Merge tasks work directly on the phase branch
|
||||||
baseBranch = initBranch;
|
baseBranch = initBranch;
|
||||||
@@ -343,6 +353,8 @@ export class DefaultDispatchManager implements DispatchManager {
|
|||||||
branchName = taskBranchName(initBranch, task.id);
|
branchName = taskBranchName(initBranch, task.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal: fall back to default branching
|
// Non-fatal: fall back to default branching
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import type { InitiativeRepository } from '../db/repositories/initiative-reposit
|
|||||||
import type { ProjectRepository } from '../db/repositories/project-repository.js';
|
import type { ProjectRepository } from '../db/repositories/project-repository.js';
|
||||||
import type { BranchManager } from '../git/branch-manager.js';
|
import type { BranchManager } from '../git/branch-manager.js';
|
||||||
import type { PhaseDispatchManager, DispatchManager, QueuedPhase, PhaseDispatchResult } from './types.js';
|
import type { PhaseDispatchManager, DispatchManager, QueuedPhase, PhaseDispatchResult } from './types.js';
|
||||||
import { initiativeBranchName, phaseBranchName } from '../git/branch-naming.js';
|
import { phaseBranchName } from '../git/branch-naming.js';
|
||||||
import { ensureProjectClone } from '../git/project-clones.js';
|
import { ensureProjectClone } from '../git/project-clones.js';
|
||||||
import { createModuleLogger } from '../logger/index.js';
|
import { createModuleLogger } from '../logger/index.js';
|
||||||
|
|
||||||
@@ -172,13 +172,13 @@ export class DefaultPhaseDispatchManager implements PhaseDispatchManager {
|
|||||||
if (this.initiativeRepository && this.projectRepository && this.branchManager && this.workspaceRoot) {
|
if (this.initiativeRepository && this.projectRepository && this.branchManager && this.workspaceRoot) {
|
||||||
try {
|
try {
|
||||||
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
||||||
if (initiative?.mergeTarget) {
|
if (initiative?.branch) {
|
||||||
const initBranch = initiativeBranchName(initiative.mergeTarget);
|
const initBranch = initiative.branch;
|
||||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||||
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
||||||
await this.branchManager.ensureBranch(clonePath, initBranch, 'main');
|
await this.branchManager.ensureBranch(clonePath, initBranch, project.defaultBranch);
|
||||||
await this.branchManager.ensureBranch(clonePath, phBranch, initBranch);
|
await this.branchManager.ensureBranch(clonePath, phBranch, initBranch);
|
||||||
}
|
}
|
||||||
log.info({ phaseId: nextPhase.phaseId, phBranch, initBranch }, 'phase branch created');
|
log.info({ phaseId: nextPhase.phaseId, phBranch, initBranch }, 'phase branch created');
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type { InitiativeRepository } from '../db/repositories/initiative-reposit
|
|||||||
import type { ProjectRepository } from '../db/repositories/project-repository.js';
|
import type { ProjectRepository } from '../db/repositories/project-repository.js';
|
||||||
import type { PhaseDispatchManager } from '../dispatch/types.js';
|
import type { PhaseDispatchManager } from '../dispatch/types.js';
|
||||||
import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js';
|
import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js';
|
||||||
import { initiativeBranchName, phaseBranchName, taskBranchName } from '../git/branch-naming.js';
|
import { phaseBranchName, taskBranchName } from '../git/branch-naming.js';
|
||||||
import { ensureProjectClone } from '../git/project-clones.js';
|
import { ensureProjectClone } from '../git/project-clones.js';
|
||||||
import { createModuleLogger } from '../logger/index.js';
|
import { createModuleLogger } from '../logger/index.js';
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export class ExecutionOrchestrator {
|
|||||||
if (!task?.phaseId || !task.initiativeId) return;
|
if (!task?.phaseId || !task.initiativeId) return;
|
||||||
|
|
||||||
const initiative = await this.initiativeRepository.findById(task.initiativeId);
|
const initiative = await this.initiativeRepository.findById(task.initiativeId);
|
||||||
if (!initiative?.mergeTarget) return;
|
if (!initiative?.branch) return;
|
||||||
|
|
||||||
const phase = await this.phaseRepository.findById(task.phaseId);
|
const phase = await this.phaseRepository.findById(task.phaseId);
|
||||||
if (!phase) return;
|
if (!phase) return;
|
||||||
@@ -72,7 +72,7 @@ export class ExecutionOrchestrator {
|
|||||||
// Skip merge tasks — they already work on the phase branch directly
|
// Skip merge tasks — they already work on the phase branch directly
|
||||||
if (task.category === 'merge') return;
|
if (task.category === 'merge') return;
|
||||||
|
|
||||||
const initBranch = initiativeBranchName(initiative.mergeTarget);
|
const initBranch = initiative.branch;
|
||||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||||
const tBranch = taskBranchName(initBranch, task.id);
|
const tBranch = taskBranchName(initBranch, task.id);
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export class ExecutionOrchestrator {
|
|||||||
if (!phase) return;
|
if (!phase) return;
|
||||||
|
|
||||||
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
||||||
if (!initiative?.mergeTarget) return;
|
if (!initiative?.branch) return;
|
||||||
|
|
||||||
if (initiative.executionMode === 'yolo') {
|
if (initiative.executionMode === 'yolo') {
|
||||||
await this.mergePhaseIntoInitiative(phaseId);
|
await this.mergePhaseIntoInitiative(phaseId);
|
||||||
@@ -178,9 +178,9 @@ export class ExecutionOrchestrator {
|
|||||||
if (!phase) return;
|
if (!phase) return;
|
||||||
|
|
||||||
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
const initiative = await this.initiativeRepository.findById(phase.initiativeId);
|
||||||
if (!initiative?.mergeTarget) return;
|
if (!initiative?.branch) return;
|
||||||
|
|
||||||
const initBranch = initiativeBranchName(initiative.mergeTarget);
|
const initBranch = initiative.branch;
|
||||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||||
|
|
||||||
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
||||||
|
|||||||
@@ -5,6 +5,23 @@
|
|||||||
* in the initiative → phase → task branch hierarchy.
|
* in the initiative → phase → task branch hierarchy.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task categories that run on the project's default branch (no initiative branch needed).
|
||||||
|
*/
|
||||||
|
export const PLANNING_CATEGORIES = ['research', 'discuss', 'plan', 'detail', 'refine'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task categories that require an initiative branch.
|
||||||
|
*/
|
||||||
|
export const EXECUTION_CATEGORIES = ['execute', 'verify', 'merge', 'review'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a task category is a planning category (runs on default branch).
|
||||||
|
*/
|
||||||
|
export function isPlanningCategory(category: string): boolean {
|
||||||
|
return (PLANNING_CATEGORIES as readonly string[]).includes(category);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a name to a URL/branch-safe slug.
|
* Convert a name to a URL/branch-safe slug.
|
||||||
* Lowercase, replace non-alphanumeric runs with single hyphens, trim hyphens.
|
* Lowercase, replace non-alphanumeric runs with single hyphens, trim hyphens.
|
||||||
@@ -17,11 +34,19 @@ export function slugify(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the initiative branch name.
|
* Generate an initiative branch name from the initiative name.
|
||||||
* Returns the mergeTarget as-is (it's already a branch name), or 'main' if unset.
|
* Format: `cw/<slugified-name>`
|
||||||
*/
|
*/
|
||||||
export function initiativeBranchName(mergeTarget: string | null): string {
|
export function generateInitiativeBranch(name: string): string {
|
||||||
return mergeTarget ?? 'main';
|
return `cw/${slugify(name)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the initiative branch name.
|
||||||
|
* Returns the branch as-is if set, or null if unset.
|
||||||
|
*/
|
||||||
|
export function initiativeBranchName(branch: string | null): string | null {
|
||||||
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
projectIds: z.array(z.string().min(1)).min(1).optional(),
|
projectIds: z.array(z.string().min(1)).min(1).optional(),
|
||||||
executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
|
executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
|
||||||
mergeTarget: z.string().nullable().optional(),
|
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const repo = requireInitiativeRepository(ctx);
|
const repo = requireInitiativeRepository(ctx);
|
||||||
@@ -36,7 +35,6 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
...(input.executionMode && { executionMode: input.executionMode }),
|
...(input.executionMode && { executionMode: input.executionMode }),
|
||||||
...(input.mergeTarget !== undefined && { mergeTarget: input.mergeTarget }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.projectIds && input.projectIds.length > 0) {
|
if (input.projectIds && input.projectIds.length > 0) {
|
||||||
@@ -102,11 +100,10 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
return repo.update(id, data);
|
return repo.update(id, data);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateInitiativeMergeConfig: publicProcedure
|
updateInitiativeConfig: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
initiativeId: z.string().min(1),
|
initiativeId: z.string().min(1),
|
||||||
mergeRequiresApproval: z.boolean().optional(),
|
mergeRequiresApproval: z.boolean().optional(),
|
||||||
mergeTarget: z.string().nullable().optional(),
|
|
||||||
executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
|
executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ export function projectProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
.input(z.object({
|
.input(z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
url: z.string().min(1),
|
url: z.string().min(1),
|
||||||
|
defaultBranch: z.string().min(1).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const repo = requireProjectRepository(ctx);
|
const repo = requireProjectRepository(ctx);
|
||||||
|
|
||||||
let project;
|
let project;
|
||||||
try {
|
try {
|
||||||
project = await repo.create({ name: input.name, url: input.url });
|
project = await repo.create({
|
||||||
|
name: input.name,
|
||||||
|
url: input.url,
|
||||||
|
...(input.defaultBranch && { defaultBranch: input.defaultBranch }),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = (error as Error).message;
|
const msg = (error as Error).message;
|
||||||
if (msg.includes('UNIQUE') || msg.includes('unique')) {
|
if (msg.includes('UNIQUE') || msg.includes('unique')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user