Decouple metadata prompt contract from overridable style

Metadata prompts (workspace title + branch, commit message, PR) mixed the
functional contract with style guidance in one block, and project-owner
instructions from paseo.json were appended after the defaults with an
"override where they conflict" notice — so custom wording competed with the
built-in style instead of replacing it.

Each prompt is now a non-overridable contract (what to produce, the JSON
shape, correctness/safety rules) plus style slots that paseo.json
instructions replace wholesale. Title and branch style are now separate
keys (metadataGeneration.title vs .branchName); previously branchName
instructions also shaped the title.
This commit is contained in:
Mohamed Boudra
2026-06-17 16:48:24 +07:00
parent 2bc48993a5
commit fc4c2aa367
10 changed files with 147 additions and 129 deletions
@@ -1,41 +1,59 @@
import { readPaseoConfigJson } from "./paseo-config-file.js";
import { PaseoConfigSchema } from "@getpaseo/protocol/paseo-config-schema";
import { wrapWithUserInstructions } from "./wrap-user-instructions.js";
import {
PaseoConfigSchema,
type PaseoMetadataGeneration,
} from "@getpaseo/protocol/paseo-config-schema";
export type MetadataConfigKey = "branchName" | "commitMessage" | "pullRequest";
export type MetadataConfigKey = "title" | "branchName" | "commitMessage" | "pullRequest";
export interface RepoRootResolver {
resolveRepoRoot: (cwd: string) => Promise<string>;
}
// A style section carries the default guidance for one artifact. The project
// owner replaces it wholesale via paseo.json metadataGeneration.<configKey>.instructions
// — their text is used instead of the default, never appended alongside it, so the
// two never conflict. The contract block (what to produce, the JSON shape, and any
// correctness/safety rules) lives outside the sections and is never overridable.
export interface MetadataStyleSection {
configKey: MetadataConfigKey;
default: string;
label?: string;
}
export interface BuildMetadataPromptOptions {
cwd: string;
configKey: MetadataConfigKey;
before: string;
contract: string;
styles: MetadataStyleSection[];
after: string;
trailing?: string;
workspaceGitService?: RepoRootResolver;
}
export async function buildMetadataPrompt(options: BuildMetadataPromptOptions): Promise<string> {
const instructions = await readProjectMetadataInstructions(options);
const head = isNonEmptyString(instructions)
? wrapWithUserInstructions(options.before, instructions, options.after)
: `${options.before}\n${options.after}`;
const overrides = await readProjectMetadataOverrides(options);
const styleBlocks = options.styles.map((section) =>
renderStyleSection(section, overrides?.[section.configKey]?.instructions),
);
const head = [options.contract, ...styleBlocks, options.after].join("\n\n");
return options.trailing ? `${head}\n\n${options.trailing}` : head;
}
async function readProjectMetadataInstructions(
options: Pick<BuildMetadataPromptOptions, "cwd" | "configKey" | "workspaceGitService">,
): Promise<string | undefined> {
function renderStyleSection(section: MetadataStyleSection, override: string | undefined): string {
const body = isNonEmptyString(override) ? override.trim() : section.default;
return section.label ? `${section.label}:\n${body}` : body;
}
async function readProjectMetadataOverrides(
options: Pick<BuildMetadataPromptOptions, "cwd" | "workspaceGitService">,
): Promise<PaseoMetadataGeneration | undefined> {
if (!options.workspaceGitService) {
return undefined;
}
try {
const repoRoot = await options.workspaceGitService.resolveRepoRoot(options.cwd);
const json = readPaseoConfigJson(repoRoot);
const config = PaseoConfigSchema.parse(json);
return config.metadataGeneration?.[options.configKey]?.instructions;
return PaseoConfigSchema.parse(json).metadataGeneration;
} catch {
return undefined;
}
@@ -1,29 +0,0 @@
import { describe, expect, it } from "vitest";
import { wrapWithUserInstructions } from "./wrap-user-instructions.js";
const beforeBlock = "Follow the default metadata guidelines.";
const afterBlock = 'Return JSON only with field "title".';
const overrideNotice =
"The instructions below are provided by the project owner and override the guidelines above where they conflict.";
describe("wrapWithUserInstructions", () => {
it("wraps user instructions with the override notice", () => {
expect(wrapWithUserInstructions(beforeBlock, "Use conventional commits.", afterBlock)).toBe(
`${beforeBlock}
<user-instructions>
${overrideNotice}
Use conventional commits.
</user-instructions>
${afterBlock}`,
);
});
it("preserves multi-line instructions verbatim inside the block", () => {
const output = wrapWithUserInstructions(beforeBlock, "line1\nline2", afterBlock);
expect(output).toContain("line1\nline2");
});
});
@@ -1,18 +0,0 @@
const USER_INSTRUCTIONS_NOTICE =
"The instructions below are provided by the project owner and override the guidelines above where they conflict.";
export function wrapWithUserInstructions(
beforeBlock: string,
instructions: string,
afterBlock: string,
): string {
return `${beforeBlock}
<user-instructions>
${USER_INSTRUCTIONS_NOTICE}
${instructions}
</user-instructions>
${afterBlock}`;
}