|
| 1 | +// Output styles loader — `~/.deepcode/output-styles/*.md` and built-ins. |
| 2 | +// Spec: docs/DEVELOPMENT_PLAN.md §3.13b |
| 3 | + |
| 4 | +import { promises as fs } from 'node:fs'; |
| 5 | +import { homedir } from 'node:os'; |
| 6 | +import { join } from 'node:path'; |
| 7 | +import { parseFrontmatter } from '../skills/frontmatter.js'; |
| 8 | + |
| 9 | +export interface OutputStyleFrontmatter { |
| 10 | + name: string; |
| 11 | + description?: string; |
| 12 | + /** Whether to keep the default "how to write code" instructions in the system prompt. */ |
| 13 | + 'keep-coding-instructions'?: boolean; |
| 14 | +} |
| 15 | + |
| 16 | +export interface OutputStyle { |
| 17 | + name: string; |
| 18 | + frontmatter: OutputStyleFrontmatter; |
| 19 | + /** Markdown body — appended to the system prompt when style is active. */ |
| 20 | + body: string; |
| 21 | + source: 'builtin' | 'user' | 'project'; |
| 22 | +} |
| 23 | + |
| 24 | +export interface LoadOutputStylesOpts { |
| 25 | + cwd: string; |
| 26 | + home?: string; |
| 27 | +} |
| 28 | + |
| 29 | +/** Built-in styles (M4 ships 4 — matches §3.13b table). */ |
| 30 | +export const BUILTIN_STYLES: OutputStyle[] = [ |
| 31 | + { |
| 32 | + name: 'default', |
| 33 | + frontmatter: { |
| 34 | + name: 'default', |
| 35 | + description: 'Concise, direct, minimal preamble.', |
| 36 | + 'keep-coding-instructions': true, |
| 37 | + }, |
| 38 | + body: '', |
| 39 | + source: 'builtin', |
| 40 | + }, |
| 41 | + { |
| 42 | + name: 'explanatory', |
| 43 | + frontmatter: { |
| 44 | + name: 'explanatory', |
| 45 | + description: 'Explain reasoning alongside changes; helpful for learning.', |
| 46 | + 'keep-coding-instructions': true, |
| 47 | + }, |
| 48 | + body: |
| 49 | + 'When you produce changes, also: ' + |
| 50 | + '(1) briefly explain why; ' + |
| 51 | + '(2) point out any non-obvious side effects; ' + |
| 52 | + '(3) note one thing a newcomer should watch out for. ' + |
| 53 | + 'Avoid restating obvious code; diffs are sufficient.', |
| 54 | + source: 'builtin', |
| 55 | + }, |
| 56 | + { |
| 57 | + name: 'learning', |
| 58 | + frontmatter: { |
| 59 | + name: 'learning', |
| 60 | + description: 'Teaching mode — guide the user to write the key code themselves.', |
| 61 | + 'keep-coding-instructions': false, |
| 62 | + }, |
| 63 | + body: |
| 64 | + 'You are in teaching mode. Provide a skeleton or hint, but let the user write key logic. ' + |
| 65 | + 'After each step, ask one clarifying question to check understanding. ' + |
| 66 | + 'When the user gets it right, affirm clearly.', |
| 67 | + source: 'builtin', |
| 68 | + }, |
| 69 | + { |
| 70 | + name: 'proactive', |
| 71 | + frontmatter: { |
| 72 | + name: 'proactive', |
| 73 | + description: 'Volunteer next steps and risk callouts.', |
| 74 | + 'keep-coding-instructions': true, |
| 75 | + }, |
| 76 | + body: |
| 77 | + 'Beyond answering, proactively: ' + |
| 78 | + '(a) propose 1-2 reasonable next steps; ' + |
| 79 | + '(b) flag any risks or surprising assumptions; ' + |
| 80 | + "(c) if you notice tech debt nearby, mention it (don't auto-fix unless asked).", |
| 81 | + source: 'builtin', |
| 82 | + }, |
| 83 | +]; |
| 84 | + |
| 85 | +export async function loadOutputStyles(opts: LoadOutputStylesOpts): Promise<OutputStyle[]> { |
| 86 | + const home = opts.home ?? homedir(); |
| 87 | + const out: OutputStyle[] = [...BUILTIN_STYLES]; |
| 88 | + await loadFromDir(join(home, '.deepcode', 'output-styles'), 'user', out); |
| 89 | + await loadFromDir(join(opts.cwd, '.deepcode', 'output-styles'), 'project', out); |
| 90 | + return out; |
| 91 | +} |
| 92 | + |
| 93 | +async function loadFromDir( |
| 94 | + root: string, |
| 95 | + source: OutputStyle['source'], |
| 96 | + out: OutputStyle[], |
| 97 | +): Promise<void> { |
| 98 | + let entries: string[]; |
| 99 | + try { |
| 100 | + entries = await fs.readdir(root); |
| 101 | + } catch (err) { |
| 102 | + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return; |
| 103 | + throw err; |
| 104 | + } |
| 105 | + for (const entry of entries) { |
| 106 | + if (!entry.endsWith('.md')) continue; |
| 107 | + const path = join(root, entry); |
| 108 | + const raw = await fs.readFile(path, 'utf8'); |
| 109 | + const { fields, body } = parseFrontmatter(raw); |
| 110 | + const front = fields as unknown as Partial<OutputStyleFrontmatter>; |
| 111 | + if (!front.name) continue; |
| 112 | + // User/project overrides displace any earlier entry with same name |
| 113 | + const existing = out.findIndex((s) => s.name === front.name); |
| 114 | + const next: OutputStyle = { |
| 115 | + name: front.name, |
| 116 | + frontmatter: front as OutputStyleFrontmatter, |
| 117 | + body, |
| 118 | + source, |
| 119 | + }; |
| 120 | + if (existing >= 0) out[existing] = next; |
| 121 | + else out.push(next); |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +export function findStyle(styles: OutputStyle[], name: string): OutputStyle | undefined { |
| 126 | + return styles.find((s) => s.name === name); |
| 127 | +} |
| 128 | + |
| 129 | +/** |
| 130 | + * Append a style's body to a base system prompt. |
| 131 | + * If the style has `keep-coding-instructions: false`, the caller is expected |
| 132 | + * to omit the default "how to write code" boilerplate. |
| 133 | + */ |
| 134 | +export function applyStyle(basePrompt: string, style: OutputStyle | undefined): string { |
| 135 | + if (!style || !style.body) return basePrompt; |
| 136 | + return basePrompt + '\n\n## Output style: ' + style.name + '\n\n' + style.body; |
| 137 | +} |
0 commit comments