* Name agents by their first prompt line instead of an LLM summary The workspace title already captures the task; the LLM-generated agent title just restated the same prompt. Drop the generator and keep the provisional title (first prompt line), so the agent label stays deterministic and explicit titles still win. * Remove the no-op agent-title setting and leftover generator plumbing Deleting the agent-title generator left its whole surface dangling: a configurable "Agent titles" project setting that no longer did anything, dead create-path plumbing, generator-only import/MCP deps, and the mock provider's agent-title parser. Remove all of it, plus a dead test mock and timing sleeps that guarded the deleted background job. Replace them with a no-mock lifecycle test asserting the title stays the first prompt line across create and settle. Old paseo.json files with metadataGeneration.agentTitle still parse via schema passthrough — see COMPAT(projectMetadataAgentTitle).
247 lines
6.9 KiB
TypeScript
247 lines
6.9 KiB
TypeScript
import type {
|
|
PaseoConfigRaw,
|
|
PaseoMetadataGeneration,
|
|
PaseoMetadataGenerationEntry,
|
|
PaseoScriptEntryRaw,
|
|
} from "@getpaseo/protocol/messages";
|
|
|
|
export type LifecycleOriginalKind = "string" | "array" | "missing";
|
|
|
|
export const METADATA_PROMPT_KEYS = ["branchName", "commitMessage", "pullRequest"] as const;
|
|
export type MetadataPromptKey = (typeof METADATA_PROMPT_KEYS)[number];
|
|
|
|
export interface ProjectScriptDraft {
|
|
id: string;
|
|
name: string;
|
|
commandText: string;
|
|
commandOriginalKind: LifecycleOriginalKind;
|
|
type: string;
|
|
portText: string;
|
|
rawEntry: PaseoScriptEntryRaw;
|
|
}
|
|
|
|
export interface ProjectConfigDraft {
|
|
setupText: string;
|
|
setupOriginalKind: LifecycleOriginalKind;
|
|
teardownText: string;
|
|
teardownOriginalKind: LifecycleOriginalKind;
|
|
scripts: ProjectScriptDraft[];
|
|
metadataPrompts: Record<MetadataPromptKey, string>;
|
|
metadataGenerationBase: PaseoMetadataGeneration | undefined;
|
|
}
|
|
|
|
interface LifecycleProjection {
|
|
text: string;
|
|
kind: LifecycleOriginalKind;
|
|
}
|
|
|
|
function projectLifecycle(value: unknown): LifecycleProjection {
|
|
if (typeof value === "string") {
|
|
return { text: value, kind: "string" };
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const lines = value.filter((entry): entry is string => typeof entry === "string");
|
|
return { text: lines.join("\n"), kind: "array" };
|
|
}
|
|
return { text: "", kind: "missing" };
|
|
}
|
|
|
|
function lifecycleFromText(
|
|
text: string,
|
|
kind: LifecycleOriginalKind,
|
|
): string | string[] | undefined {
|
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
if (lines.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (kind === "string") {
|
|
return lines.join("\n");
|
|
}
|
|
if (kind === "array") {
|
|
return lines;
|
|
}
|
|
return lines.length === 1 ? lines[0] : lines;
|
|
}
|
|
|
|
function projectScriptType(value: unknown): string {
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function projectScriptPort(value: unknown): string {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return String(value);
|
|
}
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function parseScriptPort(value: string): number | string | undefined {
|
|
const trimmed = value.trim();
|
|
if (trimmed.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
const parsed = Number(trimmed);
|
|
if (Number.isFinite(parsed)) {
|
|
return parsed;
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
let scriptDraftIdCounter = 0;
|
|
|
|
function nextScriptDraftId(): string {
|
|
scriptDraftIdCounter += 1;
|
|
return `script-draft-${scriptDraftIdCounter}`;
|
|
}
|
|
|
|
function emptyMetadataPrompts(): Record<MetadataPromptKey, string> {
|
|
return {
|
|
branchName: "",
|
|
commitMessage: "",
|
|
pullRequest: "",
|
|
};
|
|
}
|
|
|
|
export function configToDraft(config: PaseoConfigRaw | null | undefined): ProjectConfigDraft {
|
|
const worktree = config?.worktree ?? {};
|
|
const setup = projectLifecycle(worktree.setup);
|
|
const teardown = projectLifecycle(worktree.teardown);
|
|
const scripts: ProjectScriptDraft[] = [];
|
|
|
|
const scriptsRecord = config?.scripts ?? {};
|
|
for (const [name, entry] of Object.entries(scriptsRecord)) {
|
|
const command = projectLifecycle(entry.command);
|
|
scripts.push({
|
|
id: nextScriptDraftId(),
|
|
name,
|
|
commandText: command.text,
|
|
commandOriginalKind: command.kind,
|
|
type: projectScriptType(entry.type),
|
|
portText: projectScriptPort(entry.port),
|
|
rawEntry: entry,
|
|
});
|
|
}
|
|
|
|
const metadataGeneration = config?.metadataGeneration;
|
|
const metadataPrompts = emptyMetadataPrompts();
|
|
for (const key of METADATA_PROMPT_KEYS) {
|
|
const instructions = metadataGeneration?.[key]?.instructions;
|
|
if (typeof instructions === "string") {
|
|
metadataPrompts[key] = instructions;
|
|
}
|
|
}
|
|
|
|
return {
|
|
setupText: setup.text,
|
|
setupOriginalKind: setup.kind,
|
|
teardownText: teardown.text,
|
|
teardownOriginalKind: teardown.kind,
|
|
scripts,
|
|
metadataPrompts,
|
|
metadataGenerationBase: metadataGeneration,
|
|
};
|
|
}
|
|
|
|
interface ApplyDraftInput {
|
|
draft: ProjectConfigDraft;
|
|
base: PaseoConfigRaw | null | undefined;
|
|
}
|
|
|
|
export function applyDraftToConfig(input: ApplyDraftInput): PaseoConfigRaw {
|
|
const baseConfig = input.base ?? {};
|
|
const baseWorktree = baseConfig.worktree ?? {};
|
|
|
|
const nextWorktree: Record<string, unknown> = { ...baseWorktree };
|
|
const nextSetup = lifecycleFromText(input.draft.setupText, input.draft.setupOriginalKind);
|
|
if (nextSetup === undefined) {
|
|
delete nextWorktree.setup;
|
|
} else {
|
|
nextWorktree.setup = nextSetup;
|
|
}
|
|
const nextTeardown = lifecycleFromText(
|
|
input.draft.teardownText,
|
|
input.draft.teardownOriginalKind,
|
|
);
|
|
if (nextTeardown === undefined) {
|
|
delete nextWorktree.teardown;
|
|
} else {
|
|
nextWorktree.teardown = nextTeardown;
|
|
}
|
|
|
|
const nextScripts: Record<string, PaseoScriptEntryRaw> = {};
|
|
for (const row of input.draft.scripts) {
|
|
const trimmedName = row.name.trim();
|
|
if (trimmedName.length === 0) {
|
|
continue;
|
|
}
|
|
const baseEntry = row.rawEntry;
|
|
const nextEntry: Record<string, unknown> = { ...baseEntry };
|
|
const nextCommand = lifecycleFromText(row.commandText, row.commandOriginalKind);
|
|
if (nextCommand === undefined) {
|
|
delete nextEntry.command;
|
|
} else {
|
|
nextEntry.command = nextCommand;
|
|
}
|
|
const trimmedType = row.type.trim();
|
|
if (trimmedType.length === 0) {
|
|
delete nextEntry.type;
|
|
} else {
|
|
nextEntry.type = trimmedType;
|
|
}
|
|
const nextPort = parseScriptPort(row.portText);
|
|
if (nextPort === undefined) {
|
|
delete nextEntry.port;
|
|
} else {
|
|
nextEntry.port = nextPort;
|
|
}
|
|
nextScripts[trimmedName] = nextEntry as PaseoScriptEntryRaw;
|
|
}
|
|
|
|
const nextMetadataGeneration: Record<string, unknown> = {
|
|
...input.draft.metadataGenerationBase,
|
|
};
|
|
for (const key of METADATA_PROMPT_KEYS) {
|
|
const text = input.draft.metadataPrompts[key];
|
|
const baseEntry = input.draft.metadataGenerationBase?.[key] as
|
|
| PaseoMetadataGenerationEntry
|
|
| undefined;
|
|
if (text.trim().length === 0) {
|
|
if (baseEntry) {
|
|
const nextEntry: Record<string, unknown> = { ...baseEntry };
|
|
delete nextEntry.instructions;
|
|
if (Object.keys(nextEntry).length === 0) {
|
|
delete nextMetadataGeneration[key];
|
|
} else {
|
|
nextMetadataGeneration[key] = nextEntry;
|
|
}
|
|
} else {
|
|
delete nextMetadataGeneration[key];
|
|
}
|
|
} else {
|
|
nextMetadataGeneration[key] = { ...baseEntry, instructions: text };
|
|
}
|
|
}
|
|
|
|
const result: Record<string, unknown> = { ...baseConfig };
|
|
if (Object.keys(nextWorktree).length === 0) {
|
|
delete result.worktree;
|
|
} else {
|
|
result.worktree = nextWorktree;
|
|
}
|
|
if (Object.keys(nextScripts).length === 0) {
|
|
delete result.scripts;
|
|
} else {
|
|
result.scripts = nextScripts;
|
|
}
|
|
if (Object.keys(nextMetadataGeneration).length === 0) {
|
|
delete result.metadataGeneration;
|
|
} else {
|
|
result.metadataGeneration = nextMetadataGeneration;
|
|
}
|
|
return result as PaseoConfigRaw;
|
|
}
|