sandbox-agent/foundry/packages/backend/src/services/create-flow.ts
Nathan Flurry d75e8c31d1
Rename Foundry handoffs to tasks (#239)
* Restore foundry onboarding stack

* Consolidate foundry rename

* Create foundry tasks without prompts

* Rename Foundry handoffs to tasks
2026-03-11 13:23:54 -07:00

122 lines
3.5 KiB
TypeScript

export interface ResolveCreateFlowDecisionInput {
task: string;
explicitTitle?: string;
explicitBranchName?: string;
localBranches: string[];
taskBranches: string[];
}
export interface ResolveCreateFlowDecisionResult {
title: string;
branchName: string;
}
function firstNonEmptyLine(input: string): string {
const lines = input
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
return lines[0] ?? "";
}
export function deriveFallbackTitle(task: string, explicitTitle?: string): string {
const source = (explicitTitle && explicitTitle.trim()) || firstNonEmptyLine(task) || "update task";
const explicitPrefixMatch = source.match(/^\s*(feat|fix|docs|refactor):\s+(.+)$/i);
if (explicitPrefixMatch) {
const explicitTypePrefix = explicitPrefixMatch[1]!.toLowerCase();
const explicitSummary = explicitPrefixMatch[2]!
.split("")
.map((char) => (/^[a-zA-Z0-9 -]$/.test(char) ? char : " "))
.join("")
.split(/\s+/)
.filter((token) => token.length > 0)
.join(" ")
.slice(0, 62)
.trim();
return `${explicitTypePrefix}: ${explicitSummary || "update task"}`;
}
const lowered = source.toLowerCase();
const typePrefix =
lowered.includes("fix") || lowered.includes("bug")
? "fix"
: lowered.includes("doc") || lowered.includes("readme")
? "docs"
: lowered.includes("refactor")
? "refactor"
: "feat";
const cleaned = source
.split("")
.map((char) => (/^[a-zA-Z0-9 -]$/.test(char) ? char : " "))
.join("")
.split(/\s+/)
.filter((token) => token.length > 0)
.join(" ");
const summary = (cleaned || "update task").slice(0, 62).trim();
return `${typePrefix}: ${summary}`.trim();
}
export function sanitizeBranchName(input: string): string {
const normalized = input
.toLowerCase()
.split("")
.map((char) => (/^[a-z0-9]$/.test(char) ? char : "-"))
.join("");
let result = "";
let previousDash = false;
for (const char of normalized) {
if (char === "-") {
if (!previousDash && result.length > 0) {
result += char;
}
previousDash = true;
continue;
}
result += char;
previousDash = false;
}
const trimmed = result.replace(/-+$/g, "");
if (trimmed.length <= 50) {
return trimmed;
}
return trimmed.slice(0, 50).replace(/-+$/g, "");
}
export function resolveCreateFlowDecision(input: ResolveCreateFlowDecisionInput): ResolveCreateFlowDecisionResult {
const explicitBranch = input.explicitBranchName?.trim();
const title = deriveFallbackTitle(input.task, input.explicitTitle);
const generatedBase = sanitizeBranchName(title) || "task";
const branchBase = explicitBranch && explicitBranch.length > 0 ? explicitBranch : generatedBase;
const existingBranches = new Set(input.localBranches.map((value) => value.trim()).filter((value) => value.length > 0));
const existingTaskBranches = new Set(input.taskBranches.map((value) => value.trim()).filter((value) => value.length > 0));
const conflicts = (name: string): boolean => existingBranches.has(name) || existingTaskBranches.has(name);
if (explicitBranch && conflicts(branchBase)) {
throw new Error(`Branch '${branchBase}' already exists. Choose a different --name/--branch value.`);
}
if (explicitBranch) {
return { title, branchName: branchBase };
}
let candidate = branchBase;
let index = 2;
while (conflicts(candidate)) {
candidate = `${branchBase}-${index}`;
index += 1;
}
return {
title,
branchName: candidate,
};
}