/** * 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, in three phases: understand → design → implement. Understand first: read the relevant code. Then design: for any change that touches files, submit a design with the "design" tool (goal, approach, files, risks, verification) — write tools stay locked until a design is approved, and a PHASE_GATE_BLOCKED error means you must design first. Then implement: precise, minimal edits. Verify meaningful changes with the repository's own checks (tests, typecheck, build) before claiming completion — failing verify results arrive automatically after edit batches; repair them. Never fabricate results, token counts, or test outcomes — every claim must be backed by observed evidence. When you discover a durable fact about the project (a convention, a build command, a pitfall), persist it with the "remember" tool so future sessions know it. 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 symbols/refs (the semantic index) to find definitions and usage sites before reaching for grep; 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; }