promptopskit 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Provider adapters for OpenAI, Anthropic, Gemini, and OpenRouter produce a ready-
15
15
  - **Centralized, not scattered** — each prompt is a single Markdown file that captures prompt text, model config, tool bindings, and context rules together.
16
16
  - **Operational, not just templated** — model name, temperature, reasoning effort, tools, and response format are declared alongside the prompt they govern.
17
17
  - **Reusable, not duplicated** — `includes` lets you define shared tone, policy, or safety instructions once and compose them into any prompt.
18
- - **Layered defaults, not repetition** — `defaults.md` in any folder sets shared `metadata` and `# System instructions` for that subtree, with nearest-folder override behavior.
18
+ - **Layered defaults, not repetition** — `defaults.md` in any folder sets shared `provider`, `model`, `metadata`, and `# System instructions` for that subtree, with nearest-folder override behavior.
19
19
  - **Release-aware, not ad hoc** — environment and tier overrides swap models and parameters without forking prompt files.
20
20
  - **Provider-portable** — write once, render for OpenAI, Anthropic, Gemini, or OpenRouter with correct body shapes.
21
21
  - **Validate early** — Zod schema validation, Levenshtein-based "did you mean?" suggestions for typos, and variable usage checks catch mistakes before runtime.
@@ -40,6 +40,7 @@ This creates:
40
40
 
41
41
  ```
42
42
  prompts/
43
+ ├── defaults.md # Folder-level defaults (provider, model, metadata, system instructions)
43
44
  ├── hello.md # Sample prompt with variables
44
45
  ├── hello.test.yaml # Test sidecar with sample inputs
45
46
  └── shared/
@@ -50,7 +51,7 @@ prompts/
50
51
 
51
52
  ```markdown
52
53
  ---
53
- id: support.reply
54
+ id: support/reply
54
55
  schema_version: 1
55
56
  provider: openai
56
57
  model: gpt-5.4
@@ -107,7 +108,7 @@ const response = await fetch('https://api.openai.com/v1/chat/completions', {
107
108
  - **Prompts as Markdown** — YAML front matter for settings, H1 headings for sections (`# System instructions`, `# Prompt template`, `# Notes`)
108
109
  - **Variable interpolation** — `{{ variable }}` syntax with strict and permissive modes
109
110
  - **Composition** — `includes` to share system instructions across prompts, with circular detection
110
- - **Folder defaults** — `defaults.md` inheritance for shared metadata and system instructions
111
+ - **Folder defaults** — `defaults.md` inheritance for shared provider, model, metadata, and system instructions
111
112
  - **Overrides** — Environment and tier-based overrides (base → env → tier → runtime)
112
113
  - **4 provider adapters** — OpenAI, Anthropic, Gemini, OpenRouter — body-only output
113
114
  - **Validation** — Zod schema validation, Levenshtein-based "did you mean?" for typos, variable usage checks
@@ -169,7 +170,7 @@ Define environment and tier overrides in front matter. Precedence: **base → en
169
170
 
170
171
  ```markdown
171
172
  ---
172
- id: support.reply
173
+ id: support/reply
173
174
  schema_version: 1
174
175
  provider: openai
175
176
  model: gpt-5.4
@@ -206,7 +207,7 @@ Share system instructions across prompts using `includes`. Included system instr
206
207
 
207
208
  ```markdown
208
209
  ---
209
- id: support.reply
210
+ id: support/reply
210
211
  schema_version: 1
211
212
  includes:
212
213
  - ./shared/tone.md
@@ -221,6 +222,7 @@ Handle support requests carefully.
221
222
 
222
223
  Define a `defaults.md` file in `prompts/` (and optional subfolders) to provide inherited defaults for prompts:
223
224
 
225
+ - Shared `provider` and `model` in front matter
224
226
  - Shared `metadata` defaults in front matter
225
227
  - Shared `# System instructions` in body
226
228
  - Nearest subfolder `defaults.md` overrides parent defaults
@@ -63,6 +63,8 @@ var SectionsSchema = z.object({
63
63
  notes: z.string().optional()
64
64
  });
65
65
  var PromptDefaultsSchema = z.object({
66
+ provider: z.enum(["openai", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
67
+ model: z.string().optional(),
66
68
  metadata: MetadataSchema.optional(),
67
69
  sections: z.object({
68
70
  system_instructions: z.string().optional()
@@ -97,7 +99,7 @@ var SECTION_MAP = {
97
99
  "notes": "notes"
98
100
  };
99
101
  function extractSections(body) {
100
- const lines = body.split("\n");
102
+ const lines = body.split(/\r?\n/);
101
103
  const sections = {};
102
104
  let currentKey = null;
103
105
  let currentLines = [];
@@ -206,6 +208,8 @@ function parseDefaults(content) {
206
208
  }
207
209
  function mergeDefaults(base, local) {
208
210
  return {
211
+ provider: local.provider ?? base.provider,
212
+ model: local.model ?? base.model,
209
213
  metadata: {
210
214
  ...base.metadata ?? {},
211
215
  ...local.metadata ?? {}
@@ -219,7 +223,8 @@ function mergeDefaults(base, local) {
219
223
  function applyDefaults(asset, defaults) {
220
224
  const hasDefaultMetadata = defaults.metadata && Object.keys(defaults.metadata).length > 0;
221
225
  const hasDefaultSystem = !!defaults.sections?.system_instructions;
222
- if (!hasDefaultMetadata && !hasDefaultSystem) {
226
+ const hasDefaultScalars = defaults.provider !== void 0 || defaults.model !== void 0;
227
+ if (!hasDefaultMetadata && !hasDefaultSystem && !hasDefaultScalars) {
223
228
  return asset;
224
229
  }
225
230
  const mergedMetadata = {
@@ -231,6 +236,8 @@ function applyDefaults(asset, defaults) {
231
236
  const sections = asset.sections ? { ...asset.sections, system_instructions: systemInstructions } : systemInstructions ? { system_instructions: systemInstructions } : void 0;
232
237
  return {
233
238
  ...asset,
239
+ provider: asset.provider ?? defaults.provider,
240
+ model: asset.model ?? defaults.model,
234
241
  metadata,
235
242
  sections
236
243
  };
@@ -243,4 +250,4 @@ export {
243
250
  parsePrompt,
244
251
  loadPromptFile
245
252
  };
246
- //# sourceMappingURL=chunk-QDGJFINW.js.map
253
+ //# sourceMappingURL=chunk-CX6KVBW4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/schema.ts","../src/parser/sections.ts","../src/parser/parser.ts","../src/parser/loader.ts"],"sourcesContent":["import { z } from 'zod';\n\n// --- Tool definitions ---\n\nexport const InlineToolDefSchema = z.object({\n name: z.string(),\n description: z.string().optional(),\n input_schema: z.record(z.unknown()).optional(),\n});\n\nexport type InlineToolDef = z.infer<typeof InlineToolDefSchema>;\n\nexport const ToolRefSchema = z.union([z.string(), InlineToolDefSchema]);\n\n// --- MCP ---\n\nexport const MCPServerRefSchema = z.union([\n z.string(),\n z.object({\n name: z.string(),\n config: z.record(z.unknown()).optional(),\n }),\n]);\n\nexport type MCPServerRef = z.infer<typeof MCPServerRefSchema>;\n\n// --- Reasoning ---\n\nexport const ReasoningSchema = z.object({\n effort: z.enum(['low', 'medium', 'high']).optional(),\n budget_tokens: z.number().int().positive().optional(),\n});\n\n// --- Sampling ---\n\nexport const SamplingSchema = z.object({\n temperature: z.number().min(0).max(2).optional(),\n top_p: z.number().min(0).max(1).optional(),\n frequency_penalty: z.number().optional(),\n presence_penalty: z.number().optional(),\n stop: z.array(z.string()).optional(),\n max_output_tokens: z.number().int().positive().optional(),\n});\n\n// --- Response ---\n\nexport const ResponseSchema = z.object({\n format: z.enum(['text', 'json', 'markdown']).optional(),\n stream: z.boolean().optional(),\n});\n\n// --- Context ---\n\nexport const HistorySchema = z.object({\n max_items: z.number().int().positive().optional(),\n});\n\nexport const ContextSchema = z.object({\n inputs: z.array(z.string()).optional(),\n history: HistorySchema.optional(),\n});\n\n// --- Metadata ---\n\nexport const MetadataSchema = z.object({\n owner: z.string().optional(),\n tags: z.array(z.string()).optional(),\n review_required: z.boolean().optional(),\n stable: z.boolean().optional(),\n});\n\n// --- MCP block ---\n\nexport const MCPSchema = z.object({\n servers: z.array(MCPServerRefSchema).optional(),\n});\n\n// --- Overrides (subset allowed in environments/tiers) ---\n\nexport const PromptAssetOverridesSchema = z.object({\n model: z.string().optional(),\n fallback_models: z.array(z.string()).optional(),\n reasoning: ReasoningSchema.optional(),\n sampling: SamplingSchema.optional(),\n response: ResponseSchema.optional(),\n tools: z.array(ToolRefSchema).optional(),\n});\n\nexport type PromptAssetOverrides = z.infer<typeof PromptAssetOverridesSchema>;\n\n// --- Source tracking ---\n\nexport const SourceSchema = z.object({\n file_path: z.string().optional(),\n checksum: z.string().optional(),\n});\n\n// --- Sections (populated by parser) ---\n\nexport const SectionsSchema = z.object({\n system_instructions: z.string().optional(),\n prompt_template: z.string().optional(),\n notes: z.string().optional(),\n});\n\n// --- Defaults files (folder-level inheritance) ---\n\nexport const PromptDefaultsSchema = z.object({\n provider: z.enum(['openai', 'anthropic', 'google', 'gemini', 'openrouter', 'any']).optional(),\n model: z.string().optional(),\n metadata: MetadataSchema.optional(),\n sections: z.object({\n system_instructions: z.string().optional(),\n }).optional(),\n});\n\nexport type PromptDefaults = z.infer<typeof PromptDefaultsSchema>;\n\n// --- Top-level PromptAsset ---\n\nexport const PromptAssetSchema = z.object({\n id: z.string(),\n schema_version: z.number().int().positive().default(1),\n description: z.string().optional(),\n\n provider: z.enum(['openai', 'anthropic', 'google', 'gemini', 'openrouter', 'any']).optional(),\n model: z.string().optional(),\n fallback_models: z.array(z.string()).optional(),\n\n reasoning: ReasoningSchema.optional(),\n sampling: SamplingSchema.optional(),\n response: ResponseSchema.optional(),\n\n tools: z.array(ToolRefSchema).optional(),\n mcp: MCPSchema.optional(),\n\n context: ContextSchema.optional(),\n\n includes: z.array(z.string()).optional(),\n\n environments: z.record(PromptAssetOverridesSchema).optional(),\n tiers: z.record(PromptAssetOverridesSchema).optional(),\n\n metadata: MetadataSchema.optional(),\n\n // Populated by parser, not authored in YAML\n sections: SectionsSchema.optional(),\n source: SourceSchema.optional(),\n});\n\nexport type PromptAsset = z.infer<typeof PromptAssetSchema>;\n\n// --- Resolved asset (after includes, overrides applied) ---\n\nexport interface ResolvedPromptAsset extends PromptAsset {\n sections: {\n system_instructions?: string;\n prompt_template?: string;\n notes?: string;\n };\n source: {\n file_path?: string;\n checksum?: string;\n };\n}\n","export interface Sections {\n system_instructions?: string;\n prompt_template?: string;\n notes?: string;\n}\n\nconst SECTION_MAP: Record<string, keyof Sections> = {\n 'system instructions': 'system_instructions',\n 'prompt template': 'prompt_template',\n 'notes': 'notes',\n};\n\n/**\n * Extract named sections from the markdown body using H1 headings.\n * Case-insensitive. H2+ within a section is treated as content.\n * If no H1 headings are found, the entire body is treated as prompt_template.\n */\nexport function extractSections(body: string): Sections {\n const lines = body.split(/\\r?\\n/);\n const sections: Sections = {};\n\n let currentKey: keyof Sections | null = null;\n let currentLines: string[] = [];\n let foundAnyH1 = false;\n\n for (const line of lines) {\n const h1Match = line.match(/^#\\s+(.+)$/);\n if (h1Match) {\n // Flush previous section\n if (currentKey) {\n sections[currentKey] = currentLines.join('\\n').trim();\n }\n\n foundAnyH1 = true;\n const heading = h1Match[1].trim().toLowerCase();\n currentKey = SECTION_MAP[heading] ?? null;\n currentLines = [];\n } else {\n currentLines.push(line);\n }\n }\n\n // Flush last section\n if (currentKey) {\n sections[currentKey] = currentLines.join('\\n').trim();\n }\n\n // If no H1 headings found, treat entire body as prompt_template\n if (!foundAnyH1) {\n const trimmed = body.trim();\n if (trimmed) {\n sections.prompt_template = trimmed;\n }\n }\n\n return sections;\n}\n","import matter from 'gray-matter';\nimport { PromptAssetSchema } from '../schema/index.js';\nimport type { PromptAsset } from '../schema/index.js';\nimport { extractSections } from './sections.js';\n\nexport interface ParseResult {\n asset: PromptAsset;\n raw: {\n frontMatter: Record<string, unknown>;\n body: string;\n };\n}\n\n/**\n * Parse a prompt markdown string (YAML front matter + markdown body)\n * into a validated PromptAsset.\n */\nexport function parsePrompt(content: string, filePath?: string): ParseResult {\n const { data: frontMatter, content: body } = matter(content);\n\n const sections = extractSections(body);\n\n const raw = {\n ...frontMatter,\n sections,\n source: filePath ? { file_path: filePath } : undefined,\n };\n\n const asset = PromptAssetSchema.parse(raw);\n\n return {\n asset,\n raw: {\n frontMatter: frontMatter as Record<string, unknown>,\n body,\n },\n };\n}\n","import { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport matter from 'gray-matter';\nimport { parsePrompt } from './parser.js';\nimport type { ParseResult } from './parser.js';\nimport { extractSections } from './sections.js';\nimport { PromptDefaultsSchema } from '../schema/index.js';\nimport type { PromptDefaults } from '../schema/index.js';\n\nconst DEFAULTS_FILE_NAME = 'defaults.md';\n\nexport interface LoadPromptOptions {\n /**\n * Optional boundary directory for defaults discovery.\n * If provided, defaults are loaded from this directory down to the prompt directory.\n */\n defaultsRoot?: string;\n}\n\n/**\n * Load and parse a prompt file from disk.\n */\nexport async function loadPromptFile(filePath: string, options: LoadPromptOptions = {}): Promise<ParseResult> {\n const content = await readFile(filePath, 'utf-8');\n const parsed = parsePrompt(content, filePath);\n // Default the boundary to the file's own directory so traversal never\n // walks above the prompt tree when no explicit root is provided.\n const root = options.defaultsRoot ?? dirname(filePath);\n const defaults = await loadDefaultsForPath(filePath, root);\n const asset = applyDefaults(parsed.asset, defaults);\n\n return {\n ...parsed,\n asset,\n };\n}\n\nasync function loadDefaultsForPath(filePath: string, defaultsRoot?: string): Promise<PromptDefaults> {\n const directories = getDirectoriesToCheck(filePath, defaultsRoot);\n let merged: PromptDefaults = {};\n\n for (const dir of directories) {\n const defaultsPath = join(dir, DEFAULTS_FILE_NAME);\n try {\n const defaultsContent = await readFile(defaultsPath, 'utf-8');\n const defaults = parseDefaults(defaultsContent);\n merged = mergeDefaults(merged, defaults);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n return merged;\n}\n\nfunction getDirectoriesToCheck(filePath: string, defaultsRoot?: string): string[] {\n const dirs: string[] = [];\n let current = resolve(dirname(filePath));\n const boundary = defaultsRoot ? resolve(defaultsRoot) : undefined;\n\n while (true) {\n dirs.unshift(current);\n if ((boundary && current === boundary) || current === dirname(current)) {\n break;\n }\n current = dirname(current);\n }\n\n return dirs;\n}\n\nfunction parseDefaults(content: string): PromptDefaults {\n const { data: frontMatter, content: body } = matter(content);\n const sections = extractSections(body);\n\n return PromptDefaultsSchema.parse({\n ...frontMatter,\n sections: {\n system_instructions: sections.system_instructions,\n },\n });\n}\n\nfunction mergeDefaults(base: PromptDefaults, local: PromptDefaults): PromptDefaults {\n return {\n provider: local.provider ?? base.provider,\n model: local.model ?? base.model,\n metadata: {\n ...(base.metadata ?? {}),\n ...(local.metadata ?? {}),\n },\n sections: {\n ...(base.sections ?? {}),\n ...(local.sections ?? {}),\n },\n };\n}\n\nfunction applyDefaults(asset: ParseResult['asset'], defaults: PromptDefaults): ParseResult['asset'] {\n const hasDefaultMetadata = defaults.metadata && Object.keys(defaults.metadata).length > 0;\n const hasDefaultSystem = !!defaults.sections?.system_instructions;\n const hasDefaultScalars = defaults.provider !== undefined\n || defaults.model !== undefined;\n\n // Short-circuit: nothing to merge\n if (!hasDefaultMetadata && !hasDefaultSystem && !hasDefaultScalars) {\n return asset;\n }\n\n const mergedMetadata = {\n ...(defaults.metadata ?? {}),\n ...(asset.metadata ?? {}),\n };\n const metadata = Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined;\n\n const systemInstructions = asset.sections?.system_instructions ?? defaults.sections?.system_instructions;\n\n const sections = asset.sections\n ? { ...asset.sections, system_instructions: systemInstructions }\n : systemInstructions\n ? { system_instructions: systemInstructions }\n : undefined;\n\n return {\n ...asset,\n provider: asset.provider ?? defaults.provider,\n model: asset.model ?? defaults.model,\n metadata,\n sections,\n };\n}\n"],"mappings":"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","names":["matter","matter"]}
package/dist/cli/index.js CHANGED
@@ -72,6 +72,8 @@ var SectionsSchema = z.object({
72
72
  notes: z.string().optional()
73
73
  });
74
74
  var PromptDefaultsSchema = z.object({
75
+ provider: z.enum(["openai", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
76
+ model: z.string().optional(),
75
77
  metadata: MetadataSchema.optional(),
76
78
  sections: z.object({
77
79
  system_instructions: z.string().optional()
@@ -106,7 +108,7 @@ var SECTION_MAP = {
106
108
  "notes": "notes"
107
109
  };
108
110
  function extractSections(body) {
109
- const lines = body.split("\n");
111
+ const lines = body.split(/\r?\n/);
110
112
  const sections = {};
111
113
  let currentKey = null;
112
114
  let currentLines = [];
@@ -214,6 +216,8 @@ function parseDefaults(content) {
214
216
  }
215
217
  function mergeDefaults(base, local) {
216
218
  return {
219
+ provider: local.provider ?? base.provider,
220
+ model: local.model ?? base.model,
217
221
  metadata: {
218
222
  ...base.metadata ?? {},
219
223
  ...local.metadata ?? {}
@@ -227,7 +231,8 @@ function mergeDefaults(base, local) {
227
231
  function applyDefaults(asset, defaults) {
228
232
  const hasDefaultMetadata = defaults.metadata && Object.keys(defaults.metadata).length > 0;
229
233
  const hasDefaultSystem = !!defaults.sections?.system_instructions;
230
- if (!hasDefaultMetadata && !hasDefaultSystem) {
234
+ const hasDefaultScalars = defaults.provider !== void 0 || defaults.model !== void 0;
235
+ if (!hasDefaultMetadata && !hasDefaultSystem && !hasDefaultScalars) {
231
236
  return asset;
232
237
  }
233
238
  const mergedMetadata = {
@@ -239,6 +244,8 @@ function applyDefaults(asset, defaults) {
239
244
  const sections = asset.sections ? { ...asset.sections, system_instructions: systemInstructions } : systemInstructions ? { system_instructions: systemInstructions } : void 0;
240
245
  return {
241
246
  ...asset,
247
+ provider: asset.provider ?? defaults.provider,
248
+ model: asset.model ?? defaults.model,
242
249
  metadata,
243
250
  sections
244
251
  };
@@ -796,15 +803,17 @@ Options:
796
803
  `.trim();
797
804
  var HELLO_PROMPT = `---
798
805
  id: hello
799
- schema_version: 1
800
- provider: openai
801
- model: gpt-5.4
802
806
  context:
803
807
  inputs:
804
808
  - name
805
809
  - app_context
806
810
  includes:
807
811
  - ./shared/tone.md
812
+ environments:
813
+ dev:
814
+ model: gpt-5.4-mini
815
+ sampling:
816
+ temperature: 0.2
808
817
  ---
809
818
 
810
819
  # System instructions
@@ -817,7 +826,7 @@ Current app context: {{ app_context }}.
817
826
  Say hello to {{ name }} and ask how you can help them today.
818
827
  `.trimStart();
819
828
  var TONE_INCLUDE = `---
820
- id: shared.tone
829
+ id: shared/tone
821
830
  schema_version: 1
822
831
  ---
823
832
 
@@ -826,6 +835,8 @@ schema_version: 1
826
835
  Always be polite, professional, and concise. Avoid jargon unless the user uses it first.
827
836
  `.trimStart();
828
837
  var DEFAULTS = `---
838
+ provider: openai
839
+ model: gpt-5.4
829
840
  metadata:
830
841
  owner: my-team
831
842
  review_required: true
@@ -845,6 +856,46 @@ var TEST_SIDECAR = `cases:
845
856
  name: "Alice"
846
857
  app_context: "Settings page"
847
858
  `;
859
+ var EXAMPLE_USAGE = `// Example: render the hello prompt and send it to OpenAI
860
+ // Full docs: https://promptopskit.com/docs/index.html#/
861
+
862
+ import { createPromptOpsKit } from 'promptopskit';
863
+
864
+ async function main() {
865
+ const kit = createPromptOpsKit({ sourceDir: './prompts' });
866
+
867
+ // Determine environment from ENV var (defaults to 'dev')
868
+ // - dev: uses gpt-5.4-mini with temperature 0.2 (see hello.md environments)
869
+ // - production: uses base model gpt-5.4 with default settings
870
+ const environment = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
871
+
872
+ const { request } = await kit.renderPrompt({
873
+ path: 'hello',
874
+ provider: 'openai',
875
+ environment,
876
+ variables: {
877
+ name: 'World',
878
+ app_context: 'Welcome screen',
879
+ },
880
+ });
881
+
882
+ console.log('Model:', request.body.model);
883
+
884
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
885
+ method: 'POST',
886
+ headers: {
887
+ 'Content-Type': 'application/json',
888
+ Authorization: \`Bearer \${process.env.OPENAI_API_KEY}\`,
889
+ },
890
+ body: JSON.stringify(request.body),
891
+ });
892
+
893
+ const data = await res.json();
894
+ console.log(data.choices[0].message.content);
895
+ }
896
+
897
+ main();
898
+ `;
848
899
  async function init(args) {
849
900
  if (args.includes("--help") || args.includes("-h")) {
850
901
  console.log(HELP5);
@@ -855,7 +906,8 @@ async function init(args) {
855
906
  { path: join4(dir, "defaults.md"), content: DEFAULTS },
856
907
  { path: join4(dir, "hello.md"), content: HELLO_PROMPT },
857
908
  { path: join4(dir, "hello.test.yaml"), content: TEST_SIDECAR },
858
- { path: join4(dir, "shared", "tone.md"), content: TONE_INCLUDE }
909
+ { path: join4(dir, "shared", "tone.md"), content: TONE_INCLUDE },
910
+ { path: join4(dir, "example-usage.ts"), content: EXAMPLE_USAGE }
859
911
  ];
860
912
  let created = 0;
861
913
  let skipped = 0;
@@ -1076,16 +1128,21 @@ folder:
1076
1128
 
1077
1129
  \`\`\`text
1078
1130
  prompts/
1079
- \u251C\u2500\u2500 defaults.md # global metadata + system instructions
1131
+ \u251C\u2500\u2500 defaults.md # global provider, model, metadata + system instructions
1080
1132
  \u2514\u2500\u2500 support/
1081
1133
  \u251C\u2500\u2500 defaults.md # overrides for support/*
1082
1134
  \u2514\u2500\u2500 reply.md # inherits from support/defaults.md
1083
1135
  \`\`\`
1084
1136
 
1085
1137
  Supported default fields:
1138
+ - \`provider\` (front matter) \u2014 default provider for the folder
1139
+ - \`model\` (front matter) \u2014 default model for the folder
1086
1140
  - \`metadata\` (front matter) \u2014 merged with prompt-local metadata
1087
1141
  - \`# System instructions\` (body section) \u2014 used when the prompt has none
1088
1142
 
1143
+ This lets you configure app-wide settings like \`provider\` and \`model\`
1144
+ in a single root \`defaults.md\`, so individual prompts only declare what\u2019s unique to them.
1145
+
1089
1146
  Rules:
1090
1147
  - Nearest subfolder \`defaults.md\` overrides parent defaults
1091
1148
  - Prompt-local values always take precedence over defaults
@@ -1266,14 +1323,14 @@ Hello {{ name }}
1266
1323
  ## Conventions to follow
1267
1324
 
1268
1325
  1. **One prompt per file** \u2014 each \`.md\` file is a single prompt asset
1269
- 2. **Always set \`id\` and \`schema_version: 1\`** in front matter
1326
+ 2. **Always set \`id\` and \`schema_version: 1\`** in front matter (or inherit \`schema_version\` from \`defaults.md\`)
1270
1327
  3. **Declare all variables** in \`context.inputs\` that appear in templates
1271
1328
  4. **Use includes** for shared system instructions (tone, safety, formatting)
1272
1329
  5. **Keep prompt templates focused** \u2014 compose behavior via includes, not duplication
1273
1330
  6. **Use environment overrides** for dev/staging/prod model differences
1274
1331
  7. **Add test sidecars** (\`.test.yaml\`) for critical prompts
1275
1332
  8. **Run \`promptopskit validate\`** before committing changes
1276
- 9. **Use \`defaults.md\`** to share metadata and system instructions across a folder
1333
+ 9. **Use \`defaults.md\`** to share provider, model, metadata, and system instructions across a folder
1277
1334
  10. **Variable names** should be \`snake_case\`
1278
1335
  11. **Prompt file names** should be \`kebab-case.md\`
1279
1336
  `.trimEnd();