char-slop/ai-dots

ai dotfiles

git clone https://git.t4t.associates/char-slop/ai-dots

Charlotte Somsystem prompt tweaks128d7c0

main
8.3 KiB210 linesraw
1import {
2  type BuildSystemPromptOptions,
3  type 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 {
20  lede?: string;
21  staticGuidelines?: 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 {
58  const raw = loadSettings(cwd).systemPrompt;
59  if (!raw || typeof raw !== "object") return {};
60  const obj = raw as Record<string, unknown>;
61  const lede = typeof obj.lede === "string" ? obj.lede : undefined;
62  const staticGuidelines = Array.isArray(obj.staticGuidelines)
63    ? obj.staticGuidelines.filter((g): g is string => typeof g === "string" && g.length > 0)
64    : undefined;
65  return { 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> = {
72  read: "Read file contents (use offset/limit for large files; supports images)",
73  bash: "Execute a shell command (may prompt for user intervention)",
74  edit: "Replace exact text in a file; supports multiple disjoint edits per call",
75  write: "Create a new file or overwrite an existing one",
76  grep: "Search file contents for a pattern (respects .gitignore)",
77  find: "Find files by glob pattern (respects .gitignore)",
78  ls: "List directory contents",
79};
80
81function buildDynamicToolsList(options: BuildSystemPromptOptions): string {
82  const tools = options.selectedTools ?? [];
83  const snippets = options.toolSnippets ?? {};
84  const resolve = (name: string) => TOOL_SNIPPET_OVERRIDES[name] ?? snippets[name];
85  const visible = tools.filter((name) => !!resolve(name));
86
87  if (visible.length === 0) return "(none)";
88  return visible.map((name) => `- ${name}: ${resolve(name)}`).join("\n");
89}
90
91function buildDynamicGuidelines(
92  options: BuildSystemPromptOptions,
93  staticGuidelines: string[] = DEFAULT_STATIC_GUIDELINES,
94): string {
95  const tools = options.selectedTools ?? [];
96  const guidelines: string[] = [];
97
98  const hasBash = tools.includes("bash");
99  const hasGrep = tools.includes("grep");
100  const hasFind = tools.includes("find");
101  const hasLs = tools.includes("ls");
102
103  // Exploration preference based on what's available
104  if (hasBash && !hasGrep && !hasFind && !hasLs) {
105    guidelines.push("use bash for file operations like ls, rg, find");
106  } else if (hasBash && (hasGrep || hasFind || hasLs)) {
107    guidelines.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
113  if (tools.includes("edit")) {
114    guidelines.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
124  guidelines.push(...staticGuidelines);
125
126  return 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 {
132  const { lede, staticGuidelines } = readPromptSettings(options.cwd);
133  const toolsList = buildDynamicToolsList(options);
134  const guidelines = buildDynamicGuidelines(options, staticGuidelines);
135
136  const preamble = sessionLedeOverride ?? lede ?? DEFAULT_LEDE;
137  let prompt = `${preamble}\n\navailable tools:\n${toolsList}\n\nguidelines:\n${guidelines}`;
138
139  // appendSystemPrompt comes from --append-system-prompt flags and stuff
140  if (options.appendSystemPrompt) {
141    prompt += `\n\n${options.appendSystemPrompt}`;
142  }
143
144  // project context goes in also
145  const contextFiles = options.contextFiles ?? [];
146  if (contextFiles.length > 0) {
147    prompt += "\n\n# project context\n\n";
148    prompt += "project-specific instructions and guidelines:";
149    for (const { path: filePath, content } of contextFiles) {
150      prompt += `\n## ${filePath}\n\n${content}\n`;
151    }
152  }
153
154  // TODO: skills
155
156  const now = new Date();
157  const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
158  prompt += `\n\ncurrent date: ${date}`;
159  prompt += `\ncurrent working directory: ${options.cwd.replace(/\\/g, "/")}`;
160
161  return prompt;
162}
163
164export default function (pi: ExtensionAPI) {
165  let lastOptions: BuildSystemPromptOptions | undefined;
166
167  pi.on("session_start", async () => {
168    sessionLedeOverride = undefined;
169  });
170
171  pi.on("before_agent_start", async (event) => {
172    lastOptions = event.systemPromptOptions;
173    if (event.systemPromptOptions.appendSystemPrompt === undefined)
174      event.systemPromptOptions.appendSystemPrompt = DEFAULT_CODE_STYLE;
175    return { systemPrompt: buildCustomPrompt(event.systemPromptOptions) };
176  });
177
178  pi.registerCommand("sysprompt", {
179    description: "Print or edit the system prompt: /sysprompt [lede <new lede>]",
180    handler: async (args, ctx) => {
181      const trimmed = (args ?? "").trim();
182
183      if (trimmed === "") {
184        if (!lastOptions) {
185          ctx.ui.notify("System prompt has not been built yet; start a turn first.", "warning");
186          return;
187        }
188        const prompt = buildCustomPrompt(lastOptions);
189        ctx.ui.notify(
190          [`System Prompt (${prompt.length} chars)`, "", ...prompt.split("\n")].join("\n"),
191          "info",
192        );
193        return;
194      }
195
196      if (trimmed.startsWith("lede ")) {
197        const lede = trimmed.slice("lede ".length).trim();
198        if (!lede) {
199          ctx.ui.notify("Usage: /sysprompt lede <new lede>", "warning");
200          return;
201        }
202        sessionLedeOverride = lede;
203        ctx.ui.notify(`Session lede set to: ${lede}`, "info");
204        return;
205      }
206
207      ctx.ui.notify("Usage: /sysprompt lede <new lede>", "warning");
208    },
209  });
210}