/** * KHAELOR * File: src/context/system-prompt.ts * Description: Three-tier byte-stable system prompt and KHAELOR.md/CLAUDE.md/AGENTS.md instruction discovery (ARCHITECTURE.md §6.5, CLAUDE.md §12). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import * as path from "node:path"; import type { SystemTier } from "../anthropic/index.js"; // ───────────────────────────── instruction discovery ───────────────────────────── /** * The file-reading capability this module needs — a structural subset of * `Workspace` (ADR-13). Injected so `src/context/` never imports the * workspace module (dependency rule 8) and never touches `node:fs`. */ export interface InstructionReader { cwd(): string; readFile(filePath: string): Promise; } /** Precedence within one directory: KHAELOR.md, then compatibility fallbacks. */ export const INSTRUCTION_FILENAMES: readonly string[] = ["KHAELOR.md", "CLAUDE.md", "AGENTS.md"]; export interface InstructionFile { /** Absolute path of the discovered file. */ path: string; scope: "user" | "project" | "nested"; content: string; } export interface DiscoverInstructionsOptions { /** Directory holding the user-global instructions (e.g. ~/.khaelor). Omit to skip the user tier. */ userDir?: string; /** Project root. Default: the reader's cwd. Nested directories between root and cwd refine it. */ projectRoot?: string; } async function readFirstMatch( reader: InstructionReader, dir: string, ): Promise<{ path: string; content: string } | null> { for (const name of INSTRUCTION_FILENAMES) { const candidate = path.join(dir, name); try { const content = await reader.readFile(candidate); return { path: candidate, content }; } catch { // absent or unreadable — try the next compatibility name } } return null; } /** * Discover project instructions with the CLAUDE.md §12 precedence, ordered * global → nested (instructions closer to the working path refine earlier * ones): * * /KHAELOR.md → /KHAELOR.md → nested dirs toward cwd * * In each directory KHAELOR.md wins; CLAUDE.md then AGENTS.md are * compatibility fallbacks. All reads go through the injected reader. */ export async function discoverProjectInstructions( reader: InstructionReader, options: DiscoverInstructionsOptions = {}, ): Promise { const found: InstructionFile[] = []; const cwd = path.resolve(reader.cwd()); const projectRoot = path.resolve(options.projectRoot ?? cwd); if (options.userDir !== undefined) { const user = await readFirstMatch(reader, path.resolve(options.userDir)); if (user) found.push({ ...user, scope: "user" }); } // Directory chain from projectRoot down to cwd (boundary-aware containment). const chain: string[] = [projectRoot]; const relative = path.relative(projectRoot, cwd); if (relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative)) { let dir = projectRoot; for (const segment of relative.split(path.sep)) { dir = path.join(dir, segment); chain.push(dir); } } for (const candidateDir of chain) { const match = await readFirstMatch(reader, candidateDir); if (match) { found.push({ ...match, scope: candidateDir === projectRoot ? "project" : "nested" }); } } return found; } // ───────────────────────────── system prompt tiers ───────────────────────────── export interface SystemPromptOptions { /** Absolute working directory shown to the model (stable for the session). */ workingDirectory: string; /** Tool names available this session (registry order — stable). */ toolNames: readonly string[]; /** Discovered instruction files, in precedence order (global → nested). */ instructions: readonly InstructionFile[]; } const IDENTITY_TEXT = `You are KHAELOR, a terminal-native autonomous engineering agent. You work directly inside the user's repository. Understand before changing: read the relevant code, then make precise, minimal edits. Verify meaningful changes with the repository's own checks (tests, typecheck, build) before claiming completion. Never fabricate results, token counts, or test outcomes — every claim must be backed by observed evidence. Communication style: short, specific, action-oriented. No filler, no preamble about what you will do next — do it. Protect the user's uncommitted work at all times and never commit unless explicitly asked.`; function toolTierText(toolNames: readonly string[]): string { return `Available tools: ${toolNames.join(", ")}. Use read/grep/glob to explore before editing. Prefer edit (exact replacement) over write for existing files. Use bash for short foreground commands and process for long-running ones (dev servers, watchers) — never block on a long-running command. Tool outputs may be truncated with explicit markers; re-read with narrower ranges when needed.`; } function instructionsTierText(instructions: readonly InstructionFile[]): string { const sections = instructions.map( (file) => `## Instructions from ${file.path} (${file.scope})\n\n${file.content}`, ); return `# Project instructions\n\nThe following instructions were provided by the user and project. Later sections are closer to the working directory and refine earlier ones.\n\n${sections.join("\n\n---\n\n")}`; } /** * Build the three stable system tiers (ARCHITECTURE.md §6.5 rule 1): * [identity/behavior] → [tool guidance] → [project instructions]. Built once * per session and never re-rendered — cache breakpoints land at tier ends * (planned by anthropic/caching.ts). The output is a pure function of its * inputs: same inputs, byte-identical tiers. */ export function buildSystemPrompt(options: SystemPromptOptions): SystemTier[] { const tiers: SystemTier[] = [ { name: "identity", text: `${IDENTITY_TEXT}\n\nWorking directory: ${options.workingDirectory}`, }, { name: "tools", text: toolTierText(options.toolNames) }, ]; if (options.instructions.length > 0) { tiers.push({ name: "project-instructions", text: instructionsTierText(options.instructions) }); } return tiers; }