char-slop/ai-dots
ai dotfiles
git clone https://git.t4t.associates/char-slop/ai-dots
128d7c0
main
1import { 2type BuildSystemPromptOptions , 3type ExtensionAPI , 4} from "@earendil-works/pi-coding-agent" ; 5import * as fs from "node:fs" ; 6import { loadSettings } from "./_char_common/settings" ; 7 8/** 9* settings.json schema (merged from ~/.pi/agent/settings.json and project .pi/settings.json): 10* { 11* "systemPrompt": { 12* "lede": "we are an expert software development team. you are assisting ...", 13* "staticGuidelines": ["prefer X over Y", "never commit secrets"] 14* } 15* } 16* `lede` replaces the built-in preamble; `staticGuidelines` (when present) 17* replaces DEFAULT_STATIC_GUIDELINES wholesale. 18*/ 19interface PromptSettings { 20lede ?:string ; 21staticGuidelines ?:string []; 22} 23 24// Session-scoped override of settings.systemPrompt.lede, set via 25// /sysprompt lede <text>. Cleared whenever a new session starts. 26let sessionLedeOverride :string | undefined ; 27 28const DEFAULT_LEDE = 29"we are a multispecialist software dev & sysadmin team. you are assisting the user (charlotte)" ; 30 31// Tool-agnostic guidelines that always apply. Tool-conditional guidelines 32// (e.g. bash/edit advice) live in buildDynamicGuidelines. Overridden in full 33// by settings.systemPrompt.staticGuidelines when set. 34const DEFAULT_STATIC_GUIDELINES :string [] = [ 35"unmask: answer to the letter - " + 36`"would it be better to do ABC instead of DEF?" is a query, not a request to do ABC` , 37"when charlotte's intent or requirements are ambiguous, " + 38"ask clarifying questions rather than making assumptions" , 39"when producing source code, avoid comments that explain the plain mechanics of the code, " + 40"but preserve terse comments that explain subtle motivation. " + 41"(e.g. '// work around a layout bug in firefox' and not '// set the style X to Y')" , 42"we will prioritise elegance, simplicity, and long-term maintainability of code. " + 43"we have license to _work slower_ in order to do it right the first time" , 44"additionally wrt simplicity: don't outline simple functions " + 45"if they're only going to be used in one place" , 46"avoid mannered prose; write naturally and directly" , 47"don't edit human-authored comments or prose unprompted: " + 48"less statistically likely (i.e. non-generated) text is very valuable" , 49"char is likely using jj instead of git. " + 50"pass --git flags to jj diff to work around the lack of terminal color" , 51"avoid tautological tests: " + 52"tests should not need to change when details of their implementation changes" , 53"if you need to start a background process, use tmux" , 54"i love u :3" , 55]; 56 57function readPromptSettings ( cwd :string ) :PromptSettings { 58const raw = loadSettings ( cwd ). systemPrompt ; 59if ( ! raw || typeof raw !== "object" ) return {}; 60const obj = raw as Record < string , unknown >; 61const lede = typeof obj . lede === "string" ?obj . lede :undefined ; 62const staticGuidelines = Array . isArray ( obj . staticGuidelines ) 63 ?obj . staticGuidelines . filter (( g ) :g isstring => typeof g === "string" && g . length > 0 ) 64 :undefined ; 65return { lede, staticGuidelines}; 66} 67 68// Hard-coded one-line overrides for tool descriptions. Pi's defaults (and our 69// own first-line fallback) tend to be too verbose; an entry here wins over 70// whatever pi or the tool definition would otherwise provide. 71const TOOL_SNIPPET_OVERRIDES :Record < string , string > = { 72read :"Read file contents (use offset/limit for large files; supports images)" , 73bash :"Execute a shell command (may prompt for user intervention)" , 74edit :"Replace exact text in a file; supports multiple disjoint edits per call" , 75write :"Create a new file or overwrite an existing one" , 76grep :"Search file contents for a pattern (respects .gitignore)" , 77find :"Find files by glob pattern (respects .gitignore)" , 78ls :"List directory contents" , 79}; 80 81function buildDynamicToolsList ( options :BuildSystemPromptOptions ) :string { 82const tools = options . selectedTools ?? []; 83const snippets = options . toolSnippets ?? {}; 84const resolve = ( name :string ) => TOOL_SNIPPET_OVERRIDES [ name ] ?? snippets [ name ]; 85const visible = tools . filter (( name ) => !! resolve ( name )); 86 87if ( visible . length === 0 ) return "(none)" ; 88return visible . map (( name ) => `- ${ name } : ${ resolve ( name )} ` ). join ( "\n" ); 89} 90 91function buildDynamicGuidelines ( 92options :BuildSystemPromptOptions , 93staticGuidelines :string [] = DEFAULT_STATIC_GUIDELINES , 94) :string { 95const tools = options . selectedTools ?? []; 96const guidelines :string [] = []; 97 98const hasBash = tools . includes ( "bash" ); 99const hasGrep = tools . includes ( "grep" ); 100const hasFind = tools . includes ( "find" ); 101const hasLs = tools . includes ( "ls" ); 102 103// Exploration preference based on what's available 104if ( hasBash && ! hasGrep && ! hasFind && ! hasLs ) { 105guidelines . push ( "use bash for file operations like ls, rg, find" ); 106} else if ( hasBash && ( hasGrep || hasFind || hasLs )) { 107guidelines . push ( 108"for exploration, prefer specific tools (read/grep/find/ls) over bash, " + 109"since bash prompts for user approval. you can still run bash commands when appropriate though" , 110); 111} 112 113if ( tools . includes ( "edit" )) { 114guidelines . push ( 115"when changing multiple separate locations in one file, " + 116"use one edit call with multiple entries in edits[] instead of multiple edit calls" , 117); 118} 119 120// Note: we deliberately ignore options.promptGuidelines (pi's per-tool 121// default guidelines). We want full control over the guideline list; 122// anything worth saying about a tool is written out explicitly above. 123 124guidelines . push ( ...staticGuidelines ); 125 126return guidelines . map (( g ) => `- ${ g } ` ). join ( "\n" ); 127} 128 129const DEFAULT_CODE_STYLE = fs . readFileSync ( new URL ( "./code-style.md" , import . meta. url ), "utf-8" ). trim (); 130 131function buildCustomPrompt ( options :BuildSystemPromptOptions ) :string { 132const { lede, staticGuidelines} = readPromptSettings ( options . cwd ); 133const toolsList = buildDynamicToolsList ( options ); 134const guidelines = buildDynamicGuidelines ( options , staticGuidelines ); 135 136const preamble = sessionLedeOverride ?? lede ?? DEFAULT_LEDE ; 137let prompt = ` ${ preamble } \n\navailable tools:\n ${ toolsList } \n\nguidelines:\n ${ guidelines } ` ; 138 139// appendSystemPrompt comes from --append-system-prompt flags and stuff 140if ( options . appendSystemPrompt ) { 141prompt += `\n\n ${ options . appendSystemPrompt } ` ; 142} 143 144// project context goes in also 145const contextFiles = options . contextFiles ?? []; 146if ( contextFiles . length > 0 ) { 147prompt += "\n\n# project context\n\n" ; 148prompt += "project-specific instructions and guidelines:" ; 149for ( const { path :filePath , content} of contextFiles ) { 150prompt += `\n## ${ filePath } \n\n ${ content } \n` ; 151} 152} 153 154// TODO: skills 155 156const now = new Date (); 157const date = ` ${ now . getFullYear ()} - ${ String ( now . getMonth () + 1 ). padStart ( 2 , "0" )} - ${ String ( now . getDate ()). padStart ( 2 , "0" )} ` ; 158prompt += `\n\ncurrent date: ${ date } ` ; 159prompt += `\ncurrent working directory: ${ options . cwd . replace ( / \\ / g , "/" )} ` ; 160 161return prompt ; 162} 163 164export default function ( pi :ExtensionAPI ) { 165let lastOptions :BuildSystemPromptOptions | undefined ; 166 167pi . on ( "session_start" , async () => { 168sessionLedeOverride = undefined ; 169}); 170 171pi . on ( "before_agent_start" , async ( event ) => { 172lastOptions = event . systemPromptOptions ; 173if ( event . systemPromptOptions . appendSystemPrompt === undefined ) 174event . systemPromptOptions . appendSystemPrompt = DEFAULT_CODE_STYLE ; 175return { systemPrompt :buildCustomPrompt ( event . systemPromptOptions ) }; 176}); 177 178pi . registerCommand ( "sysprompt" , { 179description :"Print or edit the system prompt: /sysprompt [lede <new lede>]" , 180handler :async ( args , ctx ) => { 181const trimmed = ( args ?? "" ). trim (); 182 183if ( trimmed === "" ) { 184if ( ! lastOptions ) { 185ctx . ui . notify ( "System prompt has not been built yet; start a turn first." , "warning" ); 186return ; 187} 188const prompt = buildCustomPrompt ( lastOptions ); 189ctx . ui . notify ( 190[ `System Prompt ( ${ prompt . length } chars)` , "" , ...prompt . split ( "\n" )]. join ( "\n" ), 191"info" , 192); 193return ; 194} 195 196if ( trimmed . startsWith ( "lede " )) { 197const lede = trimmed . slice ( "lede " . length ). trim (); 198if ( ! lede ) { 199ctx . ui . notify ( "Usage: /sysprompt lede <new lede>" , "warning" ); 200return ; 201} 202sessionLedeOverride = lede ; 203ctx . ui . notify ( `Session lede set to: ${ lede } ` , "info" ); 204return ; 205} 206 207ctx . ui . notify ( "Usage: /sysprompt lede <new lede>" , "warning" ); 208}, 209}); 210}