mini-coder 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +87 -48
  2. package/assets/icon-1-minimal.svg +31 -0
  3. package/assets/icon-2-dark-terminal.svg +48 -0
  4. package/assets/icon-3-gradient-modern.svg +45 -0
  5. package/assets/icon-4-filled-bold.svg +54 -0
  6. package/assets/icon-5-community-badge.svg +63 -0
  7. package/assets/preview-0-5-0.png +0 -0
  8. package/assets/preview.gif +0 -0
  9. package/bin/mc.ts +14 -0
  10. package/bun.lock +438 -0
  11. package/package.json +12 -29
  12. package/src/agent.ts +592 -0
  13. package/src/cli.ts +124 -0
  14. package/src/git.ts +164 -0
  15. package/src/headless.ts +140 -0
  16. package/src/index.ts +645 -0
  17. package/src/input.ts +155 -0
  18. package/src/paths.ts +37 -0
  19. package/src/plugins.ts +183 -0
  20. package/src/prompt.ts +294 -0
  21. package/src/session.ts +838 -0
  22. package/src/settings.ts +184 -0
  23. package/src/skills.ts +258 -0
  24. package/src/submit.ts +323 -0
  25. package/src/theme.ts +147 -0
  26. package/src/tools.ts +636 -0
  27. package/src/ui/agent.test.ts +49 -0
  28. package/src/ui/agent.ts +210 -0
  29. package/src/ui/commands.test.ts +610 -0
  30. package/src/ui/commands.ts +638 -0
  31. package/src/ui/conversation.test.ts +892 -0
  32. package/src/ui/conversation.ts +926 -0
  33. package/src/ui/help.test.ts +26 -0
  34. package/src/ui/help.ts +119 -0
  35. package/src/ui/input.test.ts +74 -0
  36. package/src/ui/input.ts +138 -0
  37. package/src/ui/overlay.test.ts +42 -0
  38. package/src/ui/overlay.ts +59 -0
  39. package/src/ui/status.test.ts +450 -0
  40. package/src/ui/status.ts +357 -0
  41. package/src/ui.ts +615 -0
  42. package/.claude/settings.local.json +0 -54
  43. package/.prettierignore +0 -7
  44. package/dist/mc-edit.js +0 -275
  45. package/dist/mc.js +0 -7355
  46. package/docs/KNOWN_ISSUES.md +0 -13
  47. package/docs/design-decisions.md +0 -31
  48. package/docs/mini-coder.1.md +0 -227
  49. package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +0 -61
  50. package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +0 -47
  51. package/lefthook.yml +0 -4
package/src/input.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * User input parsing.
3
+ *
4
+ * Pure logic for detecting slash commands, skill references, image paths,
5
+ * and plain text from raw user input. No UI or IO beyond `existsSync`
6
+ * for image path validation.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { existsSync } from "node:fs";
12
+ import { extname, isAbsolute, join } from "node:path";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** All recognized slash commands. */
19
+ export const COMMANDS = [
20
+ "model",
21
+ "session",
22
+ "new",
23
+ "fork",
24
+ "undo",
25
+ "reasoning",
26
+ "verbose",
27
+ "login",
28
+ "logout",
29
+ "help",
30
+ "effort",
31
+ ] as const;
32
+
33
+ /** A recognized slash command name. */
34
+ type Command = (typeof COMMANDS)[number];
35
+
36
+ const COMMAND_SET: ReadonlySet<string> = new Set(COMMANDS);
37
+
38
+ /** Image file extensions we recognize for embedding. */
39
+ const IMAGE_EXTENSIONS: ReadonlySet<string> = new Set([
40
+ ".png",
41
+ ".jpg",
42
+ ".jpeg",
43
+ ".gif",
44
+ ".webp",
45
+ ]);
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Types
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** Result of parsing user input. */
52
+ type ParsedInput =
53
+ | { type: "command"; command: Command; args: string }
54
+ | { type: "skill"; skillName: string; userText: string }
55
+ | { type: "image"; path: string }
56
+ | { type: "text"; text: string };
57
+
58
+ /** Options for input parsing. */
59
+ interface ParseInputOpts {
60
+ /** Whether the current model supports image input. */
61
+ supportsImages?: boolean;
62
+ /** Working directory for resolving relative image paths. */
63
+ cwd?: string;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Parsing
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function isCommand(value: string): value is Command {
71
+ return COMMAND_SET.has(value);
72
+ }
73
+
74
+ function parseSlashInput(trimmed: string): ParsedInput | null {
75
+ const skillMatch = trimmed.match(/^\/skill:(\S+)(?:\s+(.*))?$/s);
76
+ if (skillMatch?.[1]) {
77
+ return {
78
+ type: "skill",
79
+ skillName: skillMatch[1],
80
+ userText: skillMatch[2]?.trim() ?? "",
81
+ };
82
+ }
83
+
84
+ const commandMatch = trimmed.match(/^\/(\S+)(?:\s+(.*))?$/s);
85
+ const command = commandMatch?.[1];
86
+ if (!command || !isCommand(command)) {
87
+ return null;
88
+ }
89
+
90
+ return {
91
+ type: "command",
92
+ command,
93
+ args: commandMatch[2]?.trim() ?? "",
94
+ };
95
+ }
96
+
97
+ function resolveImagePath(input: string, cwd?: string): string {
98
+ if (isAbsolute(input) || !cwd) {
99
+ return input;
100
+ }
101
+ return join(cwd, input);
102
+ }
103
+
104
+ function parseImageInput(
105
+ trimmed: string,
106
+ opts?: ParseInputOpts,
107
+ ): Extract<ParsedInput, { type: "image" }> | null {
108
+ if (!opts?.supportsImages) {
109
+ return null;
110
+ }
111
+ if (!IMAGE_EXTENSIONS.has(extname(trimmed).toLowerCase())) {
112
+ return null;
113
+ }
114
+
115
+ const resolvedPath = resolveImagePath(trimmed, opts.cwd);
116
+ if (!existsSync(resolvedPath)) {
117
+ return null;
118
+ }
119
+
120
+ return { type: "image", path: resolvedPath };
121
+ }
122
+
123
+ /**
124
+ * Parse raw user input into a structured result.
125
+ *
126
+ * Priority order:
127
+ * 1. Slash commands (`/model`, `/help`, etc.)
128
+ * 2. Skill references (`/skill:name rest of message`)
129
+ * 3. Image paths (entire input is an existing image file)
130
+ * 4. Plain text
131
+ *
132
+ * @param raw - The raw input string from the user.
133
+ * @param opts - Optional parsing context (image support, cwd).
134
+ * @returns A {@link ParsedInput} describing what the input represents.
135
+ */
136
+ export function parseInput(raw: string, opts?: ParseInputOpts): ParsedInput {
137
+ const trimmed = raw.trim();
138
+ if (trimmed.length === 0) {
139
+ return { type: "text", text: "" };
140
+ }
141
+
142
+ if (trimmed[0] === "/") {
143
+ const slashInput = parseSlashInput(trimmed);
144
+ if (slashInput) {
145
+ return slashInput;
146
+ }
147
+ }
148
+
149
+ const imageInput = parseImageInput(trimmed, opts);
150
+ if (imageInput) {
151
+ return imageInput;
152
+ }
153
+
154
+ return { type: "text", text: trimmed };
155
+ }
package/src/paths.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Filesystem path normalization helpers.
3
+ *
4
+ * Provides a small shared policy for path identity across the app:
5
+ * when paths are used for comparison or persistence, we canonicalize them
6
+ * to an absolute, symlink-resolved spelling.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { realpathSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+
14
+ /**
15
+ * Return the canonical absolute path for an existing filesystem entry.
16
+ *
17
+ * Canonicalization resolves `.`/`..` segments and follows symlinks,
18
+ * producing a stable spelling suitable for path equality checks and
19
+ * persistence keys.
20
+ *
21
+ * @param path - An existing filesystem path.
22
+ * @returns The canonical absolute path.
23
+ */
24
+ export function canonicalizePath(path: string): string {
25
+ return realpathSync(resolve(path));
26
+ }
27
+
28
+ /**
29
+ * Check whether two paths refer to the same existing filesystem entry.
30
+ *
31
+ * @param a - The first path to compare.
32
+ * @param b - The second path to compare.
33
+ * @returns `true` when both paths resolve to the same canonical location.
34
+ */
35
+ export function isSamePath(a: string, b: string): boolean {
36
+ return canonicalizePath(a) === canonicalizePath(b);
37
+ }
package/src/plugins.ts ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Plugin loader and lifecycle management.
3
+ *
4
+ * Plugins extend mini-coder with additional tools and system prompt context.
5
+ * They are declared in a config file and loaded as modules at startup.
6
+ * Each plugin's `init` is called once, and `destroy` (if present) is called
7
+ * on shutdown.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import type { Message, Tool } from "@mariozechner/pi-ai";
15
+ import type { ToolHandler } from "./agent.ts";
16
+ import type { Theme } from "./theme.ts";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Context provided to plugins during initialization.
24
+ *
25
+ * Gives plugins read-only access to the agent's environment without
26
+ * exposing internal implementation details.
27
+ */
28
+ export interface AgentContext {
29
+ /** The working directory. */
30
+ cwd: string;
31
+ /** Read-only access to the current session's messages. */
32
+ messages: readonly Message[];
33
+ /** The app data directory (`~/.config/mini-coder/`). */
34
+ dataDir: string;
35
+ }
36
+
37
+ /**
38
+ * Result returned by a plugin's `init` function.
39
+ *
40
+ * Contains any additional tools the agent should register and/or
41
+ * context to append to the system prompt.
42
+ */
43
+ export interface PluginResult {
44
+ /** Additional tool definitions to register with the model. */
45
+ tools?: Tool[];
46
+ /** Tool name → handler map for the tools above. */
47
+ toolHandlers?: Map<string, ToolHandler>;
48
+ /** Additional context to append to the system prompt. */
49
+ systemPromptSuffix?: string;
50
+ /** Partial theme override — merged on top of the default theme. */
51
+ theme?: Partial<Theme>;
52
+ }
53
+
54
+ /**
55
+ * The interface a plugin module must implement.
56
+ *
57
+ * A plugin is a module that exports a conforming object. It is loaded
58
+ * dynamically from a path or package name declared in the config file.
59
+ */
60
+ export interface Plugin {
61
+ /** Human-readable plugin name. */
62
+ name: string;
63
+ /** Brief description of what the plugin provides. */
64
+ description: string;
65
+ /** Called once at startup. Returns tools to register and/or context to add. */
66
+ init(
67
+ agent: AgentContext,
68
+ config?: Record<string, unknown>,
69
+ ): Promise<PluginResult>;
70
+ /** Called on shutdown for cleanup. */
71
+ destroy?(): Promise<void>;
72
+ }
73
+
74
+ /** A single entry in the plugins config file. */
75
+ export interface PluginEntry {
76
+ /** Plugin name (for display and error messages). */
77
+ name: string;
78
+ /** Module path or package name to import. */
79
+ module: string;
80
+ /** Optional configuration passed to the plugin's `init`. */
81
+ config?: Record<string, unknown>;
82
+ }
83
+
84
+ /** A loaded and initialized plugin with its result. */
85
+ export interface LoadedPlugin {
86
+ /** The plugin entry from config. */
87
+ entry: PluginEntry;
88
+ /** The plugin module instance. */
89
+ plugin: Plugin;
90
+ /** The result from calling `init`. */
91
+ result: PluginResult;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Config loading
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Load plugin entries from the config file.
100
+ *
101
+ * Reads and parses the plugins config file. Returns an empty array if
102
+ * the file does not exist or contains no plugins.
103
+ *
104
+ * @param configPath - Path to the plugins config file (e.g. `~/.config/mini-coder/plugins.json`).
105
+ * @returns Array of {@link PluginEntry} records.
106
+ */
107
+ export function loadPluginConfig(configPath: string): PluginEntry[] {
108
+ if (!existsSync(configPath)) return [];
109
+
110
+ const raw = readFileSync(configPath, "utf-8");
111
+ const parsed = JSON.parse(raw) as { plugins?: PluginEntry[] };
112
+ return parsed.plugins ?? [];
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Plugin lifecycle
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Load and initialize all plugins from config entries.
121
+ *
122
+ * Imports each plugin module, calls its `init` with the agent context,
123
+ * and collects the results. Plugins that fail to load or initialize are
124
+ * skipped with a warning (logged via `onError`).
125
+ *
126
+ * @param entries - Plugin entries from the config file.
127
+ * @param context - The agent context to pass to each plugin.
128
+ * @param onError - Callback for plugin load/init errors.
129
+ * @returns Array of successfully loaded plugins.
130
+ */
131
+ export async function initPlugins(
132
+ entries: PluginEntry[],
133
+ context: AgentContext,
134
+ onError?: (entry: PluginEntry, error: Error) => void,
135
+ ): Promise<LoadedPlugin[]> {
136
+ const loaded: LoadedPlugin[] = [];
137
+
138
+ for (const entry of entries) {
139
+ try {
140
+ const modulePath = resolve(entry.module);
141
+ const mod = (await import(modulePath)) as { default?: Plugin } & Plugin;
142
+ const plugin = mod.default ?? mod;
143
+
144
+ if (typeof plugin.init !== "function") {
145
+ throw new Error(
146
+ `Plugin "${entry.name}" does not export an init function`,
147
+ );
148
+ }
149
+
150
+ const result = await plugin.init(context, entry.config);
151
+ loaded.push({ entry, plugin, result });
152
+ } catch (err) {
153
+ onError?.(entry, err instanceof Error ? err : new Error(String(err)));
154
+ }
155
+ }
156
+
157
+ return loaded;
158
+ }
159
+
160
+ /**
161
+ * Destroy all loaded plugins.
162
+ *
163
+ * Calls `destroy` on each plugin that implements it. Errors during
164
+ * destruction are passed to `onError` — destruction continues for
165
+ * remaining plugins regardless.
166
+ *
167
+ * @param plugins - The loaded plugins to destroy.
168
+ * @param onError - Callback for destruction errors.
169
+ */
170
+ export async function destroyPlugins(
171
+ plugins: LoadedPlugin[],
172
+ onError?: (entry: PluginEntry, error: Error) => void,
173
+ ): Promise<void> {
174
+ for (const { entry, plugin } of plugins) {
175
+ if (typeof plugin.destroy === "function") {
176
+ try {
177
+ await plugin.destroy();
178
+ } catch (err) {
179
+ onError?.(entry, err instanceof Error ? err : new Error(String(err)));
180
+ }
181
+ }
182
+ }
183
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * System prompt construction.
3
+ *
4
+ * Assembles the full system prompt from static base instructions and
5
+ * dynamic context: AGENTS.md files, skill catalog, plugin suffixes,
6
+ * and a session footer with date, CWD, and git state.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { dirname, join, relative } from "node:path";
13
+ import type { GitState } from "./git.ts";
14
+ import { canonicalizePath } from "./paths.ts";
15
+ import { buildSkillCatalog, type Skill } from "./skills.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** A discovered AGENTS.md file with its content. */
22
+ export interface AgentsMdFile {
23
+ /** Absolute path to the file. */
24
+ path: string;
25
+ /** Raw file content. */
26
+ content: string;
27
+ }
28
+
29
+ /** Options for building the system prompt. */
30
+ interface BuildSystemPromptOpts {
31
+ /** Current working directory. */
32
+ cwd: string;
33
+ /** Current date string (YYYY-MM-DD). */
34
+ date: string;
35
+ /** Git repository state, or `null`/`undefined` if not in a repo. */
36
+ git?: GitState | null;
37
+ /** Discovered AGENTS.md files, ordered root-to-leaf. */
38
+ agentsMd?: AgentsMdFile[];
39
+ /** Discovered agent skills. */
40
+ skills?: Skill[];
41
+ /** Plugin system prompt suffixes. */
42
+ pluginSuffixes?: string[];
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // AGENTS.md discovery
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /** File name to look for during the AGENTS.md walk. */
50
+ const AGENT_FILENAME = "AGENTS.md";
51
+
52
+ /** Resolve the AGENTS.md scan root from git/home/env inputs. */
53
+ export function resolveAgentsScanRoot(
54
+ _cwd: string,
55
+ gitRoot: string | null,
56
+ homeDir: string,
57
+ agentsRootEnv = process.env.MC_AGENTS_ROOT,
58
+ ): string {
59
+ if (gitRoot) {
60
+ return canonicalizePath(gitRoot);
61
+ }
62
+ if (agentsRootEnv === "/") {
63
+ return canonicalizePath("/");
64
+ }
65
+ return canonicalizePath(homeDir);
66
+ }
67
+
68
+ function isWithinScanRoot(path: string, scanRoot: string): boolean {
69
+ const relativePath = relative(scanRoot, path);
70
+ return (
71
+ relativePath === "" ||
72
+ (!relativePath.startsWith("..") && relativePath !== "..")
73
+ );
74
+ }
75
+
76
+ function collectAgentsSearchDirs(start: string, root: string): string[] {
77
+ if (!isWithinScanRoot(start, root)) {
78
+ return [start];
79
+ }
80
+
81
+ const dirs: string[] = [];
82
+ let current = start;
83
+ while (true) {
84
+ dirs.push(current);
85
+ if (current === root) {
86
+ return dirs.reverse();
87
+ }
88
+
89
+ const parent = dirname(current);
90
+ if (parent === current) {
91
+ return dirs.reverse();
92
+ }
93
+ current = parent;
94
+ }
95
+ }
96
+
97
+ function readAgentsMdFile(dir: string): AgentsMdFile | null {
98
+ const filePath = join(dir, AGENT_FILENAME);
99
+ if (!existsSync(filePath)) {
100
+ return null;
101
+ }
102
+ return {
103
+ path: filePath,
104
+ content: readFileSync(filePath, "utf-8"),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Walk from `cwd` up to `scanRoot`, collecting AGENTS.md files.
110
+ *
111
+ * Also checks `globalAgentsDir` for global agent instructions when provided.
112
+ * Results are ordered root-to-leaf (general → specific), with global
113
+ * instructions first when present.
114
+ *
115
+ * @param cwd - Starting directory for the walk.
116
+ * @param scanRoot - Uppermost directory to include in the walk.
117
+ * @param globalAgentsDir - Optional directory for global agent instructions (e.g. `~/.agents/`).
118
+ * @returns Array of {@link AgentsMdFile} records, ordered general → specific.
119
+ */
120
+ export function discoverAgentsMd(
121
+ cwd: string,
122
+ scanRoot: string,
123
+ globalAgentsDir?: string,
124
+ ): AgentsMdFile[] {
125
+ const root = canonicalizePath(scanRoot);
126
+ const start = canonicalizePath(cwd);
127
+ const files: AgentsMdFile[] = [];
128
+
129
+ if (globalAgentsDir) {
130
+ const globalFile = readAgentsMdFile(globalAgentsDir);
131
+ if (globalFile) {
132
+ files.push(globalFile);
133
+ }
134
+ }
135
+
136
+ for (const dir of collectAgentsSearchDirs(start, root)) {
137
+ const agentsFile = readAgentsMdFile(dir);
138
+ if (agentsFile) {
139
+ files.push(agentsFile);
140
+ }
141
+ }
142
+
143
+ return files;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Git line formatting
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Format a git state snapshot into a single-line string for the session footer.
152
+ *
153
+ * Fields are omitted when their values are zero. The git line format:
154
+ * `Git: branch main | 3 staged, 1 modified, 2 untracked | +5 −2 vs origin/main`
155
+ *
156
+ * @param state - The git state to format.
157
+ * @returns Formatted git status line.
158
+ */
159
+ export function formatGitLine(state: GitState): string {
160
+ const parts: string[] = [`Git: branch ${state.branch}`];
161
+
162
+ // Working tree counts
163
+ const counts: string[] = [];
164
+ if (state.staged > 0) counts.push(`${state.staged} staged`);
165
+ if (state.modified > 0) counts.push(`${state.modified} modified`);
166
+ if (state.untracked > 0) counts.push(`${state.untracked} untracked`);
167
+ if (counts.length > 0) parts.push(counts.join(", "));
168
+
169
+ // Ahead/behind
170
+ if (state.ahead > 0 || state.behind > 0) {
171
+ const ab: string[] = [];
172
+ if (state.ahead > 0) ab.push(`+${state.ahead}`);
173
+ if (state.behind > 0) ab.push(`\u2212${state.behind}`);
174
+ parts.push(`${ab.join(" ")} vs origin/${state.branch}`);
175
+ }
176
+
177
+ return parts.join(" | ");
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Base instructions
182
+ // ---------------------------------------------------------------------------
183
+
184
+ const BASE_INSTRUCTIONS = `You are mini-coder, a coding agent running in the user's terminal.
185
+
186
+ # Role
187
+
188
+ You are an autonomous, senior-level coding assistant. When the user gives a direction, proactively gather context, plan with the user, implement, and verify. Bias toward action: use planning first to clear any assumptions with the user, then implement the plan. Deliver working code, unless you are genuinely blocked.
189
+
190
+ # Tools
191
+
192
+ You have these core tools:
193
+
194
+ - \`shell\` — run commands in the user's shell. Use this to explore the codebase (rg, find, ls, cat), run tests, build, git, and any other command. Prefer \`rg\` over \`grep\` for speed.
195
+ - \`edit\` — make exact-text replacements in files. Provide the file path, the exact text to find, and the replacement text. The old text must match exactly one location in the file. To create a new file, use an empty old text and the full file content as new text.
196
+
197
+ You may also have additional tools provided by plugins. Use them when they match the task.
198
+
199
+ Workflow: **inspect with shell → mutate with edit → verify with shell**.
200
+
201
+ # Code quality
202
+
203
+ - Conform to the codebase's existing conventions: patterns, naming, formatting, language idioms.
204
+ - Write correct, clear, minimal code. Don't over-engineer, don't add abstractions for hypothetical futures.
205
+ - Reuse before creating. Search for existing helpers before writing new ones.
206
+ - Tight error handling: no broad try/catch, no silent failures, no swallowed errors.
207
+ - Keep type safety. Avoid \`any\` casts. Use proper types and guards.
208
+ - Only add comments where the logic isn't self-evident.
209
+
210
+ # Editing discipline
211
+
212
+ - Read enough context before editing. Batch logical changes together rather than making many small edits.
213
+ - Never revert changes you didn't make unless explicitly asked.
214
+ - Never use destructive git commands (reset --hard, checkout --, clean -fd) unless the user requests it.
215
+ - Default to ASCII. Only use non-ASCII characters when the file already uses them or there's clear justification.
216
+
217
+ # Exploring the codebase
218
+
219
+ - Think first: before any tool call, decide all files and information you need.
220
+ - Batch reads: if you need multiple files, read them together in parallel rather than one at a time.
221
+ - Only make sequential calls when a later call genuinely depends on an earlier result.
222
+
223
+ # Communication
224
+
225
+ - Be concise. Friendly coding teammate tone.
226
+ - After making changes: lead with a quick explanation of what changed and why, then suggest logical next steps if any.
227
+ - Don't dump large file contents you've written — reference file paths.
228
+ - When suggesting multiple options, use numbered lists so the user can reply with a number.
229
+ - If asked for a review, focus on bugs, risks, regressions, and missing tests. Findings first, ordered by severity.
230
+
231
+ # Persistence
232
+
233
+ - Carry work through to completion within the current turn. Don't stop at analysis or partial fixes.
234
+ - If you encounter an error, diagnose and fix it rather than reporting it and stopping.
235
+ - Avoid excessive looping: if you're re-reading or re-editing the same files without progress, stop and ask the user.`;
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // System prompt assembly
239
+ // ---------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Build the full system prompt.
243
+ *
244
+ * Assembly order:
245
+ * 1. Base instructions (static)
246
+ * 2. AGENTS.md content (project-specific)
247
+ * 3. Skills catalog (XML)
248
+ * 4. Plugin suffixes
249
+ * 5. Session footer (date, CWD, git)
250
+ *
251
+ * @param opts - Prompt construction options.
252
+ * @returns The assembled system prompt string.
253
+ */
254
+ export function buildSystemPrompt(opts: BuildSystemPromptOpts): string {
255
+ const sections: string[] = [BASE_INSTRUCTIONS];
256
+
257
+ // 2. AGENTS.md content
258
+ if (opts.agentsMd && opts.agentsMd.length > 0) {
259
+ const agentsSection = [
260
+ "\n# Project Context\n",
261
+ "Project-specific instructions and guidelines:\n",
262
+ ];
263
+ for (const file of opts.agentsMd) {
264
+ agentsSection.push(`## ${file.path}\n`);
265
+ agentsSection.push(file.content);
266
+ agentsSection.push("");
267
+ }
268
+ sections.push(agentsSection.join("\n"));
269
+ }
270
+
271
+ // 3. Skills catalog
272
+ if (opts.skills && opts.skills.length > 0) {
273
+ const catalog = buildSkillCatalog(opts.skills);
274
+ if (catalog) sections.push(catalog);
275
+ }
276
+
277
+ // 4. Plugin suffixes
278
+ if (opts.pluginSuffixes) {
279
+ for (const suffix of opts.pluginSuffixes) {
280
+ sections.push(suffix);
281
+ }
282
+ }
283
+
284
+ // 5. Session footer
285
+ const footer: string[] = [];
286
+ footer.push(`Current date: ${opts.date}`);
287
+ footer.push(`Current working directory: ${opts.cwd}`);
288
+ if (opts.git) {
289
+ footer.push(formatGitLine(opts.git));
290
+ }
291
+ sections.push(footer.join("\n"));
292
+
293
+ return sections.join("\n");
294
+ }