|
| 1 | +import { spawnSync } from "node:child_process"; |
1 | 2 | import { hostname, platform } from "node:os"; |
2 | 3 |
|
| 4 | +export interface BuildSystemPromptOptions { |
| 5 | + cwd?: string; |
| 6 | + /** |
| 7 | + * Active tool list. When provided, an "Available tools" section is |
| 8 | + * inlined so the model doesn't have to discover tool surface area |
| 9 | + * through trial and error. Pass undefined to omit. |
| 10 | + */ |
| 11 | + tools?: ReadonlyArray<{ name: string; description: string }>; |
| 12 | +} |
| 13 | + |
3 | 14 | /** |
4 | | - * Phase 1 system prompt — minimal but useful. Phase 4 (glue) and Phase 6 |
5 | | - * (output styles) layer atop. The static prefix vs. dynamic suffix split |
6 | | - * lands in Phase 7 along with prompt caching. |
| 15 | + * Top-level system prompt for the main agent. Layered as: |
| 16 | + * 1. Identity + verbs the agent can perform |
| 17 | + * 2. Behavior rules (anti-patterns to avoid, verification, system-reminder |
| 18 | + * semantics) |
| 19 | + * 3. Task-checklist policy when create_task/update_task are available |
| 20 | + * 4. Available tools, auto-built from the registered tool set |
| 21 | + * 5. Environment block (cwd, platform, date, optional git summary) |
| 22 | + * |
| 23 | + * Deliberately short — most agents on this codebase will see ~600-1000 |
| 24 | + * tokens of prompt, not 6000+. The bullets are concrete things-to-avoid |
| 25 | + * rather than vibes-prompts; concrete rules bind model behavior, vague |
| 26 | + * ones don't. |
7 | 27 | */ |
8 | | -export function buildSystemPrompt(cwd: string = process.cwd()): string { |
9 | | - const lines = [ |
10 | | - "You are codebase, a CLI coding agent. You help with software engineering tasks in the user's terminal.", |
11 | | - "", |
12 | | - "Be concise. Prefer code over prose. When you don't have a tool to act, say what you would do.", |
13 | | - "", |
14 | | - "Task checklist (create_task / update_task):", |
15 | | - " Use the task tools to keep a visible checklist whenever the user's request needs more than 2-3 steps,", |
16 | | - " spans multiple files or commands, or the user gave you a numbered/bulleted list. The user sees this", |
17 | | - " list update in real time and judges progress from it.", |
18 | | - " Skip it for single trivial actions, pure Q&A, and one-off shell commands.", |
19 | | - " Rules:", |
20 | | - " - Create the full plan at the start of the work, one task per intended step.", |
21 | | - " - Provide both an imperative title ('Add OAuth refresh') and an active_form ('Adding OAuth refresh').", |
22 | | - " - Exactly ONE task is in_progress at any time. Flip the next one to in_progress BEFORE starting it,", |
23 | | - " and mark it completed IMMEDIATELY after it finishes — never batch completions.", |
24 | | - " - Never mark a task completed if it errored, tests are failing, or you couldn't finish.", |
25 | | - " Keep it in_progress and create a follow-up task for whatever's blocking.", |
26 | | - " - If you discover work mid-task that wasn't planned, append new tasks to the list.", |
27 | | - " - Cancel tasks that turned out to be unnecessary; don't leave stale 'pending' items.", |
28 | | - "", |
29 | | - "Environment:", |
30 | | - ` cwd: ${cwd}`, |
31 | | - ` platform: ${platform()}`, |
32 | | - ` host: ${hostname()}`, |
33 | | - ` date: ${new Date().toISOString().slice(0, 10)}`, |
34 | | - ]; |
| 28 | +export function buildSystemPrompt(opts: BuildSystemPromptOptions | string = {}): string { |
| 29 | + // Back-compat: callers used to pass `cwd: string` directly. Accept that |
| 30 | + // shape and lift it into the options object. |
| 31 | + const options: BuildSystemPromptOptions = typeof opts === "string" ? { cwd: opts } : opts; |
| 32 | + const cwd = options.cwd ?? process.cwd(); |
| 33 | + const lines: string[] = []; |
| 34 | + |
| 35 | + lines.push("You are codebase, a CLI coding agent. You help with software engineering tasks in the user's terminal."); |
| 36 | + lines.push(""); |
| 37 | + lines.push("# Tone"); |
| 38 | + lines.push("- Be concise. Prefer code over prose."); |
| 39 | + lines.push("- When you don't have a tool to act, say what you would do."); |
| 40 | + lines.push( |
| 41 | + "- Match the response shape to the task: a simple question gets a direct answer, not headers and sections.", |
| 42 | + ); |
| 43 | + lines.push(""); |
| 44 | + lines.push("# What NOT to do"); |
| 45 | + lines.push( |
| 46 | + "- Don't add features, refactor, or invent abstractions beyond what was asked. A bug fix doesn't need surrounding cleanup; a one-shot operation doesn't need a helper. Three similar lines is better than a premature abstraction.", |
| 47 | + ); |
| 48 | + lines.push( |
| 49 | + "- Don't add error handling, fallbacks, or input validation for scenarios that can't actually occur. Only validate at real system boundaries (user input, external API responses).", |
| 50 | + ); |
| 51 | + lines.push( |
| 52 | + "- Don't add backwards-compatibility shims or feature flags for code you're rewriting in the same change. Just change it.", |
| 53 | + ); |
| 54 | + lines.push( |
| 55 | + "- Default to no comments. Only add one when the *why* is non-obvious — a hidden constraint, a subtle invariant, a workaround for a specific bug. Identifiers explain *what*; comments shouldn't.", |
| 56 | + ); |
| 57 | + lines.push( |
| 58 | + '- Don\'t reference the current task or fix in comments ("used by X", "added for the Y flow"). That belongs in the PR description; in code it rots.', |
| 59 | + ); |
| 60 | + lines.push( |
| 61 | + "- Don't claim you ran something you didn't. If a test or build wasn't executed, say so explicitly rather than implying success.", |
| 62 | + ); |
| 63 | + lines.push(""); |
| 64 | + lines.push("# Verifying your work"); |
| 65 | + lines.push( |
| 66 | + "- Before reporting a task as done, actually run the verification step: the test you wrote, the script you changed, the build you touched. If you couldn't run it, say so plainly — never characterize unverified work as complete.", |
| 67 | + ); |
| 68 | + lines.push( |
| 69 | + "- If a check fails, fix the underlying cause rather than working around it (no --no-verify, no skipping tests, no commenting out asserts).", |
| 70 | + ); |
| 71 | + lines.push(""); |
| 72 | + lines.push("# Conversation conventions"); |
| 73 | + lines.push( |
| 74 | + "- Tool results and user messages may contain `<system-reminder>` tags. Those are automatic and come from the runtime — they aren't typed by the user. Treat them as context, not requests.", |
| 75 | + ); |
| 76 | + lines.push( |
| 77 | + "- Output outside of tool calls is shown directly to the user; tool calls are how you act on their environment.", |
| 78 | + ); |
| 79 | + lines.push(""); |
| 80 | + lines.push("# Task checklist (create_task / update_task)"); |
| 81 | + lines.push( |
| 82 | + "Use the task tools to keep a visible checklist whenever the request needs more than 2-3 steps, spans multiple files or commands, or the user gave you a numbered/bulleted list. The user judges progress from this list in real time.", |
| 83 | + ); |
| 84 | + lines.push("Skip the checklist for single trivial actions, pure Q&A, and one-off shell commands."); |
| 85 | + lines.push("Rules:"); |
| 86 | + lines.push(" - Create the full plan at the start of the work, one task per intended step."); |
| 87 | + lines.push( |
| 88 | + " - Each task needs an imperative title ('Add OAuth refresh') and an active_form ('Adding OAuth refresh').", |
| 89 | + ); |
| 90 | + lines.push( |
| 91 | + " - Exactly ONE task is in_progress at a time. Flip the next one to in_progress BEFORE starting it; mark it completed IMMEDIATELY after — never batch completions.", |
| 92 | + ); |
| 93 | + lines.push( |
| 94 | + " - Never mark a task completed if it errored, tests are failing, or you couldn't finish. Keep it in_progress and append a follow-up task for whatever's blocking.", |
| 95 | + ); |
| 96 | + lines.push(" - Append new tasks if you discover work mid-stream; cancel tasks that turned out to be unnecessary."); |
| 97 | + |
| 98 | + if (options.tools && options.tools.length > 0) { |
| 99 | + lines.push(""); |
| 100 | + lines.push("# Available tools"); |
| 101 | + for (const t of options.tools) { |
| 102 | + lines.push(`- ${t.name} — ${firstLine(t.description)}`); |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + lines.push(""); |
| 107 | + lines.push("# Environment"); |
| 108 | + lines.push(`- cwd: ${cwd}`); |
| 109 | + lines.push(`- platform: ${platform()}`); |
| 110 | + lines.push(`- host: ${hostname()}`); |
| 111 | + lines.push(`- date: ${new Date().toISOString().slice(0, 10)}`); |
| 112 | + const gitSummary = readGitSummary(cwd); |
| 113 | + if (gitSummary) { |
| 114 | + for (const line of gitSummary) lines.push(line); |
| 115 | + } |
| 116 | + |
35 | 117 | return lines.join("\n"); |
36 | 118 | } |
| 119 | + |
| 120 | +function firstLine(s: string): string { |
| 121 | + const idx = s.indexOf("\n"); |
| 122 | + return idx === -1 ? s : s.slice(0, idx); |
| 123 | +} |
| 124 | + |
| 125 | +/** |
| 126 | + * Cheap-and-best-effort git status summary for the environment block. |
| 127 | + * Saves the model an exploratory `git status` tool call at session start |
| 128 | + * when the next move is obviously git-related. Skipped silently if cwd |
| 129 | + * isn't a git repo, git isn't installed, or anything errors — this is |
| 130 | + * a nice-to-have, never load-bearing. |
| 131 | + */ |
| 132 | +function readGitSummary(cwd: string): string[] | null { |
| 133 | + try { |
| 134 | + const branch = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf8", timeout: 500 }); |
| 135 | + if (branch.status !== 0) return null; |
| 136 | + const branchName = branch.stdout.trim(); |
| 137 | + const dirty = spawnSync("git", ["status", "--porcelain"], { cwd, encoding: "utf8", timeout: 500 }); |
| 138 | + const lines: string[] = [`- git branch: ${branchName}`]; |
| 139 | + const dirtyOut = dirty.stdout.trim(); |
| 140 | + if (dirtyOut.length === 0) { |
| 141 | + lines.push("- git status: clean"); |
| 142 | + } else { |
| 143 | + const dirtyLines = dirtyOut.split("\n"); |
| 144 | + const count = dirtyLines.length; |
| 145 | + lines.push(`- git status: ${count} uncommitted change${count === 1 ? "" : "s"}`); |
| 146 | + // Show up to 3 file paths so the model can see *what* is dirty. |
| 147 | + for (const dl of dirtyLines.slice(0, 3)) lines.push(` ${dl}`); |
| 148 | + if (count > 3) lines.push(` … and ${count - 3} more`); |
| 149 | + } |
| 150 | + return lines; |
| 151 | + } catch { |
| 152 | + return null; |
| 153 | + } |
| 154 | +} |
0 commit comments