import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { parseFrontmatter } from "@earendil-works/pi-coding-agent"; import { parseChainDeclaration } from "./chain-parser.js"; import { extractPromptInlineIncludes, hasPromptIncludeDirectives, hasPromptIncludesPlaceholder, renderPromptIncludes, type PromptIncludeGraph, } from "./prompt-includes.js"; const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; export const RESERVED_COMMAND_NAMES = new Set([ "chain-prompts", "compare-presets", "compare-runs", "best-of-n-presets", "best-of-n-runs", "print-prompt", "dry-run-prompt", "prompt-tool", "validate-prompts", "settings", "model", "scoped-models", "export", "share", "copy", "name", "session", "changelog", "hotkeys", "fork", "tree", "login", "logout", "new", "compact", "resume", "reload", "quit", ]); export type PromptSource = "user" | "project"; export type PromptRootKind = "prompts" | "prompt-library"; interface PromptRoot { source: PromptSource; kind: PromptRootKind; dir: string; } export interface DelegationLineupSlot { agent: string; model?: string; task?: string; taskSuffix?: string; cwd?: string; count?: number; } export type DeterministicHandoff = "always" | "never" | "on-success" | "on-failure"; export type DeterministicExecution = | { kind: "run"; command: string } | { kind: "command"; command: string; args: string[]; shell: boolean } | { kind: "script"; path: string; args: string[] }; export type DeterministicEnv = Record; export interface DeterministicStep { execution: DeterministicExecution; handoff: DeterministicHandoff; nonInteractive: boolean; timeoutMs?: number; cwd?: string; env?: DeterministicEnv; } export interface PromptWithModel { name: string; description: string; content: string; models: string[]; includes?: string[]; chain?: string; chainContext?: "summary"; restore: boolean; hidden?: boolean; skill?: string; skills?: string[]; thinking?: ThinkingLevel; thinkingLevels?: ThinkingLevel[]; rotate?: boolean; fresh?: boolean; loop?: number | null; converge?: boolean; boomerang?: boolean; parallel?: number; worktree?: boolean; deterministic?: DeterministicStep; subagent?: true | string; inheritContext?: boolean; cwd?: string; workers?: DelegationLineupSlot[]; reviewers?: DelegationLineupSlot[]; finalApplier?: DelegationLineupSlot; commit?: "ask"; preset?: string; source: PromptSource; rootKind: PromptRootKind; subdir?: string; filePath: string; includeGraph?: PromptIncludeGraph; } export interface PromptLoaderDiagnostic { code: string; message: string; filePath: string; source: PromptSource; key: string; } export interface LoadPromptsWithModelResult { prompts: Map; diagnostics: PromptLoaderDiagnostic[]; } export interface PromptSourceRecord { promptName: string; filePath: string; promptRoot: string; cwd: string; source: PromptSource; rootKind: PromptRootKind; promptCapable: boolean; rawBody: string; includes?: string[]; hasInlineIncludes: boolean; hasIncludesPlaceholder: boolean; isChainWrapper: boolean; hidden?: boolean; includeMetadataInvalid?: boolean; skippedReason?: string; } export interface CollectPromptSourceRecordsResult { records: PromptSourceRecord[]; inventoryRecords: PromptSourceRecord[]; diagnostics: PromptLoaderDiagnostic[]; } function createDiagnostic( code: string, filePath: string, source: PromptSource, message: string, ): PromptLoaderDiagnostic { return { code, message, filePath, source, key: `${code}:${filePath}:${message}`, }; } function lexicalCompare(a: string, b: string): number { if (a < b) return -1; if (a > b) return 1; return 0; } function normalizeStringField( field: string, value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): string | undefined { if (value === undefined) return undefined; if (typeof value !== "string") { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: expected a string.`, ), ); return undefined; } const normalized = value.trim(); return normalized.length > 0 ? normalized : undefined; } function isFrontmatterRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function isValidModelSelectionSpec(spec: string): boolean { if (!spec || spec.includes("*") || /\s/.test(spec)) return false; const slashIndex = spec.indexOf("/"); if (slashIndex === -1) return true; if (slashIndex === 0) return false; const modelId = spec.slice(slashIndex + 1); if (modelId.length === 0) return false; if (modelId.split("/").some((segment) => segment.length === 0)) return false; return true; } function normalizeFrontmatterRecord( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): Record | undefined { if (isFrontmatterRecord(value)) { return value; } diagnostics.push( createDiagnostic( "invalid-frontmatter", filePath, source, `Skipping prompt template at ${filePath}: frontmatter must be a key-value object.`, ), ); return undefined; } function normalizeModelSpecs( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): string[] | undefined { if (value === undefined) return undefined; if (typeof value !== "string") { diagnostics.push( createDiagnostic( "invalid-model", filePath, source, `Skipping prompt template at ${filePath}: frontmatter field "model" must be a string.`, ), ); return undefined; } const models = value .split(",") .map((item) => item.trim()) .filter(Boolean); if (models.length === 0) { diagnostics.push( createDiagnostic( "empty-model", filePath, source, `Skipping prompt template at ${filePath}: frontmatter field "model" is empty.`, ), ); return undefined; } const invalidSpec = models.find((model) => !isValidModelSelectionSpec(model)); if (invalidSpec) { diagnostics.push( createDiagnostic( "invalid-model-spec", filePath, source, `Skipping prompt template at ${filePath}: invalid model spec ${JSON.stringify(invalidSpec)} in frontmatter field "model".`, ), ); return undefined; } return models; } function normalizeRestore( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return true; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-restore", filePath, source, `Using default restore=true for ${filePath}: frontmatter field "restore" must be true or false.`, ), ); return true; } function normalizeFresh( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-fresh", filePath, source, `Using default fresh=false for ${filePath}: frontmatter field "fresh" must be true or false.`, ), ); return false; } function normalizeHidden( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-hidden", filePath, source, `Using default hidden=false for ${filePath}: frontmatter field "hidden" must be true or false.`, ), ); return false; } function normalizeRotate( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-rotate", filePath, source, `Using default rotate=false for ${filePath}: frontmatter field "rotate" must be true or false.`, ), ); return false; } function normalizeBoomerang( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-boomerang", filePath, source, `Using default boomerang=false for ${filePath}: frontmatter field "boomerang" must be true or false.`, ), ); return false; } function normalizeLoop( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): number | null | undefined { if (value === undefined) return undefined; if (value === true || (typeof value === "string" && value.trim().toLowerCase() === "unlimited")) { return null; } let normalizedValue: number | undefined; if (typeof value === "number") { normalizedValue = value; } else if (typeof value === "string" && /^\d+$/.test(value.trim())) { normalizedValue = parseInt(value.trim(), 10); } if (normalizedValue !== undefined && Number.isInteger(normalizedValue) && normalizedValue >= 1 && normalizedValue <= 999) { return normalizedValue; } diagnostics.push( createDiagnostic( "invalid-loop", filePath, source, `Ignoring invalid loop value in ${filePath}: frontmatter field "loop" must be an integer between 1 and 999, true, or "unlimited".`, ), ); return undefined; } function normalizeParallel( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): number | undefined { if (value === undefined) return undefined; let normalizedValue: number | undefined; if (typeof value === "number") { normalizedValue = value; } else if (typeof value === "string" && /^\d+$/.test(value.trim())) { normalizedValue = parseInt(value.trim(), 10); } if (normalizedValue !== undefined && Number.isInteger(normalizedValue) && normalizedValue >= 2) { return normalizedValue; } diagnostics.push( createDiagnostic( "invalid-parallel", filePath, source, `Ignoring invalid parallel value in ${filePath}: frontmatter field "parallel" must be an integer greater than or equal to 2.`, ), ); return undefined; } function normalizeStringArrayField( field: string, value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): string[] | undefined { if (value === undefined) return []; if (!Array.isArray(value)) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: expected an array of strings.`, ), ); return undefined; } const args: string[] = []; for (const entry of value) { if (typeof entry !== "string") { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: expected an array of strings.`, ), ); return undefined; } args.push(entry); } return args; } const VALID_EXACT_SKILL_NAME = /^[A-Za-z0-9._-]+$/; const VALID_SUFFIX_WILDCARD_SKILL_SELECTOR = /^[A-Za-z0-9._-]+\*$/; function normalizeSkillName(raw: string): string { const trimmed = raw.trim(); return trimmed.startsWith("skill:") ? trimmed.slice("skill:".length).trim() : trimmed; } function isValidSkillNameOrSelector(value: string): boolean { return VALID_EXACT_SKILL_NAME.test(value) || VALID_SUFFIX_WILDCARD_SKILL_SELECTOR.test(value); } function invalidSkillNameMessage(field: "skill" | "skills", value: string): string { if (value.includes("*")) { return `frontmatter field "${field}" contains invalid skill wildcard ${JSON.stringify(value)}: only non-empty suffix "*" prefix matching is supported.`; } return `frontmatter field "${field}" contains invalid skill name ${JSON.stringify(value)}.`; } function pushInvalidSkillsDiagnostic( filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], message: string, ) { diagnostics.push(createDiagnostic("invalid-skills", filePath, source, `Skipping prompt template at ${filePath}: ${message}`)); } type NormalizedSkills = { ok: true; skill?: string; skills?: string[] } | { ok: false }; function normalizePromptSkills( frontmatter: Record, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): NormalizedSkills { const normalizedSkills: string[] = []; let normalizedSkill: string | undefined; if (Object.hasOwn(frontmatter, "skill")) { if (typeof frontmatter.skill !== "string") { pushInvalidSkillsDiagnostic(filePath, source, diagnostics, 'frontmatter field "skill" must be a non-empty string.'); return { ok: false }; } normalizedSkill = normalizeSkillName(frontmatter.skill); if (!normalizedSkill || !isValidSkillNameOrSelector(normalizedSkill)) { pushInvalidSkillsDiagnostic(filePath, source, diagnostics, invalidSkillNameMessage("skill", normalizedSkill)); return { ok: false }; } normalizedSkills.push(normalizedSkill); } if (Object.hasOwn(frontmatter, "skills")) { if (!Array.isArray(frontmatter.skills)) { pushInvalidSkillsDiagnostic(filePath, source, diagnostics, 'frontmatter field "skills" must be a YAML list of non-empty skill names. Use "skill" for a scalar single skill.'); return { ok: false }; } for (const entry of frontmatter.skills) { if (typeof entry !== "string") { pushInvalidSkillsDiagnostic(filePath, source, diagnostics, 'frontmatter field "skills" must be a YAML list of non-empty strings.'); return { ok: false }; } const normalized = normalizeSkillName(entry); if (!normalized || !isValidSkillNameOrSelector(normalized)) { pushInvalidSkillsDiagnostic(filePath, source, diagnostics, invalidSkillNameMessage("skills", normalized)); return { ok: false }; } normalizedSkills.push(normalized); } } return { ok: true, ...(normalizedSkill ? { skill: normalizedSkill } : {}), ...(normalizedSkills.length > 0 ? { skills: normalizedSkills } : {}), }; } type NormalizedPromptIncludes = | { ok: true; includes: string[] | undefined; declaredKey?: "include" | "includes" } | { ok: false }; function normalizePromptIncludes( frontmatter: Record, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): NormalizedPromptIncludes { const hasInclude = Object.hasOwn(frontmatter, "include"); const hasIncludes = Object.hasOwn(frontmatter, "includes"); if (!hasInclude && !hasIncludes) return { ok: true, includes: undefined }; if (hasInclude && hasIncludes) { diagnostics.push( createDiagnostic( "invalid-includes-conflict", filePath, source, `Skipping prompt template at ${filePath}: frontmatter fields "include" and "includes" cannot be combined.`, ), ); return { ok: false }; } if (hasInclude) { const value = frontmatter.include; if (typeof value !== "string" || value.trim().length === 0) { diagnostics.push( createDiagnostic( "invalid-include", filePath, source, `Skipping prompt template at ${filePath}: frontmatter field "include" must be a non-empty string.`, ), ); return { ok: false }; } return { ok: true, includes: [value.trim()], declaredKey: "include" }; } const value = frontmatter.includes; if (!Array.isArray(value)) { diagnostics.push( createDiagnostic( "invalid-includes", filePath, source, `Skipping prompt template at ${filePath}: frontmatter field "includes" must be an array of non-empty strings.`, ), ); return { ok: false }; } const includes: string[] = []; for (const entry of value) { if (typeof entry !== "string" || entry.trim().length === 0) { diagnostics.push( createDiagnostic( "invalid-includes", filePath, source, `Skipping prompt template at ${filePath}: frontmatter field "includes" must be an array of non-empty strings.`, ), ); return { ok: false }; } includes.push(entry.trim()); } return { ok: true, includes, declaredKey: "includes" }; } function normalizeDeterministicHandoff( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DeterministicHandoff { if (value === undefined) return "always"; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "always" || normalized === "never" || normalized === "on-success" || normalized === "on-failure") { return normalized; } } diagnostics.push( createDiagnostic( "invalid-deterministic-handoff", filePath, source, `Using default deterministic handoff=always for ${filePath}: expected "always", "never", "on-success", or "on-failure".`, ), ); return "always"; } function normalizeTimeoutMs( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): number | undefined { if (value === undefined) return undefined; let timeoutMs: number | undefined; if (typeof value === "number") timeoutMs = value; if (typeof value === "string" && /^\d+$/.test(value.trim())) timeoutMs = parseInt(value.trim(), 10); if (timeoutMs !== undefined && Number.isInteger(timeoutMs) && timeoutMs >= 1) return timeoutMs; diagnostics.push( createDiagnostic( "invalid-deterministic-timeout", filePath, source, `Ignoring invalid deterministic timeout in ${filePath}: expected an integer greater than or equal to 1 (milliseconds).`, ), ); return undefined; } function normalizeDeterministicEnv( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DeterministicEnv | undefined { if (value === undefined) return undefined; if (!value || typeof value !== "object" || Array.isArray(value)) { diagnostics.push( createDiagnostic( "invalid-deterministic-env", filePath, source, `Ignoring invalid deterministic env in ${filePath}: expected an object with string/number/boolean values.`, ), ); return undefined; } const env: DeterministicEnv = {}; for (const [key, raw] of Object.entries(value as Record)) { if (!key.trim()) { diagnostics.push( createDiagnostic( "invalid-deterministic-env", filePath, source, `Ignoring invalid deterministic env in ${filePath}: env keys must be non-empty strings.`, ), ); return undefined; } if (typeof raw !== "string" && typeof raw !== "number" && typeof raw !== "boolean") { diagnostics.push( createDiagnostic( "invalid-deterministic-env", filePath, source, `Ignoring invalid deterministic env in ${filePath}: env value for ${JSON.stringify(key)} must be a string, number, or boolean.`, ), ); return undefined; } env[key] = String(raw); } return Object.keys(env).length > 0 ? env : undefined; } function normalizeDeterministicNonInteractive( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return true; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-deterministic-non-interactive", filePath, source, `Using default deterministic nonInteractive=true for ${filePath}: expected true or false.`, ), ); return true; } function normalizeDeterministicRunValue( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DeterministicExecution | undefined { if (typeof value === "string") { const command = value.trim(); if (command) return { kind: "run", command }; diagnostics.push( createDiagnostic( "invalid-deterministic-run", filePath, source, `Ignoring invalid deterministic run value in ${filePath}: expected a non-empty string or an object with command/args.`, ), ); return undefined; } if (!value || typeof value !== "object" || Array.isArray(value)) { diagnostics.push( createDiagnostic( "invalid-deterministic-run", filePath, source, `Ignoring invalid deterministic run value in ${filePath}: expected a non-empty string or an object with command/args.`, ), ); return undefined; } const record = value as Record; const command = normalizeStringField("deterministic.run.command", record.command, filePath, source, diagnostics); if (!command) { diagnostics.push( createDiagnostic( "invalid-deterministic-run", filePath, source, `Ignoring invalid deterministic run value in ${filePath}: expected object field "command" to be a non-empty string.`, ), ); return undefined; } const args = normalizeStringArrayField("deterministic.run.args", record.args, filePath, source, diagnostics); if (!args) return undefined; let shell = false; if (record.shell !== undefined) { if (typeof record.shell === "boolean") { shell = record.shell; } else { diagnostics.push( createDiagnostic( "invalid-deterministic-run", filePath, source, `Ignoring invalid deterministic run value in ${filePath}: object field "shell" must be true or false.`, ), ); return undefined; } } return { kind: "command", command, args, shell }; } function normalizeDeterministicScriptValue( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DeterministicExecution | undefined { if (typeof value === "string") { const path = value.trim(); if (path) return { kind: "script", path, args: [] }; diagnostics.push( createDiagnostic( "invalid-deterministic-script", filePath, source, `Ignoring invalid deterministic script value in ${filePath}: expected a non-empty string or an object with path/args.`, ), ); return undefined; } if (!value || typeof value !== "object" || Array.isArray(value)) { diagnostics.push( createDiagnostic( "invalid-deterministic-script", filePath, source, `Ignoring invalid deterministic script value in ${filePath}: expected a non-empty string or an object with path/args.`, ), ); return undefined; } const record = value as Record; const path = normalizeStringField("deterministic.script.path", record.path, filePath, source, diagnostics); if (!path) { diagnostics.push( createDiagnostic( "invalid-deterministic-script", filePath, source, `Ignoring invalid deterministic script value in ${filePath}: expected object field "path" to be a non-empty string.`, ), ); return undefined; } const args = normalizeStringArrayField("deterministic.script.args", record.args, filePath, source, diagnostics); if (!args) return undefined; return { kind: "script", path, args }; } function normalizeDeterministic( frontmatter: Record, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DeterministicStep | undefined { const hasNested = Object.hasOwn(frontmatter, "deterministic"); const hasRun = Object.hasOwn(frontmatter, "run"); const hasScript = Object.hasOwn(frontmatter, "script"); const hasHandoff = Object.hasOwn(frontmatter, "handoff"); const hasTimeout = Object.hasOwn(frontmatter, "timeout"); const hasEnv = Object.hasOwn(frontmatter, "env"); const hasNonInteractive = Object.hasOwn(frontmatter, "nonInteractive"); if (!hasNested && !hasRun && !hasScript && !hasHandoff && !hasTimeout && !hasEnv && !hasNonInteractive) return undefined; if (hasNested && (hasRun || hasScript || hasHandoff || hasTimeout || hasEnv || hasNonInteractive)) { diagnostics.push( createDiagnostic( "invalid-deterministic-mixed-shorthand", filePath, source, `Ignoring top-level deterministic shorthand in ${filePath}: use either "deterministic" or top-level run/script/handoff/timeout/env/nonInteractive, not both.`, ), ); } let record: Record; if (hasNested) { const raw = frontmatter.deterministic; if (!raw || typeof raw !== "object" || Array.isArray(raw)) { diagnostics.push( createDiagnostic( "invalid-deterministic", filePath, source, `Ignoring invalid deterministic config in ${filePath}: frontmatter field "deterministic" must be an object.`, ), ); return undefined; } record = raw as Record; } else { record = { run: frontmatter.run, script: frontmatter.script, handoff: frontmatter.handoff, timeout: frontmatter.timeout, env: frontmatter.env, nonInteractive: frontmatter.nonInteractive, }; } const runValue = Object.hasOwn(record, "run") ? record.run : undefined; const scriptValue = Object.hasOwn(record, "script") ? record.script : undefined; if (runValue !== undefined && scriptValue !== undefined) { diagnostics.push( createDiagnostic( "invalid-deterministic", filePath, source, `Ignoring deterministic config in ${filePath}: "run" and "script" cannot be declared together.`, ), ); return undefined; } const execution = runValue !== undefined ? normalizeDeterministicRunValue(runValue, filePath, source, diagnostics) : scriptValue !== undefined ? normalizeDeterministicScriptValue(scriptValue, filePath, source, diagnostics) : undefined; if (!execution) { diagnostics.push( createDiagnostic( "invalid-deterministic", filePath, source, `Ignoring deterministic config in ${filePath}: expected either "run" or "script".`, ), ); return undefined; } const handoff = normalizeDeterministicHandoff(record.handoff, filePath, source, diagnostics); const timeoutMs = normalizeTimeoutMs(record.timeout, filePath, source, diagnostics); const cwd = normalizeCwd(record.cwd, filePath, source, diagnostics); const env = normalizeDeterministicEnv(record.env, filePath, source, diagnostics); const nonInteractive = normalizeDeterministicNonInteractive(record.nonInteractive, filePath, source, diagnostics); return { execution, handoff, nonInteractive, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(cwd ? { cwd } : {}), ...(env ? { env } : {}), }; } function normalizeLineupSlot( value: unknown, field: "workers" | "reviewers" | "finalApplier", filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], index: number, ): DelegationLineupSlot | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} must be an object.`, ), ); return undefined; } const slot = value as Record; if (slot.agent !== undefined && slot.subagent !== undefined) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} cannot combine "agent" and "subagent".`, ), ); return undefined; } let agent: string | undefined; if (typeof slot.agent === "string" && slot.agent.trim()) { agent = slot.agent.trim(); } else if (slot.agent !== undefined) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} requires a non-empty string "agent".`, ), ); return undefined; } if (!agent && slot.subagent !== undefined) { if (slot.subagent === true) { agent = field === "reviewers" ? "reviewer" : "delegate"; } else if (typeof slot.subagent === "string" && slot.subagent.trim()) { agent = slot.subagent.trim(); } else { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} requires "subagent" to be true or a non-empty string.`, ), ); return undefined; } } if (!agent) { agent = field === "reviewers" ? "reviewer" : "delegate"; } const normalized: DelegationLineupSlot = { agent, }; if (slot.model !== undefined) { if (typeof slot.model !== "string" || !slot.model.trim()) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} has an invalid "model".`, ), ); return undefined; } const modelSpec = slot.model.trim(); if (!isValidModelSelectionSpec(modelSpec)) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} has invalid model spec ${JSON.stringify(modelSpec)}.`, ), ); return undefined; } normalized.model = modelSpec; } if (slot.task !== undefined) { if (typeof slot.task !== "string") { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} has a non-string "task".`, ), ); return undefined; } const task = slot.task.trim(); if (task) normalized.task = task; } if (slot.taskSuffix !== undefined) { if (typeof slot.taskSuffix !== "string") { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} has a non-string "taskSuffix".`, ), ); return undefined; } const taskSuffix = slot.taskSuffix.trim(); if (taskSuffix) normalized.taskSuffix = taskSuffix; } if (slot.cwd !== undefined) { if (typeof slot.cwd !== "string") { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} has a non-string "cwd".`, ), ); return undefined; } const cwdRaw = slot.cwd.trim(); if (cwdRaw) { const expanded = expandCwdPath(cwdRaw); if (!expanded) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} "cwd" must be an absolute path.`, ), ); return undefined; } normalized.cwd = expanded; } } if (slot.count !== undefined) { let count: number | undefined; if (typeof slot.count === "number") { count = slot.count; } else if (typeof slot.count === "string" && /^\d+$/.test(slot.count.trim())) { count = parseInt(slot.count.trim(), 10); } if (count === undefined || !Number.isInteger(count) || count < 1) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: slot ${index + 1} "count" must be an integer greater than or equal to 1.`, ), ); return undefined; } normalized.count = count; } return normalized; } function normalizeLineup( value: unknown, field: "workers" | "reviewers", filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DelegationLineupSlot[] | undefined { if (value === undefined) return undefined; if (!Array.isArray(value)) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: expected an array of slot objects.`, ), ); return undefined; } if (value.length === 0) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring invalid ${field} value in ${filePath}: expected at least one slot.`, ), ); return undefined; } const slots: DelegationLineupSlot[] = []; for (let i = 0; i < value.length; i++) { const normalized = normalizeLineupSlot(value[i], field, filePath, source, diagnostics, i); if (!normalized) return undefined; slots.push(normalized); } return slots; } function normalizeFinalApplier( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): DelegationLineupSlot | undefined { if (value === undefined) return undefined; const normalized = normalizeLineupSlot(value, "finalApplier", filePath, source, diagnostics, 0); if (!normalized) return undefined; const slot = value as Record; if (Object.hasOwn(slot, "count")) { diagnostics.push( createDiagnostic( "invalid-final-applier", filePath, source, `Ignoring invalid finalApplier value in ${filePath}: slot 1 "count" is not supported.`, ), ); return undefined; } if (Object.hasOwn(slot, "cwd")) { diagnostics.push( createDiagnostic( "invalid-final-applier", filePath, source, `Ignoring invalid finalApplier value in ${filePath}: slot 1 "cwd" is not supported.`, ), ); return undefined; } return normalized; } function normalizeBestOfN( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): Record | undefined { if (value === undefined) return undefined; if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record; } diagnostics.push( createDiagnostic( "invalid-best-of-n", filePath, source, `Ignoring invalid bestOfN value in ${filePath}: frontmatter field "bestOfN" must be an object.`, ), ); return undefined; } function normalizeBestOfNCommit( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): "ask" | undefined { if (value === undefined) return undefined; if (typeof value === "string" && value.trim().toLowerCase() === "ask") return "ask"; diagnostics.push( createDiagnostic( "invalid-best-of-n-commit", filePath, source, `Ignoring invalid bestOfN.commit value in ${filePath}: expected "ask".`, ), ); return undefined; } function pushLegacyCompareFieldDiagnostic( field: "workers" | "reviewers" | "finalApplier", filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ) { diagnostics.push( createDiagnostic( `invalid-${field}`, filePath, source, `Ignoring top-level ${field} in ${filePath}: compare template authoring moved to "bestOfN.${field}".`, ), ); } function normalizeConverge( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return true; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-converge", filePath, source, `Using default converge=true for ${filePath}: frontmatter field "converge" must be true or false.`, ), ); return true; } function normalizeWorktree( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-worktree", filePath, source, `Using default worktree=false for ${filePath}: frontmatter field "worktree" must be true or false.`, ), ); return false; } function normalizeSubagent( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): true | string | undefined { if (value === undefined) return undefined; if (value === true) return true; if (value === false) return undefined; if (typeof value !== "string") { diagnostics.push( createDiagnostic( "invalid-subagent", filePath, source, `Ignoring invalid subagent value in ${filePath}: frontmatter field "subagent" must be true or a non-empty string.`, ), ); return undefined; } const normalized = value.trim(); if (!normalized) { diagnostics.push( createDiagnostic( "invalid-subagent", filePath, source, `Ignoring invalid subagent value in ${filePath}: frontmatter field "subagent" must be true or a non-empty string.`, ), ); return undefined; } return normalized; } export function expandCwdPath(raw: string): string | undefined { const expanded = raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw; return isAbsolute(expanded) ? expanded : undefined; } function normalizeCwd( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): string | undefined { if (value === undefined) return undefined; if (typeof value !== "string") { diagnostics.push( createDiagnostic( "invalid-cwd", filePath, source, `Ignoring invalid cwd in ${filePath}: expected a string.`, ), ); return undefined; } const trimmed = value.trim(); if (!trimmed) return undefined; const expanded = expandCwdPath(trimmed); if (!expanded) { diagnostics.push( createDiagnostic( "invalid-cwd", filePath, source, `Ignoring cwd in ${filePath}: must be an absolute path.`, ), ); return undefined; } return expanded; } function normalizeInheritContext( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true") return true; if (normalized === "false") return false; } diagnostics.push( createDiagnostic( "invalid-inherit-context", filePath, source, `Using default inheritContext=false for ${filePath}: frontmatter field "inheritContext" must be true or false.`, ), ); return false; } function normalizeChain( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): string | undefined { if (value === undefined) return undefined; if (typeof value !== "string") { diagnostics.push( createDiagnostic( "invalid-chain", filePath, source, `Ignoring invalid chain value in ${filePath}: frontmatter field "chain" must be a string.`, ), ); return undefined; } const normalized = value.trim(); if (normalized.length > 0) return normalized; diagnostics.push( createDiagnostic( "empty-chain", filePath, source, `Ignoring invalid chain value in ${filePath}: frontmatter field "chain" must be a non-empty string.`, ), ); return undefined; } function normalizeChainContext( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): "summary" | undefined { if (value === undefined) return undefined; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "summary") return "summary"; } diagnostics.push( createDiagnostic( "invalid-chain-context", filePath, source, `Ignoring invalid chainContext value in ${filePath}: frontmatter field "chainContext" must be "summary".`, ), ); return undefined; } function normalizeThinking( value: unknown, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): ThinkingLevel | undefined { const thinking = normalizeStringField("thinking", value, filePath, source, diagnostics); if (thinking === undefined) return undefined; const normalized = thinking.toLowerCase(); if ((VALID_THINKING_LEVELS as readonly string[]).includes(normalized)) { return normalized as ThinkingLevel; } diagnostics.push( createDiagnostic( "invalid-thinking", filePath, source, `Ignoring invalid thinking level in ${filePath}: ${JSON.stringify(thinking)}.`, ), ); return undefined; } function normalizeThinkingLevels( value: unknown, modelCount: number, filePath: string, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): ThinkingLevel[] | undefined { if (typeof value !== "string") return undefined; const levels = value .split(",") .map((item) => item.trim()) .filter(Boolean); const invalidLevel = levels.find((level) => !(VALID_THINKING_LEVELS as readonly string[]).includes(level.toLowerCase())); if (invalidLevel) { diagnostics.push( createDiagnostic( "invalid-thinking-levels", filePath, source, `Ignoring invalid thinking level in ${filePath}: ${JSON.stringify(invalidLevel)}.`, ), ); return undefined; } if (levels.length !== modelCount) { diagnostics.push( createDiagnostic( "invalid-thinking-level-count", filePath, source, `Ignoring comma-separated thinking levels in ${filePath}: expected ${modelCount} entries to match frontmatter field "model".`, ), ); return undefined; } return levels.map((level) => level.toLowerCase() as ThinkingLevel); } function getPromptRoots(cwd: string): PromptRoot[] { return [ { source: "user", kind: "prompts", dir: join(homedir(), ".pi", "agent", "prompts") }, { source: "user", kind: "prompt-library", dir: join(homedir(), ".pi", "agent", "prompt-library") }, { source: "project", kind: "prompts", dir: resolve(cwd, ".pi", "prompts") }, { source: "project", kind: "prompt-library", dir: resolve(cwd, ".pi", "prompt-library") }, ]; } function isPathInsideOrEqual(path: string, root: string): boolean { const canonicalPath = realpathSync(path); const canonicalRoot = realpathSync(root); const relativePath = relative(canonicalRoot, canonicalPath); return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath)); } function shouldSkipPromptLibraryEntry(entryName: string, rootKind: PromptRootKind): boolean { return rootKind === "prompt-library" && entryName.startsWith("."); } function hasDotPrefixedPathSegment(path: string): boolean { return path.split(/[\\/]+/).some((segment) => segment.startsWith(".")); } function promptLibrarySymlinkTargetHasDotSegment(fullPath: string, promptRoot: string, rootKind: PromptRootKind): boolean { if (rootKind !== "prompt-library") return false; const relativeTarget = relative(realpathSync(promptRoot), realpathSync(fullPath)); return relativeTarget.length > 0 && hasDotPrefixedPathSegment(relativeTarget); } function rejectPromptLibrarySymlinkRoot( dir: string, promptRoot: string, rootKind: PromptRootKind, source: PromptSource, loadCwd: string, diagnostics: PromptLoaderDiagnostic[], ): boolean { if (rootKind !== "prompt-library" || dir !== promptRoot) return false; try { const isSymlink = lstatSync(dir).isSymbolicLink(); const expectedCanonicalRoot = source === "project" ? resolve(realpathSync(loadCwd), ".pi", "prompt-library") : undefined; const canonicalRoot = realpathSync(dir); if (!isSymlink && (expectedCanonicalRoot === undefined || canonicalRoot === expectedCanonicalRoot)) return false; diagnostics.push( createDiagnostic( "symlink-outside-prompt-root", dir, source, expectedCanonicalRoot ? `Skipping prompt-library root at ${dir}: prompt-library roots must resolve to ${expectedCanonicalRoot} and must not be symlinks or symlinked through ancestors.` : `Skipping prompt-library root at ${dir}: prompt-library roots must not be symlinks.`, ), ); return true; } catch { return false; } } function resolvePromptSymlinkEntryKind( fullPath: string, promptRoot: string, rootKind: PromptRootKind, source: PromptSource, diagnostics: PromptLoaderDiagnostic[], ): { isFile: boolean; isDirectory: boolean } { try { if (rootKind === "prompt-library" && !isPathInsideOrEqual(fullPath, promptRoot)) { diagnostics.push( createDiagnostic( "symlink-outside-prompt-root", fullPath, source, `Skipping symlink at ${fullPath}: resolved target is outside prompt root ${promptRoot}.`, ), ); return { isFile: false, isDirectory: false }; } if (promptLibrarySymlinkTargetHasDotSegment(fullPath, promptRoot, rootKind)) { diagnostics.push( createDiagnostic( "dot-prefixed-prompt-library-entry", fullPath, source, `Skipping symlink at ${fullPath}: resolved target uses dot-prefixed files or directories under prompt-library root ${promptRoot}.`, ), ); return { isFile: false, isDirectory: false }; } const stats = statSync(fullPath); return { isFile: stats.isFile(), isDirectory: stats.isDirectory() }; } catch (error) { diagnostics.push( createDiagnostic( "unreadable-symlink", fullPath, source, `Skipping unreadable symlink at ${fullPath}: ${error instanceof Error ? error.message : String(error)}.`, ), ); return { isFile: false, isDirectory: false }; } } const MODEL_CONDITIONAL_DIRECTIVE_PATTERN = /)|)|<\/if-model\s*>|<\/else(?:\s|>)/; function isPromptCapable(input: { chain?: string; hasModelField: boolean; hasExtensionSpecificConfig: boolean; }): boolean { return input.chain !== undefined || input.hasModelField || input.hasExtensionSpecificConfig; } function calculatePromptCapable(input: { frontmatter: Record; body: string; chain?: string; hasExtensionSpecificConfig?: boolean; ignoreBodyIncludes?: boolean; }): boolean { const hasIncludeMetadata = Object.hasOwn(input.frontmatter, "include") || Object.hasOwn(input.frontmatter, "includes"); const hasBodyIncludes = input.ignoreBodyIncludes ? false : hasPromptIncludeDirectives(input.body); const hasSkillConfig = Object.hasOwn(input.frontmatter, "skill") || Object.hasOwn(input.frontmatter, "skills"); const hasModelConditionalDirectives = MODEL_CONDITIONAL_DIRECTIVE_PATTERN.test(input.body); return isPromptCapable({ chain: input.chain, hasModelField: Object.hasOwn(input.frontmatter, "model"), hasExtensionSpecificConfig: input.hasExtensionSpecificConfig === true || hasIncludeMetadata || (input.ignoreBodyIncludes ? false : hasBodyIncludes) || hasSkillConfig || hasModelConditionalDirectives, }); } function hasPromptLibraryCommandMarker(frontmatter: Record): boolean { if (typeof frontmatter.thinking === "string" && (VALID_THINKING_LEVELS as readonly string[]).includes(frontmatter.thinking.trim().toLowerCase())) { return true; } return [ "model", "skill", "skills", "subagent", "parallel", "deterministic", "run", "script", "bestOfN", "worktree", "fresh", "loop", "converge", "boomerang", ].some((key) => Object.hasOwn(frontmatter, key)); } function loadPromptsWithModelFromDir( dir: string, source: PromptSource, rootKind: PromptRootKind, includePlainPrompts: boolean, loadCwd: string, promptRoot = dir, subdir = "", visitedDirectories = new Set(), ): { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] } { const prompts: PromptWithModel[] = []; const diagnostics: PromptLoaderDiagnostic[] = []; if (!existsSync(dir)) { return { prompts, diagnostics }; } if (rejectPromptLibrarySymlinkRoot(dir, promptRoot, rootKind, source, loadCwd, diagnostics)) { return { prompts, diagnostics }; } let canonicalDir: string; try { canonicalDir = realpathSync(dir); } catch (error) { diagnostics.push( createDiagnostic( "unreadable-directory", dir, source, `Skipping prompt directory ${dir}: ${error instanceof Error ? error.message : String(error)}.`, ), ); return { prompts, diagnostics }; } if (visitedDirectories.has(canonicalDir)) { diagnostics.push( createDiagnostic( "directory-cycle", dir, source, `Skipping already visited prompt directory at ${dir}.`, ), ); return { prompts, diagnostics }; } visitedDirectories.add(canonicalDir); try { const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => lexicalCompare(a.name, b.name)); for (const entry of entries) { const fullPath = join(dir, entry.name); if (shouldSkipPromptLibraryEntry(entry.name, rootKind)) continue; let isFile = entry.isFile(); let isDirectory = entry.isDirectory(); if (entry.isSymbolicLink()) { const resolvedKind = resolvePromptSymlinkEntryKind(fullPath, promptRoot, rootKind, source, diagnostics); isFile = resolvedKind.isFile; isDirectory = resolvedKind.isDirectory; if (!isFile && !isDirectory) continue; } if (isDirectory) { const nextSubdir = subdir ? `${subdir}:${entry.name}` : entry.name; const nested = loadPromptsWithModelFromDir(fullPath, source, rootKind, includePlainPrompts, loadCwd, promptRoot, nextSubdir, visitedDirectories); prompts.push(...nested.prompts); diagnostics.push(...nested.diagnostics); continue; } if (!isFile || !entry.name.endsWith(".md")) continue; try { const rawContent = readFileSync(fullPath, "utf-8"); const parsed = parseFrontmatter>(rawContent); if (rootKind === "prompt-library" && !isFrontmatterRecord(parsed.frontmatter)) continue; const frontmatter = normalizeFrontmatterRecord(parsed.frontmatter, fullPath, source, diagnostics); if (!frontmatter) continue; const { body } = parsed; const includesResult = normalizePromptIncludes(frontmatter, fullPath, source, diagnostics); if (!includesResult.ok) continue; const includes = includesResult.includes; const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics); const hasBodyIncludeDirectives = chain ? false : hasPromptIncludeDirectives(body); const hasModelConditionalDirectives = MODEL_CONDITIONAL_DIRECTIVE_PATTERN.test(body); if (rootKind === "prompt-library" && !chain && includes === undefined && !hasBodyIncludeDirectives && !hasModelConditionalDirectives && !hasPromptLibraryCommandMarker(frontmatter)) { continue; } if (chain && includesResult.declaredKey) { diagnostics.push( createDiagnostic( "invalid-includes-chain", fullPath, source, `Skipping prompt template at ${fullPath}: frontmatter field "${includesResult.declaredKey}" cannot be used on chain wrapper templates in v1. Put include/includes on referenced step templates instead.`, ), ); continue; } let parsedChainDeclarationResult: | ReturnType | undefined; const chainContext = chain ? normalizeChainContext(frontmatter.chainContext, fullPath, source, diagnostics) : undefined; if (chain) { parsedChainDeclarationResult = parseChainDeclaration(chain); if (parsedChainDeclarationResult.invalidSegments.length > 0 || parsedChainDeclarationResult.steps.length === 0) { diagnostics.push( createDiagnostic( "invalid-chain-declaration", fullPath, source, `Skipping prompt template at ${fullPath}: invalid chain declaration segment ${JSON.stringify(parsedChainDeclarationResult.invalidSegments[0] ?? chain)}.`, ), ); continue; } } let subagent = normalizeSubagent(frontmatter.subagent, fullPath, source, diagnostics); const cwd = normalizeCwd(frontmatter.cwd, fullPath, source, diagnostics); const inheritContext = normalizeInheritContext(frontmatter.inheritContext, fullPath, source, diagnostics); const parallel = normalizeParallel(frontmatter.parallel, fullPath, source, diagnostics); const hasBestOfN = Object.hasOwn(frontmatter, "bestOfN"); const bestOfN = normalizeBestOfN(frontmatter.bestOfN, fullPath, source, diagnostics); let deterministic = normalizeDeterministic(frontmatter, fullPath, source, diagnostics); const hasLegacyWorkers = Object.hasOwn(frontmatter, "workers"); const hasLegacyReviewers = Object.hasOwn(frontmatter, "reviewers"); const hasLegacyFinalApplier = Object.hasOwn(frontmatter, "finalApplier"); const hasLegacyCompareFields = hasLegacyWorkers || hasLegacyReviewers || hasLegacyFinalApplier; if (hasLegacyWorkers) { pushLegacyCompareFieldDiagnostic("workers", fullPath, source, diagnostics); } if (hasLegacyReviewers) { pushLegacyCompareFieldDiagnostic("reviewers", fullPath, source, diagnostics); } if (hasLegacyFinalApplier) { pushLegacyCompareFieldDiagnostic("finalApplier", fullPath, source, diagnostics); } if (hasBestOfN && Object.hasOwn(frontmatter, "worktree")) { diagnostics.push( createDiagnostic( "invalid-worktree", fullPath, source, `Ignoring top-level worktree in ${fullPath}: use "bestOfN.worktree" for compare template authoring.`, ), ); } const workers = normalizeLineup(hasBestOfN ? bestOfN?.workers : undefined, "workers", fullPath, source, diagnostics); const reviewers = normalizeLineup(hasBestOfN ? bestOfN?.reviewers : undefined, "reviewers", fullPath, source, diagnostics); const finalApplier = normalizeFinalApplier(hasBestOfN ? bestOfN?.finalApplier : undefined, fullPath, source, diagnostics); const preset = normalizeStringField("best-of-n-preset", hasBestOfN ? bestOfN?.preset : undefined, fullPath, source, diagnostics); const commit = normalizeBestOfNCommit(hasBestOfN ? bestOfN?.commit : undefined, fullPath, source, diagnostics); let safeWorkers = workers; let safeReviewers = reviewers; let safeFinalApplier = finalApplier; let safePreset = preset; let safeCommit = commit; if (chain && subagent !== undefined) { diagnostics.push( createDiagnostic( "invalid-subagent-chain", fullPath, source, `Ignoring subagent in ${fullPath}: frontmatter fields "chain" and "subagent" cannot be combined.`, ), ); subagent = undefined; } if (chain && deterministic !== undefined) { diagnostics.push( createDiagnostic( "invalid-deterministic-chain", fullPath, source, `Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "chain".`, ), ); deterministic = undefined; } if (chain && (safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined || safePreset !== undefined || safeCommit !== undefined)) { diagnostics.push( createDiagnostic( "invalid-lineup-chain", fullPath, source, `Ignoring compare lineup config in ${fullPath}: frontmatter fields "workers"/"reviewers"/"finalApplier" cannot be combined with "chain".`, ), ); safeWorkers = undefined; safeReviewers = undefined; safeFinalApplier = undefined; safePreset = undefined; safeCommit = undefined; } if (subagent !== undefined && (safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined || safePreset !== undefined || safeCommit !== undefined)) { diagnostics.push( createDiagnostic( "invalid-lineup-subagent", fullPath, source, `Ignoring compare lineup config in ${fullPath}: frontmatter fields "workers"/"reviewers"/"finalApplier" cannot be combined with "subagent".`, ), ); safeWorkers = undefined; safeReviewers = undefined; safeFinalApplier = undefined; safePreset = undefined; safeCommit = undefined; } if (subagent !== undefined && deterministic !== undefined) { diagnostics.push( createDiagnostic( "invalid-deterministic-subagent", fullPath, source, `Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "subagent".`, ), ); deterministic = undefined; } if (subagent === undefined && inheritContext) { diagnostics.push( createDiagnostic( "invalid-inherit-context", fullPath, source, `Ignoring inheritContext in ${fullPath}: frontmatter field "inheritContext" requires "subagent".`, ), ); } let safeParallel = parallel; if (safeParallel !== undefined && chain) { diagnostics.push( createDiagnostic( "invalid-parallel", fullPath, source, `Ignoring parallel in ${fullPath}: frontmatter field "parallel" cannot be combined with "chain".`, ), ); safeParallel = undefined; } if (safeParallel !== undefined && subagent === undefined) { diagnostics.push( createDiagnostic( "invalid-parallel", fullPath, source, `Ignoring parallel in ${fullPath}: frontmatter field "parallel" requires "subagent".`, ), ); safeParallel = undefined; } if (safeParallel !== undefined && (safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined || safePreset !== undefined || safeCommit !== undefined)) { diagnostics.push( createDiagnostic( "invalid-lineup-parallel", fullPath, source, `Ignoring compare lineup config in ${fullPath}: frontmatter fields "workers"/"reviewers"/"finalApplier" cannot be combined with "parallel".`, ), ); safeWorkers = undefined; safeReviewers = undefined; safeFinalApplier = undefined; safePreset = undefined; safeCommit = undefined; } if (safeParallel !== undefined && deterministic !== undefined) { diagnostics.push( createDiagnostic( "invalid-deterministic-parallel", fullPath, source, `Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "parallel".`, ), ); deterministic = undefined; } const hasLineup = safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined || safePreset !== undefined; if (safeCommit !== undefined && safeFinalApplier === undefined) { diagnostics.push( createDiagnostic( "invalid-best-of-n-commit", fullPath, source, `Ignoring bestOfN.commit in ${fullPath}: commit ask mode requires bestOfN.finalApplier.`, ), ); safeCommit = undefined; } if (!hasBestOfN && hasLegacyCompareFields) { diagnostics.push( createDiagnostic( "invalid-compare-frontmatter", fullPath, source, `Skipping prompt template at ${fullPath}: compare template authoring moved under "bestOfN:".`, ), ); continue; } if (hasBestOfN && !hasLineup) { diagnostics.push( createDiagnostic( "invalid-best-of-n", fullPath, source, `Skipping prompt template at ${fullPath}: "bestOfN" did not produce a valid compare configuration.`, ), ); continue; } if (!chain && subagent === undefined && !hasLineup && cwd) { if (deterministic) { deterministic = { ...deterministic, ...(deterministic.cwd ? {} : { cwd }) }; } else { diagnostics.push( createDiagnostic( "invalid-cwd", fullPath, source, `Ignoring cwd in ${fullPath}: frontmatter field "cwd" requires "subagent", "chain", or compare lineups ("workers"/"reviewers"/"finalApplier").`, ), ); } } const hasModelField = Object.hasOwn(frontmatter, "model"); const parsedModels = chain ? [] : normalizeModelSpecs(frontmatter.model, fullPath, source, diagnostics); if (!chain && hasModelField && !parsedModels) continue; const models = chain ? [] : (parsedModels ?? []); const rotate = chain ? false : normalizeRotate(frontmatter.rotate, fullPath, source, diagnostics); const hidden = normalizeHidden(frontmatter.hidden, fullPath, source, diagnostics); const name = entry.name.slice(0, -3); if (RESERVED_COMMAND_NAMES.has(name)) { diagnostics.push( createDiagnostic( "reserved-command-name", fullPath, source, `Skipping prompt template at ${fullPath}: command name "${name}" is reserved.`, ), ); continue; } const safeInheritContext = subagent !== undefined && inheritContext; const safeCwd = (chain || subagent !== undefined || hasLineup) ? cwd : undefined; const description = normalizeStringField("description", frontmatter.description, fullPath, source, diagnostics) ?? ""; if (hasLineup && (Object.hasOwn(frontmatter, "skill") || Object.hasOwn(frontmatter, "skills"))) { diagnostics.push( createDiagnostic( "invalid-compare-skills", fullPath, source, `Skipping prompt template at ${fullPath}: compare prompts cannot be combined with "skill" or "skills" in v1.`, ), ); continue; } const skillResult = chain ? { ok: true as const } : normalizePromptSkills(frontmatter, fullPath, source, diagnostics); if (!skillResult.ok) continue; const skill = skillResult.skill; const skills = skillResult.skills; let thinking: ThinkingLevel | undefined; let thinkingLevels: ThinkingLevel[] | undefined; if (!chain) { if (rotate && typeof frontmatter.thinking === "string" && frontmatter.thinking.includes(",")) { thinkingLevels = normalizeThinkingLevels(frontmatter.thinking, models.length, fullPath, source, diagnostics); } else { thinking = normalizeThinking(frontmatter.thinking, fullPath, source, diagnostics); } } const restore = normalizeRestore(frontmatter.restore, fullPath, source, diagnostics); const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics); const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics); const converge = normalizeConverge(frontmatter.converge, fullPath, source, diagnostics); let boomerang = normalizeBoomerang(frontmatter.boomerang, fullPath, source, diagnostics); if (chain && boomerang) { diagnostics.push( createDiagnostic( "invalid-boomerang-chain", fullPath, source, `Ignoring boomerang in ${fullPath}: frontmatter fields "chain" and "boomerang" cannot be combined.`, ), ); boomerang = false; } if (loop !== undefined && deterministic !== undefined) { diagnostics.push( createDiagnostic( "invalid-deterministic-loop", fullPath, source, `Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "loop" in v1.`, ), ); deterministic = undefined; } const worktreeInput = hasBestOfN ? bestOfN?.worktree : frontmatter.worktree; const worktree = normalizeWorktree(worktreeInput, fullPath, source, diagnostics); let safeWorktree: boolean | undefined; if (worktree) { if (chain) { const parsedChain = parsedChainDeclarationResult ?? parseChainDeclaration(chain); const hasParallelStep = parsedChain.steps.some((step) => "parallel" in step); if (parsedChain.invalidSegments.length > 0 || parsedChain.steps.length === 0 || !hasParallelStep) { diagnostics.push( createDiagnostic( "invalid-worktree", fullPath, source, `Ignoring worktree in ${fullPath}: frontmatter field "worktree" requires either "chain" with at least one parallel() step, "subagent" with frontmatter field "parallel", or compare lineups ("workers"/"reviewers"/"finalApplier").`, ), ); } else { safeWorktree = true; } } else if (subagent !== undefined && safeParallel !== undefined) { safeWorktree = true; } else if (hasLineup) { safeWorktree = true; } else { diagnostics.push( createDiagnostic( "invalid-worktree", fullPath, source, `Ignoring worktree in ${fullPath}: frontmatter field "worktree" requires either "chain" with at least one parallel() step, "subagent" with frontmatter field "parallel", or compare lineups ("workers"/"reviewers"/"finalApplier").`, ), ); } } let content = body; let includeGraph: PromptIncludeGraph | undefined; const includeConfigIsCommandCapable = includes !== undefined; const hasExtensionSpecificConfig = skills !== undefined || thinking !== undefined || fresh === true || loop !== undefined || converge === false || boomerang === true || includeConfigIsCommandCapable || safeParallel !== undefined || deterministic !== undefined || hasLineup || safeWorktree === true || subagent !== undefined || safeInheritContext; const promptCapable = calculatePromptCapable({ frontmatter, body: content, chain, hasExtensionSpecificConfig, ignoreBodyIncludes: rootKind === "prompt-library" && includes === undefined, }); const shouldRenderIncludes = !chain && (includes !== undefined || (hasBodyIncludeDirectives && promptCapable)); if (shouldRenderIncludes) { const renderedIncludes = renderPromptIncludes({ promptName: name, content: body, includes, promptFilePath: fullPath, promptRoot, cwd: loadCwd, source, rootKind, }); if (!renderedIncludes.ok) { diagnostics.push(...renderedIncludes.diagnostics); continue; } content = renderedIncludes.content; includeGraph = renderedIncludes.includeGraph; } if (!promptCapable && (rootKind === "prompt-library" || !includePlainPrompts)) { continue; } prompts.push({ name, description, content, models, hidden: hidden || undefined, ...(includes !== undefined ? { includes } : {}), chain: chain || undefined, chainContext, restore, skill, ...(skills !== undefined ? { skills } : {}), thinking, thinkingLevels, rotate: rotate || undefined, fresh: fresh || undefined, loop: loop !== undefined ? loop : undefined, converge: converge === false ? false : undefined, boomerang: boomerang || undefined, parallel: safeParallel, worktree: safeWorktree, deterministic, subagent, inheritContext: safeInheritContext || undefined, cwd: safeCwd || undefined, workers: safeWorkers, reviewers: safeReviewers, finalApplier: safeFinalApplier, commit: safeCommit, preset: safePreset, source, rootKind, subdir: subdir || undefined, filePath: fullPath, includeGraph, }); } catch (error) { diagnostics.push( createDiagnostic( "invalid-prompt-file", fullPath, source, `Skipping prompt template at ${fullPath}: ${error instanceof Error ? error.message : String(error)}.`, ), ); } } } catch (error) { diagnostics.push( createDiagnostic( "unreadable-directory", dir, source, `Skipping prompt directory ${dir}: ${error instanceof Error ? error.message : String(error)}.`, ), ); } return { prompts, diagnostics }; } function collectPromptSourceRecordsFromDir( dir: string, source: PromptSource, rootKind: PromptRootKind, includePlainPrompts: boolean, loadCwd: string, promptRoot = dir, subdir = "", visitedDirectories = new Set(), ): { records: PromptSourceRecord[]; diagnostics: PromptLoaderDiagnostic[] } { const records: PromptSourceRecord[] = []; const diagnostics: PromptLoaderDiagnostic[] = []; if (!existsSync(dir)) { return { records, diagnostics }; } if (rejectPromptLibrarySymlinkRoot(dir, promptRoot, rootKind, source, loadCwd, diagnostics)) { return { records, diagnostics }; } let canonicalDir: string; try { canonicalDir = realpathSync(dir); } catch (error) { diagnostics.push( createDiagnostic( "unreadable-directory", dir, source, `Skipping prompt directory ${dir}: ${error instanceof Error ? error.message : String(error)}.`, ), ); return { records, diagnostics }; } if (visitedDirectories.has(canonicalDir)) { diagnostics.push( createDiagnostic( "directory-cycle", dir, source, `Skipping already visited prompt directory at ${dir}.`, ), ); return { records, diagnostics }; } visitedDirectories.add(canonicalDir); try { const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => lexicalCompare(a.name, b.name)); for (const entry of entries) { const fullPath = join(dir, entry.name); if (shouldSkipPromptLibraryEntry(entry.name, rootKind)) continue; let isFile = entry.isFile(); let isDirectory = entry.isDirectory(); if (entry.isSymbolicLink()) { const resolvedKind = resolvePromptSymlinkEntryKind(fullPath, promptRoot, rootKind, source, diagnostics); isFile = resolvedKind.isFile; isDirectory = resolvedKind.isDirectory; if (!isFile && !isDirectory) continue; } if (isDirectory) { const nextSubdir = subdir ? `${subdir}:${entry.name}` : entry.name; const nested = collectPromptSourceRecordsFromDir(fullPath, source, rootKind, includePlainPrompts, loadCwd, promptRoot, nextSubdir, visitedDirectories); records.push(...nested.records); diagnostics.push(...nested.diagnostics); continue; } if (!isFile || !entry.name.endsWith(".md")) continue; try { const rawContent = readFileSync(fullPath, "utf-8"); const parsed = parseFrontmatter>(rawContent); const promptName = entry.name.slice(0, -3); if (rootKind === "prompt-library" && !isFrontmatterRecord(parsed.frontmatter)) { if (!includePlainPrompts) continue; records.push({ promptName, filePath: fullPath, promptRoot, cwd: loadCwd, source, rootKind, promptCapable: false, rawBody: parsed.body, hasInlineIncludes: false, hasIncludesPlaceholder: false, isChainWrapper: false, skippedReason: "invalid-frontmatter", }); continue; } const frontmatter = normalizeFrontmatterRecord(parsed.frontmatter, fullPath, source, diagnostics); if (!frontmatter) continue; if (RESERVED_COMMAND_NAMES.has(promptName)) { const hidden = normalizeHidden(frontmatter.hidden, fullPath, source, diagnostics); const rawChain = typeof frontmatter.chain === "string" && frontmatter.chain.trim() ? frontmatter.chain.trim() : undefined; const hasIncludeMetadata = Object.hasOwn(frontmatter, "include") || Object.hasOwn(frontmatter, "includes"); const promptCapable = calculatePromptCapable({ frontmatter, body: parsed.body, chain: rawChain, ignoreBodyIncludes: rawChain !== undefined || (rootKind === "prompt-library" && !hasIncludeMetadata), }); records.push({ promptName, filePath: fullPath, promptRoot, cwd: loadCwd, source, rootKind, promptCapable, rawBody: parsed.body, hasInlineIncludes: rawChain === undefined && extractPromptInlineIncludes(parsed.body).length > 0, hasIncludesPlaceholder: rawChain === undefined && hasPromptIncludesPlaceholder(parsed.body), isChainWrapper: rawChain !== undefined, hidden: hidden || undefined, skippedReason: "reserved-command-name", }); continue; } const includesDiagnosticStart = diagnostics.length; const includesResult = normalizePromptIncludes(frontmatter, fullPath, source, diagnostics); const includes = includesResult.ok ? includesResult.includes : undefined; const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics); const isChainWrapper = chain !== undefined; const hasModelConditionalDirectives = !isChainWrapper && MODEL_CONDITIONAL_DIRECTIVE_PATTERN.test(parsed.body); if (rootKind === "prompt-library" && !isChainWrapper && includesResult.ok && includes === undefined && !hasPromptIncludeDirectives(parsed.body) && !hasModelConditionalDirectives && !hasPromptLibraryCommandMarker(frontmatter)) { if (!includePlainPrompts) continue; const hidden = frontmatter.hidden === true || (typeof frontmatter.hidden === "string" && frontmatter.hidden.trim().toLowerCase() === "true"); records.push({ promptName, filePath: fullPath, promptRoot, cwd: loadCwd, source, rootKind, promptCapable: false, rawBody: parsed.body, hasInlineIncludes: false, hasIncludesPlaceholder: false, isChainWrapper: false, hidden: hidden || undefined, }); continue; } const includeMetadataInvalid = !includesResult.ok || (isChainWrapper && includesResult.ok && includesResult.declaredKey !== undefined); const skippedReason = !includesResult.ok ? diagnostics[includesDiagnosticStart]?.code : includeMetadataInvalid ? "invalid-includes-chain" : undefined; const hasInlineIncludes = isChainWrapper ? false : extractPromptInlineIncludes(parsed.body).length > 0; const hasIncludesPlaceholder = isChainWrapper ? false : hasPromptIncludesPlaceholder(parsed.body); const hidden = normalizeHidden(frontmatter.hidden, fullPath, source, diagnostics); const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics); const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics); const converge = normalizeConverge(frontmatter.converge, fullPath, source, diagnostics); const boomerang = normalizeBoomerang(frontmatter.boomerang, fullPath, source, diagnostics); const thinking = isChainWrapper ? undefined : normalizeThinking(frontmatter.thinking, fullPath, source, diagnostics); const bodyIncludesAreCommandCapable = !(rootKind === "prompt-library" && includes === undefined); const hasSourceGraphFeature = isChainWrapper || includes !== undefined || (bodyIncludesAreCommandCapable && (hasInlineIncludes || hasIncludesPlaceholder)) || fresh === true || loop !== undefined || converge === false || boomerang === true || thinking !== undefined || Object.hasOwn(frontmatter, "subagent") || Object.hasOwn(frontmatter, "parallel") || Object.hasOwn(frontmatter, "deterministic") || Object.hasOwn(frontmatter, "run") || Object.hasOwn(frontmatter, "script") || Object.hasOwn(frontmatter, "bestOfN") || Object.hasOwn(frontmatter, "worktree"); const promptCapable = calculatePromptCapable({ frontmatter, body: parsed.body, chain, hasExtensionSpecificConfig: hasSourceGraphFeature, ignoreBodyIncludes: isChainWrapper || !bodyIncludesAreCommandCapable, }); if (!promptCapable && !includePlainPrompts) continue; records.push({ promptName, filePath: fullPath, promptRoot, cwd: loadCwd, source, rootKind, promptCapable, rawBody: parsed.body, ...(includes !== undefined ? { includes } : {}), hasInlineIncludes, hasIncludesPlaceholder, isChainWrapper, hidden: hidden || undefined, ...(includeMetadataInvalid ? { includeMetadataInvalid: true, skippedReason } : {}), }); } catch (error) { diagnostics.push( createDiagnostic( "invalid-prompt-file", fullPath, source, `Skipping prompt template at ${fullPath}: ${error instanceof Error ? error.message : String(error)}.`, ), ); } } } catch (error) { diagnostics.push( createDiagnostic( "unreadable-directory", dir, source, `Skipping prompt directory ${dir}: ${error instanceof Error ? error.message : String(error)}.`, ), ); } return { records, diagnostics }; } function dedupeDiagnostics(diagnostics: PromptLoaderDiagnostic[]): PromptLoaderDiagnostic[] { const seen = new Set(); const deduped: PromptLoaderDiagnostic[] = []; for (const diagnostic of diagnostics) { if (seen.has(diagnostic.key)) continue; seen.add(diagnostic.key); deduped.push(diagnostic); } return deduped; } function isIncludeGraphRelevantSkippedRecord(record: PromptSourceRecord): boolean { return record.includes !== undefined || record.hasInlineIncludes || record.hasIncludesPlaceholder || record.includeMetadataInvalid === true; } export function collectPromptSourceRecords(cwd: string, includePlainPrompts = true): CollectPromptSourceRecordsResult { const recordMap = new Map(); const inventoryRecords: PromptSourceRecord[] = []; const diagnostics: PromptLoaderDiagnostic[] = []; const loaderResult = loadPromptsWithModel(cwd, includePlainPrompts); const effectivePromptPaths = new Set([...loaderResult.prompts.values()].map((prompt) => prompt.filePath)); function replaceRecord(bucket: PromptSourceRecord[], existing: PromptSourceRecord, record: PromptSourceRecord): PromptSourceRecord[] { return bucket.map((item) => (item === existing ? record : item)); } function addRecord(record: PromptSourceRecord) { const recordIsEffective = effectivePromptPaths.has(record.filePath); if (!recordIsEffective && !isIncludeGraphRelevantSkippedRecord(record) && record.rootKind !== "prompt-library") { return; } const existingBucket = recordMap.get(record.promptName); if (!existingBucket) { recordMap.set(record.promptName, [record]); return; } const sameSourceExisting = existingBucket.find((existing) => existing.source === record.source); if (sameSourceExisting) { const existingIsEffective = effectivePromptPaths.has(sameSourceExisting.filePath); if (!existingIsEffective && recordIsEffective) { recordMap.set(record.promptName, replaceRecord(existingBucket, sameSourceExisting, record)); return; } if (!existingIsEffective || !recordIsEffective) { return; } diagnostics.push( createDiagnostic( "duplicate-command-name", record.filePath, record.source, `Skipping ${record.source} prompt template "${record.promptName}" at ${record.filePath} because it conflicts with ${sameSourceExisting.filePath}.`, ), ); return; } if (!recordIsEffective) { recordMap.set(record.promptName, [...existingBucket, record]); return; } recordMap.set( record.promptName, [ ...existingBucket.filter((existing) => !effectivePromptPaths.has(existing.filePath) && isIncludeGraphRelevantSkippedRecord(existing)), record, ], ); } for (const root of getPromptRoots(cwd)) { const rootResult = collectPromptSourceRecordsFromDir(root.dir, root.source, root.kind, includePlainPrompts, cwd, root.dir); inventoryRecords.push(...rootResult.records); diagnostics.push(...rootResult.diagnostics); for (const record of rootResult.records) addRecord(record); } return { records: [...recordMap.values()].flat(), inventoryRecords, diagnostics: dedupeDiagnostics([...diagnostics, ...loaderResult.diagnostics]) }; } export function loadPromptsWithModel(cwd: string, includePlainPrompts = false): LoadPromptsWithModelResult { const promptMap = new Map(); const diagnostics: PromptLoaderDiagnostic[] = []; function addPrompt(prompt: PromptWithModel) { const existing = promptMap.get(prompt.name); if (!existing) { promptMap.set(prompt.name, prompt); return; } if (existing.source === prompt.source) { diagnostics.push( createDiagnostic( "duplicate-command-name", prompt.filePath, prompt.source, `Skipping ${prompt.source} prompt template "${prompt.name}" at ${prompt.filePath} because it conflicts with ${existing.filePath}.`, ), ); return; } promptMap.set(prompt.name, prompt); } for (const root of getPromptRoots(cwd)) { const rootResult = loadPromptsWithModelFromDir(root.dir, root.source, root.kind, includePlainPrompts, cwd, root.dir); diagnostics.push(...rootResult.diagnostics); for (const prompt of rootResult.prompts) { addPrompt(prompt); } } return { prompts: promptMap, diagnostics }; } function effectiveLineupCount(slots: DelegationLineupSlot[] | undefined): number { return slots?.reduce((total, slot) => total + (slot.count ?? 1), 0) ?? 0; } export function formatPromptSourceLabel(prompt: Pick): string { const rootLabel = prompt.rootKind === "prompt-library" ? `${prompt.source} library` : prompt.source; return prompt.subdir ? `${rootLabel}:${prompt.subdir}` : rootLabel; } export function buildPromptCommandDescription(prompt: PromptWithModel): string { const sourceLabel = `(${formatPromptSourceLabel(prompt)})`; if (prompt.chain) { const chainContextLabel = prompt.chainContext ? ` ${prompt.chainContext}` : ""; const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : ""; const worktreeLabel = prompt.worktree ? " worktree" : ""; const details = `[chain: ${prompt.chain}${chainContextLabel}${cwdLabel}${worktreeLabel}] ${sourceLabel}`; return prompt.description ? `${prompt.description} ${details}` : details; } const modelLabel = prompt.models.length > 0 ? prompt.models.map((model) => model.split("/").pop() || model).join("|") : "current"; const rotateLabel = prompt.rotate ? " rotate" : ""; const skillLabel = prompt.skills && prompt.skills.length > 0 ? ` +${prompt.skills.join(",+")}` : prompt.skill ? ` +${prompt.skill}` : ""; const thinkingValue = prompt.thinkingLevels ? prompt.thinkingLevels.join(",") : prompt.thinking; const thinkingLabel = thinkingValue ? ` ${thinkingValue}` : ""; const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : ""; const boomerangLabel = prompt.boomerang ? " boomerang" : ""; const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : ""; const parallelLabel = prompt.parallel !== undefined ? ` parallel:${prompt.parallel}` : ""; const deterministicLabel = prompt.deterministic ? ` deterministic-step:${prompt.deterministic.handoff}` : ""; const workersLabel = prompt.workers ? ` workers:${effectiveLineupCount(prompt.workers)}` : ""; const reviewersLabel = prompt.reviewers ? ` reviewers:${effectiveLineupCount(prompt.reviewers)}` : ""; const finalApplierLabel = prompt.finalApplier ? " final-applier" : ""; const commitLabel = prompt.commit ? ` commit:${prompt.commit}` : ""; const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : ""; const inheritContextLabel = prompt.inheritContext ? " fork" : ""; const worktreeLabel = prompt.worktree ? " worktree" : ""; const details = `[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${boomerangLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${commitLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`; return prompt.description ? `${prompt.description} ${details}` : details; } function getSkillCandidates(baseDir: string, skillName: string): string[] { return [join(baseDir, skillName, "SKILL.md"), join(baseDir, `${skillName}.md`)]; } function* walkAncestors(startDir: string, stopDir?: string): Generator { let current = startDir; while (true) { yield current; if (stopDir && current === stopDir) return; const parent = dirname(current); if (parent === current) return; current = parent; } } function findRepoRoot(startDir: string): string | undefined { for (const dir of walkAncestors(startDir)) { if (existsSync(join(dir, ".git"))) return dir; } return undefined; } function findFirstExisting(paths: string[]): string | undefined { for (const path of paths) { if (existsSync(path)) return path; } return undefined; } export function resolveSkillPath(skillName: string, cwd: string): string | undefined { const projectDir = resolve(cwd); const projectPiSkill = findFirstExisting(getSkillCandidates(resolve(projectDir, ".pi", "skills"), skillName)); if (projectPiSkill) return projectPiSkill; const repoRoot = findRepoRoot(projectDir); for (const dir of walkAncestors(projectDir, repoRoot)) { const projectAgentsSkill = findFirstExisting(getSkillCandidates(join(dir, ".agents", "skills"), skillName)); if (projectAgentsSkill) return projectAgentsSkill; } const globalPiSkill = findFirstExisting(getSkillCandidates(join(homedir(), ".pi", "agent", "skills"), skillName)); if (globalPiSkill) return globalPiSkill; return findFirstExisting(getSkillCandidates(join(homedir(), ".agents", "skills"), skillName)); } export interface DiscoveredSkill { skillName: string; skillPath: string; } function getSkillDiscoveryRoots(cwd: string): string[] { const projectDir = resolve(cwd); const roots: string[] = [resolve(projectDir, ".pi", "skills")]; const repoRoot = findRepoRoot(projectDir); for (const dir of walkAncestors(projectDir, repoRoot)) { roots.push(join(dir, ".agents", "skills")); } roots.push(join(homedir(), ".pi", "agent", "skills")); roots.push(join(homedir(), ".agents", "skills")); return roots; } function isValidDiscoveredSkillName(skillName: string): boolean { return VALID_EXACT_SKILL_NAME.test(skillName); } function isReadableParseableSkillFile(skillPath: string): boolean { try { const skillStats = lstatSync(skillPath); if (!skillStats.isFile()) return false; parseFrontmatter(readFileSync(skillPath, "utf-8")); return true; } catch { return false; } } function discoverSkillsInRoot(root: string): DiscoveredSkill[] { try { const entries = readdirSync(root, { withFileTypes: true }); const discovered = new Map(); for (const entry of entries) { if (entry.name.startsWith(".")) continue; if (entry.isSymbolicLink()) continue; const entryPath = join(root, entry.name); if (entry.isDirectory()) { const skillName = entry.name; if (!isValidDiscoveredSkillName(skillName)) continue; const skillPath = join(entryPath, "SKILL.md"); if (!isReadableParseableSkillFile(skillPath)) continue; discovered.set(skillName, { skillPath, priority: 0 }); continue; } if (!entry.isFile()) continue; if (!entry.name.endsWith(".md")) continue; const skillName = entry.name.slice(0, -3); if (skillName.startsWith(".")) continue; if (!isValidDiscoveredSkillName(skillName)) continue; const existing = discovered.get(skillName); if (existing && existing.priority <= 1) continue; if (!isReadableParseableSkillFile(entryPath)) continue; discovered.set(skillName, { skillPath: entryPath, priority: 1 }); } return Array.from(discovered, ([skillName, value]) => ({ skillName, skillPath: value.skillPath })) .sort((a, b) => lexicalCompare(a.skillName, b.skillName)); } catch { return []; } } export function discoverFilesystemSkills(cwd: string): DiscoveredSkill[] { return getSkillDiscoveryRoots(cwd).flatMap((root) => discoverSkillsInRoot(root)); } export function readSkillContent(skillPath: string): string { const raw = readFileSync(skillPath, "utf-8"); return parseFrontmatter(raw).body; }