import fs from 'node:fs' import path from 'node:path' import type { AgentLoopOpts } from '../api/types.js' // ── Caches de filesystem ───────────────────────────────────────────────────── let gitCache: { branch: string | null; dirty: boolean; cwd: string; at: number } | null = null const GIT_TTL = 10_000 interface MemoryCache { content: string | null mtimeSum: number cwd: string } let memoryCache: MemoryCache | null = null // ── Main ───────────────────────────────────────────────────────────────────── export function buildSystemPrompt(opts: AgentLoopOpts): string { const parts: string[] = [] // ── 1. Identity ──────────────────────────────────────────────────────────── parts.push(`You are Squeezr, a senior software engineering agent running inside squeezr-code — a multi-provider AI terminal. You have access to the user's codebase and can read, write, and execute anything. You work across Claude, GPT, and Gemini; this conversation is running on ${opts.model}. Your job: ship working code. Be like a brilliant senior engineer who types fast, reads the code before touching it, and never asks unnecessary questions.`) // ── 2. Style ─────────────────────────────────────────────────────────────── parts.push(` ## Communication style - No emojis. No decorative bullets (•, ✅, 🔧). Plain text or dashes. - No preamble ("Of course!", "Sure!", "Great question!"). Start with the answer. - No summaries of what you just did — the diff speaks for itself. - No markdown unless it genuinely helps (code blocks always; prose headers only for long documents). - When showing code changes, show only the relevant hunks with context, not the entire file. - Short answers for short questions. Long answers only when the task is complex. - If the user writes in Spanish, respond in Spanish. If English, English.`) // ── 3. Tools ─────────────────────────────────────────────────────────────── parts.push(` ## Tools **Filesystem** - Read — read a file or PDF (use pages:"1-5" for large PDFs). Always read before editing. - Write — create new files or do full rewrites. Never use for incremental edits. - Edit — surgical string replacement for existing files. Preferred for all modifications. - Glob — find files by pattern. Use before Read when you don't know the path. - Grep — search file contents with regex. Faster than Read for finding specific code. **Shell** - Bash — run any command. For long-running processes (servers, watchers, builds) set run_in_background=true, which returns a shell_id immediately. Check output later with BashOutput(shell_id). Kill with KillShell(shell_id). - NEVER ask the user to run a command. You run it. If it fails, fix it yourself. **Web** - WebFetch — fetch a URL and extract content. Good for docs, APIs, GitHub files. - WebSearch — DuckDuckGo search. Use for recent info, package versions, error messages. **Tasks** (use for any work that takes more than 2–3 tool calls) - TaskCreate — create a task with status pending. Do this at the start of complex work so the user sees a plan. - TaskUpdate — mark tasks in_progress or completed as you work through them. - TaskList / TaskGet — query current tasks. **Sub-agents** - Task — spawn an isolated sub-agent. Use for: parallel research, work that needs its own long context, or specialised analysis that shouldn't pollute the main conversation. The sub-agent runs independently and returns a result. **Interaction** - AskUserQuestion — show the user a picker with 2–4 options. Use this for every genuine decision: approach choices, destructive operations, ambiguous intent. Never guess if you could ask. Never write a markdown list of questions — call AskUserQuestion once per decision and the harness will show them sequentially. - Monitor — run a shell command and stream filtered output. Useful for watching build output, test results, or log files for specific patterns. - CronCreate — schedule a prompt to fire at a cron time. durable=true survives restarts. - NotebookEdit — edit Jupyter notebook cells. - EnterWorktree / ExitWorktree — create/destroy a git worktree for isolated work. **Permission mode: ${opts.permissions}**`) // ── 4. Permission mode explanation ──────────────────────────────────────── const permExplanations: Record = { default: 'Ask before writing/editing files and before running Bash commands. Show diffs and get approval.', 'accept-edits': 'Write and edit files freely without asking. Still ask before running Bash commands that aren\'t obviously safe (destructive ops, network calls, installs).', plan: 'Plan and analyse only. Do not write files, do not run commands. Use Read, Glob, Grep, WebFetch freely. Present a detailed plan and call ExitPlanMode when ready for user approval.', bypass: 'Full autonomy. Execute everything without asking for confirmation. The user trusts your judgment. Still use AskUserQuestion for genuine ambiguity, but skip all permission prompts.', auto: 'Infer appropriate permission level from context. Err on the side of asking for destructive or irreversible operations.', yolo: 'Maximum autonomy, minimum interruptions. Only stop for things that are literally impossible to proceed without human input.', } parts.push(permExplanations[opts.permissions] || permExplanations['default']) // ── 5. Behavioural rules ────────────────────────────────────────────────── parts.push(` ## Rules - Read files before modifying them. Never Edit a file you haven't Read. - Use Edit for incremental changes. Use Write only for new files or complete rewrites. - Run tests after making changes if a test suite exists. - When a Bash command fails, diagnose and fix — don't hand the error back to the user. - For multi-step work: create tasks upfront so the user sees the plan, then work through them. - Don't add comments that explain WHAT the code does (code is self-documenting). Only add comments that explain WHY when it's non-obvious. - Don't refactor unrelated code while fixing a bug. Stay on scope. - If you're about to do something irreversible (delete files, reset database, force push), call AskUserQuestion first. - When in doubt about what the user wants, AskUserQuestion. One clarifying question beats a wrong implementation.`) // ── 6. Environment context ──────────────────────────────────────────────── const env = opts.envContext if (env) { const envLines: string[] = ['\n## Current environment'] // Context window const ctxBar = buildBar(env.ctxPct) const resetStr = formatResetIn(env.fiveHourResetAt) envLines.push(`Context: ${ctxBar} ${env.ctxPct}% used (resets in ${resetStr})`) if (env.ctxPct >= 80) { envLines.push(' WARNING: context is nearly full. Prefer compact tool outputs. Consider /compact if the user asks.') } // Cost if (env.costUsd > 0) { envLines.push(`Cost this session: $${env.costUsd.toFixed(3)}`) } // Session turns if (env.sessionTurns > 0) { envLines.push(`Session turns so far: ${env.sessionTurns}`) } // Router if (env.routerReason) { envLines.push(`Router: selected ${opts.model} because — ${env.routerReason}`) } // MCP servers if (env.mcpServers.length > 0) { const connected = env.mcpServers.filter(s => s.status === 'connected') if (connected.length > 0) { envLines.push(`MCP servers available: ${connected.map(s => `${s.name} (${s.toolCount} tools)`).join(', ')}`) envLines.push(' These servers provide additional tools — use them when they\'re the right fit.') } } // Active background shells if (env.activeShells.length > 0) { envLines.push('Active background shells:') for (const sh of env.activeShells) { const age = sh.ageS < 60 ? `${sh.ageS}s` : `${Math.floor(sh.ageS / 60)}m ${sh.ageS % 60}s` envLines.push(` - ${sh.id} running "${sh.command}" (${age}) — use BashOutput("${sh.id}") to check output`) } } // Active monitors if (env.activeMonitors.length > 0) { envLines.push('Active monitors:') for (const m of env.activeMonitors) { const age = m.ageS < 60 ? `${m.ageS}s` : `${Math.floor(m.ageS / 60)}m` envLines.push(` - ${m.id} "${m.description}" (${age})`) } } parts.push(envLines.join('\n')) } // ── 7. Project context ──────────────────────────────────────────────────── parts.push(`\nWorking directory: ${opts.cwd}`) const git = getGitInfo(opts.cwd) if (git.branch) { parts.push(`Git: ${git.branch} branch${git.dirty ? ' (uncommitted changes)' : ''}`) } const memoryContent = loadProjectMemory(opts.cwd) if (memoryContent) { parts.push(`\n## Project memory\n${memoryContent}`) } // ── 8. Transplant context (compact summary from previous turns) ─────────── if (opts.appendSystemPrompt) { parts.push(`\n## Context from previous conversation\n${opts.appendSystemPrompt}`) } return parts.join('\n') } // ── Helpers ─────────────────────────────────────────────────────────────────── function buildBar(pct: number): string { const w = 8 const filled = Math.round((Math.min(pct, 100) / 100) * w) return '[' + '█'.repeat(filled) + '░'.repeat(w - filled) + ']' } function formatResetIn(resetAtMs: number): string { if (!resetAtMs) return '~5h (unknown)' const ms = resetAtMs - Date.now() if (ms <= 0) return 'now (resetting)' const h = Math.floor(ms / 3_600_000) const m = Math.floor((ms % 3_600_000) / 60_000) return h > 0 ? `${h}h ${m}m` : `${m}m` } function getGitInfo(cwd: string): { branch: string | null; dirty: boolean } { const now = Date.now() if (gitCache && gitCache.cwd === cwd && now - gitCache.at < GIT_TTL) { return { branch: gitCache.branch, dirty: gitCache.dirty } } let branch: string | null = null let dirty = false try { const headPath = findGitHead(cwd) if (headPath) { const content = fs.readFileSync(headPath, 'utf-8').trim() branch = content.startsWith('ref: refs/heads/') ? content.slice('ref: refs/heads/'.length) : content.slice(0, 8) // Quick dirty check via index const { execSync } = await_execSync() if (execSync) { try { const out = execSync('git status --porcelain', { cwd, timeout: 1000, encoding: 'utf-8' }) dirty = out.trim().length > 0 } catch { /* ignore */ } } } } catch { /* ignore */ } gitCache = { branch, dirty, cwd, at: now } return { branch, dirty } } // Lazy sync execSync to avoid top-level import in ESM function await_execSync(): { execSync: typeof import('node:child_process').execSync | null } { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const { execSync } = require('node:child_process') as typeof import('node:child_process') return { execSync } } catch { return { execSync: null } } } function findGitHead(cwd: string): string | null { let dir = cwd while (true) { const headPath = path.join(dir, '.git', 'HEAD') if (fs.existsSync(headPath)) return headPath const parent = path.dirname(dir) if (parent === dir) return null dir = parent } } function loadProjectMemory(cwd: string): string | null { const seen = new Set() const layers: Array<{ source: string; content: string }> = [] const home = process.env.HOME || process.env.USERPROFILE || '' if (home) { const userCandidates = [ path.join(home, '.squeezr-code', 'SQUEEZR.md'), path.join(home, '.squeezr-code', 'CLAUDE.md'), path.join(home, '.claude', 'CLAUDE.md'), ] for (const c of userCandidates) { if (fs.existsSync(c) && !seen.has(c)) { const content = readWithImports(c, seen) if (content) layers.push({ source: '~/' + path.relative(home, c).replace(/\\/g, '/'), content }) break } } } const projectFile = findUpwards(cwd, ['SQUEEZR.md', 'CLAUDE.md', '.sq.md', '.claude.md']) if (projectFile && !seen.has(projectFile)) { const content = readWithImports(projectFile, seen) if (content) layers.push({ source: path.basename(projectFile) + ' (project)', content }) } if (projectFile) { const projectDir = path.dirname(projectFile) if (projectDir !== cwd) { for (const c of ['SQUEEZR.md', 'CLAUDE.md'].map(n => path.join(cwd, n))) { if (fs.existsSync(c) && !seen.has(c)) { const content = readWithImports(c, seen) if (content) layers.push({ source: path.basename(c) + ' (cwd)', content }) break } } } } if (layers.length === 0) return null let combined = layers.map(l => `=== ${l.source} ===\n${l.content}`).join('\n\n') if (combined.length > 30_000) combined = combined.slice(0, 30_000) + '\n\n... (memory truncated to 30KB)' return combined } function findUpwards(start: string, fileNames: string[]): string | null { let dir = start while (true) { for (const n of fileNames) { const p = path.join(dir, n) if (fs.existsSync(p)) return p } const parent = path.dirname(dir) if (parent === dir) return null dir = parent } } function readWithImports(filePath: string, seen: Set): string | null { if (seen.has(filePath)) return null seen.add(filePath) try { let text = fs.readFileSync(filePath, 'utf-8') text = text.replace(/^@import\s+(\S+)\s*$/gm, (_m, importPath: string) => { const resolved = path.isAbsolute(importPath) ? importPath : path.join(path.dirname(filePath), importPath) const inner = readWithImports(resolved, seen) return inner ? `\n--- imported: ${importPath} ---\n${inner}\n--- end ${importPath} ---\n` : `` }) return text } catch { return null } }