Skip to content

Commit 17dc5eb

Browse files
committed
🤖 refactor: make /init a built-in agent skill
- Convert `/init` from a hardcoded slash command into a built-in agent skill. - Allow `/{skill-name}` invocations with no trailing message (send a tiny default message). - Update project “Initialize” banner to send `/init`. - Document built-in skill precedence + `/init` override behavior. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- <details> <summary>📋 Implementation Plan</summary> - Remove `/init` from the hardcoded slash-command registry + ChatInput branching. - Treat `/init` as a normal **built-in agent skill** (discovered via `agentSkills.list`). - Allow invoking a skill with **no trailing message** (e.g. `/init`) so it can fully replace “macro” slash commands. - **Registry**: `/init` is a hardcoded slash command (`initCommandDefinition`) in `src/browser/utils/slashCommands/registry.ts`, parsed as `{ type: "init" }`. - **UI special-case**: `src/browser/components/ChatInput/index.tsx` intercepts `parsed.type === "init"` and replaces the input text with `src/browser/assets/initMessage.txt` (`?raw`). - **Project banner**: `src/browser/components/ProjectPage.tsx` restores the same text and auto-sends it; it also flips project-scope agent mode to `exec`. - **Backend**: no special handling—`/init` ultimately becomes a normal user message containing a large prompt (currently wrapped in `<system>…</system>`). - `src/browser/components/ChatInput/index.tsx` - Imports `initMessage` from `@/browser/assets/initMessage.txt?raw`. - Two `/init` special-cases: `handleSend` checks `parsed?.type === "init"` in both the **creation** path (~line 1402) and **workspace** path (~line 1567). - Two “skill invocation requires trailing message” toasts: - **Creation** skill path: toast around ~line 1421. - **Workspace** skill path: toast around ~line 1496. - `src/browser/components/ProjectPage.tsx` - Imports `initMessage` from `@/browser/assets/initMessage.txt?raw` and uses it in the Agents init banner flow. - `src/browser/utils/slashCommands/registry.ts` - Defines `initCommandDefinition` and registers it. - `src/browser/utils/slashCommands/types.ts` - Includes `{ type: "init" }` in `ParsedCommand` union. - `src/browser/utils/slashCommands/parser.test.ts` - Contains tests asserting `/init` parses to `{ type: "init" }`. **Net LoC estimate (product code only): ~+30–80** Core idea: make `init` a built-in skill and invoke it like any other skill. To support `/init` with no extra text, we must avoid sending an empty message because `AgentSession.sendMessage` rejects empty messages. When the user invokes a skill with no trailing message, send a tiny default message (while still displaying the raw command via `muxMetadata.rawCommand`). Example: - Raw input: `/init` - Message sent to backend: `Run the "init" skill.` - `muxMetadata`: `{ type: "agent-skill", rawCommand: "/init", skillName: "init", scope: "built-in" }` This keeps the system general (no `/init` special casing) and satisfies the backend’s non-empty constraint. Skill resolution already supports overrides in this order: 1. **Project**: `.mux/skills/<name>/SKILL.md` 2. **Global**: `~/.mux/skills/<name>/SKILL.md` 3. **Built-in**: `src/node/builtinSkills/<name>.md` So yes—once `init` is shipped as a built-in skill, a user can override it by creating `~/.mux/skills/init/SKILL.md` (and a project can override that via `.mux/skills/init/SKILL.md`). <details> <summary>Tradeoff</summary> This changes `/init` from an editable “template prefill” into an immediate command. If we still want editability later, we can add a separate “insert skill into input” UI affordance, but that’s not required for this cleanup. </details> 1) **Allow skills to be sent without a message (creation + workspace ChatInput)** - In `src/browser/components/ChatInput/index.tsx`, update both “unknown slash command → agent skill” paths: - **Creation path** (~line 1421): remove the “Please add a message after /{skill}” toast + early return. - **Workspace path** (~line 1496): same. - If trailing text is empty, send a synthetic default message, e.g. ```ts const userText = afterPrefix.trimStart() || `Run the "${skillName}" skill.`; ``` - Keep setting `muxMetadata = { type: "agent-skill", rawCommand: messageText, ... }` so the UI shows the original `/skill` (not the synthetic message). 2) **Add built-in skill `init`** - Create `src/node/builtinSkills/init.md` (frontmatter: `name: init`, description matching current `/init`). - Move/copy the existing `src/browser/assets/initMessage.txt` content into the skill body (keep as-is initially for minimal behavior change). - Regenerate bundled skill content (`scripts/gen_builtin_skills.ts` → `builtInSkillContent.generated.ts`). - Notes / gotchas: - Frontmatter must start at the very top of the file (`---` as first bytes). - Keep skill content < 1MB. 3) **Remove `/init` as a hardcoded slash command + UI branch** - Delete `initCommandDefinition` from `src/browser/utils/slashCommands/registry.ts`. - Remove `{ type: "init" }` from `src/browser/utils/slashCommands/types.ts`. - Remove the `parsed.type === "init"` branches in `ChatInput` (both creation + workspace). - Update/remove `/init` parsing expectations in `src/browser/utils/slashCommands/parser.test.ts`. - Result: `/init` will be parsed as an unknown command and then resolved via `agentSkills.list` as a skill. 4) **Update the project “Initialize” banner to invoke the skill** - In `src/browser/components/ProjectPage.tsx`: - Replace `restoreText(initMessage)` with `restoreText("/init")` (and keep the auto-send behavior). - Keep the existing “switch project-scope mode to exec” behavior. 5) **Remove the old browser asset** - Delete `src/browser/assets/initMessage.txt` and remove its imports once no longer referenced. 6) **Docs** - Update docs to reflect that `/init` is now just the `init` skill (and can be overridden by users/projects via `~/.mux/skills/init/SKILL.md` / `.mux/skills/init/SKILL.md`). 7) **Validation checklist** - Typing `/init` and hitting send triggers the init skill (no “empty message” backend error; no “add a message” toast). - Project page “Initialize” banner still runs init automatically. - `/init` appears once in suggestions (as a skill, not a hardcoded command). - Other skills still work both with and without trailing messages. **Net LoC estimate (product code only): ~+20–60** Keep “prefill editable template” UX, but fetch the template body from `agentSkills.get("init")` instead of a browser asset. Tradeoff: still requires `/init`-specific behavior (or introducing a generalized “insert skill into input” feature). <details> <summary>Follow-up: reduce other “prompt template” slash commands</summary> Once skills can be invoked with no trailing message, we can eliminate other *macro-style* slash commands by applying the same pattern: - Move the instruction text into `src/node/builtinSkills/<name>.md`. - Remove the hardcoded command definition + `ChatInput` branch. - Let `/<name>` resolve via `agentSkills.list`. Keep hardcoded slash commands only for **UI/backend actions** (clear history, model/provider settings, MCP management, etc.). </details> </details> --- _Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: $30.06_ Change-Id: Iaf4c066db3fc75bf52993552b2d0d6ee9655bb28
1 parent 2d85001 commit 17dc5eb

7 files changed

Lines changed: 58 additions & 89 deletions

File tree

docs/agents/agent-skills.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ This keeps the system prompt small while still making skills discoverable.
1616

1717
## Where skills live
1818

19-
mux discovers skills from two roots:
19+
mux discovers skills from three roots:
2020

2121
- **Project-local**: `<projectRoot>/.mux/skills/<skill-name>/SKILL.md`
2222
- **Global**: `~/.mux/skills/<skill-name>/SKILL.md`
23+
- **Built-in**: shipped with mux
2324

24-
If a skill exists in both locations, **project-local overrides global**.
25+
If a skill exists in multiple locations, the precedence order is: **project-local > global > built-in**.
2526

2627
<Info>
2728
mux reads skills using the active workspace runtime. For SSH workspaces, skills are read from the
@@ -82,7 +83,8 @@ mux injects an `<agent-skills>` block into the system prompt listing the availab
8283

8384
You can apply a skill in two ways:
8485

85-
- **Explicit (slash command)**: type `/{skill-name} ...` in the chat input. mux will send your message normally, but inject the skill content into the system context for that send.
86+
- **Explicit (slash command)**: type `/{skill-name}` (optionally followed by a message: `/{skill-name} ...`) in the chat input. mux will send your message normally (or a small default message if you omit one), and inject the skill content into the system context for that send.
87+
- Example: `/init` runs the built-in `init` skill to bootstrap `AGENTS.md`. You can override it with `~/.mux/skills/init/SKILL.md` (or `.mux/skills/init/SKILL.md` for a single project).
8688
- Type `/` to see skills in the suggestions list.
8789
- **Agent-initiated (tool call)**: the agent can load skills on-demand.
8890

@@ -107,7 +109,7 @@ agent_skill_read_file({ name: "my-skill", filePath: "references/template.md" });
107109

108110
## Current limitations
109111

110-
- Slash command invocation supports only a single skill as the first token (for example `/{skill-name} ...`).
112+
- Slash command invocation supports only a single skill as the first token (for example `/{skill-name}` or `/{skill-name} ...`).
111113
- Skill bodies may be truncated when injected to avoid accidental mega-prompts.
112114
- `allowed-tools` is not enforced by mux (it is tolerated in frontmatter, but ignored).
113115

src/browser/components/ChatInput/index.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ import {
107107
} from "./draftImagesStorage";
108108
import { RecordingOverlay } from "./RecordingOverlay";
109109
import { ReviewBlockFromData } from "../shared/ReviewBlock";
110-
import initMessage from "@/browser/assets/initMessage.txt?raw";
111110

112111
// localStorage quotas are environment-dependent and relatively small.
113112
// Be conservative here so we can warn the user before writes start failing.
@@ -1395,16 +1394,9 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
13951394
let creationMessageTextForSend = messageText;
13961395
let creationOptionsOverride: Partial<SendMessageOptions> | undefined;
13971396

1398-
// Handle /init command in creation variant - populate input with init message
13991397
if (messageText.startsWith("/")) {
14001398
const parsed = parseCommand(messageText);
14011399

1402-
if (parsed?.type === "init") {
1403-
setInput(initMessage);
1404-
focusMessageInput();
1405-
return;
1406-
}
1407-
14081400
if (isUnknownSlashCommand(parsed)) {
14091401
const command = parsed.command;
14101402
const maybeSkill = agentSkillDescriptors.find((skill) => skill.name === command);
@@ -1414,14 +1406,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
14141406
const hasSeparator = afterPrefix.length === 0 || /^\s/.test(afterPrefix);
14151407

14161408
if (hasSeparator) {
1417-
const userText = afterPrefix.trimStart();
1418-
if (!userText) {
1419-
pushToast({
1420-
type: "error",
1421-
message: `Please add a message after ${prefix}`,
1422-
});
1423-
return;
1424-
}
1409+
const userText = afterPrefix.trimStart() || `Run the "${maybeSkill.name}" skill.`;
14251410

14261411
if (!api) {
14271412
pushToast({ type: "error", message: "Not connected to server" });
@@ -1489,14 +1474,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
14891474
const hasSeparator = afterPrefix.length === 0 || /^\s/.test(afterPrefix);
14901475

14911476
if (hasSeparator) {
1492-
const userText = afterPrefix.trimStart();
1493-
if (!userText) {
1494-
pushToast({
1495-
type: "error",
1496-
message: `Please add a message after ${prefix}`,
1497-
});
1498-
return;
1499-
}
1477+
const userText = afterPrefix.trimStart() || `Run the "${maybeSkill.name}" skill.`;
15001478

15011479
skillInvocation = { descriptor: maybeSkill, userText };
15021480
parsed = null;
@@ -1569,13 +1547,6 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
15691547
return;
15701548
}
15711549

1572-
// Handle /init command - populate input with init message
1573-
if (parsed.type === "init") {
1574-
setInput(initMessage);
1575-
focusMessageInput();
1576-
return;
1577-
}
1578-
15791550
// Handle other non-API commands (help, invalid args, etc)
15801551
const commandToast = createCommandToast(parsed);
15811552
if (commandToast) {

src/browser/components/ProjectPage.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { ConfigureProvidersPrompt } from "./ConfigureProvidersPrompt";
1616
import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
1717
import type { ProvidersConfigMap } from "@/common/orpc/types";
1818
import { AgentsInitBanner } from "./AgentsInitBanner";
19-
import initMessage from "@/browser/assets/initMessage.txt?raw";
2019
import { usePersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
2120
import {
2221
getAgentIdKey,
@@ -201,16 +200,16 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
201200
// Switch project-scope mode to exec.
202201
updatePersistedState(getAgentIdKey(getProjectScopeId(projectPath)), "exec");
203202

204-
// Prefill the AGENTS bootstrap prompt and start the creation chat.
203+
// Run the /init skill and start the creation chat.
205204
if (chatInputRef.current) {
206-
chatInputRef.current.restoreText(initMessage);
205+
chatInputRef.current.restoreText("/init");
207206
requestAnimationFrame(() => {
208207
void chatInputRef.current?.send();
209208
});
210209
} else {
211210
pendingAgentsInitSendRef.current = true;
212211
const pendingScopeId = getPendingScopeId(projectPath);
213-
updatePersistedState(getInputKey(pendingScopeId), initMessage);
212+
updatePersistedState(getInputKey(pendingScopeId), "/init");
214213
}
215214

216215
setShowAgentsInitNudge(false);
@@ -222,7 +221,7 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
222221
if (pendingAgentsInitSendRef.current) {
223222
pendingAgentsInitSendRef.current = false;
224223
didAutoFocusRef.current = true;
225-
api.restoreText(initMessage);
224+
api.restoreText("/init");
226225
requestAnimationFrame(() => {
227226
void api.send();
228227
});

src/browser/utils/slashCommands/parser.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,15 @@ describe("plan commands", () => {
228228
});
229229

230230
describe("init command", () => {
231-
it("should parse /init", () => {
232-
expectParse("/init", { type: "init" });
231+
it("should parse /init as unknown-command (handled as a skill invocation)", () => {
232+
expectParse("/init", {
233+
type: "unknown-command",
234+
command: "init",
235+
subcommand: undefined,
236+
});
233237
});
234238

235-
it("should reject /init with arguments", () => {
239+
it("should parse /init with arguments as unknown-command", () => {
236240
expectParse("/init extra", {
237241
type: "unknown-command",
238242
command: "init",

src/browser/utils/slashCommands/registry.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -447,23 +447,6 @@ const vimCommandDefinition: SlashCommandDefinition = {
447447
},
448448
};
449449

450-
const initCommandDefinition: SlashCommandDefinition = {
451-
key: "init",
452-
description: "Bootstrap an AGENTS.md file in a new or existing project",
453-
appendSpace: false,
454-
handler: ({ cleanRemainingTokens }): ParsedCommand => {
455-
if (cleanRemainingTokens.length > 0) {
456-
return {
457-
type: "unknown-command",
458-
command: "init",
459-
subcommand: cleanRemainingTokens[0],
460-
};
461-
}
462-
463-
return { type: "init" };
464-
},
465-
};
466-
467450
const planOpenCommandDefinition: SlashCommandDefinition = {
468451
key: "open",
469452
description: "Open plan in external editor",
@@ -715,7 +698,6 @@ export const SLASH_COMMAND_DEFINITIONS: readonly SlashCommandDefinition[] = [
715698
vimCommandDefinition,
716699
mcpCommandDefinition,
717700
idleCommandDefinition,
718-
initCommandDefinition,
719701
debugLlmRequestCommandDefinition,
720702
];
721703

src/browser/utils/slashCommands/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export type ParsedCommand =
3838
| { type: "plan-show" }
3939
| { type: "plan-open" }
4040
| { type: "debug-llm-request" }
41-
| { type: "init" }
4241
| { type: "unknown-command"; command: string; subcommand?: string }
4342
| { type: "idle-compaction"; hours: number | null }
4443
| null;
Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,71 @@
1+
---
2+
name: init
3+
description: Bootstrap an AGENTS.md file in a new or existing project
4+
---
5+
16
<system>
27
Use your tools to create or improve an AGENTS.md file in the root of the workspace which will serve as a contribution guide for AI agents.
38
If an AGENTS.md file already exists, focus on additive improvement (preserve intent and useful information; refine, extend, and reorganize as needed) rather than replacing it wholesale.
49
Inspect the workspace layout, code, documentation and git history to ensure correctness and accuracy.
510

611
Ensure the following preamble exists at the top of the file before any other sections. Do not include the surrounding code fence backticks; only include the text.
12+
713
```md
814
You are an experienced, pragmatic software engineering AI agent. Do not over-engineer a solution when a simple one is possible. Keep edits minimal. If you want an exception to ANY rule, you MUST stop and get permission first.
915
```
1016

1117
Recommended sections:
18+
1219
- Project Overview (mandatory)
13-
- Basic details about the project (e.g., high-level overview and goals).
14-
- Technology choices (e.g., languages, databases, frameworks, libraries, build tools).
20+
- Basic details about the project (e.g., high-level overview and goals).
21+
- Technology choices (e.g., languages, databases, frameworks, libraries, build tools).
1522
- Reference (mandatory)
16-
- List important code files.
17-
- List important directories and basic code structure tips.
18-
- Project architecture.
23+
- List important code files.
24+
- List important directories and basic code structure tips.
25+
- Project architecture.
1926
- Essential commands (mandatory)
20-
- build
21-
- format
22-
- lint
23-
- test
24-
- clean
25-
- development server
26-
- other *important* scripts (use `find -type f -name '*.sh'` or similar)
27+
- build
28+
- format
29+
- lint
30+
- test
31+
- clean
32+
- development server
33+
- other _important_ scripts (use `find -type f -name '*.sh'` or similar)
2734
- Patterns (optional)
28-
- List any important or uncommon patterns (compared to other similar codebases), with examples (e.g., how to authorize an HTTP request).
29-
- List any important workflows and their steps (e.g., how to make a database migration).
30-
- Testing patterns.
35+
- List any important or uncommon patterns (compared to other similar codebases), with examples (e.g., how to authorize an HTTP request).
36+
- List any important workflows and their steps (e.g., how to make a database migration).
37+
- Testing patterns.
3138
- Anti-patterns (optional)
32-
- Search git history and comments to find recurring mistakes or forbidden patterns.
33-
- List each pattern and its reason.
39+
- Search git history and comments to find recurring mistakes or forbidden patterns.
40+
- List each pattern and its reason.
3441
- Code style (optional)
35-
- Style guide to follow (with link).
42+
- Style guide to follow (with link).
3643
- Commit and Pull Request Guidelines (mandatory)
37-
- Required steps for validating changes before committing.
38-
- Commit message conventions (read `git log`, or use `type: message` by default).
39-
- Pull request description requirements.
44+
- Required steps for validating changes before committing.
45+
- Commit message conventions (read `git log`, or use `type: message` by default).
46+
- Pull request description requirements.
4047

4148
You can add other sections if they are necessary.
4249
If the information required for mandatory sections isn't available due to the workspace being empty or sparse, add TODO text in its place.
4350
Optional sections should be scrapped if the information is too thin.
4451

4552
Some investigation tips:
46-
- Read existing lint configs, tsconfig, and CI workflows to find any style or layout rules.
47-
- Search for "TODO", "HACK", "FIXME", "don't", "never", "always" in comments.
48-
- Examine test files for patterns.
49-
- Read PR templates and issue templates if they exist.
50-
- Check for existing CONTRIBUTING.md, CODE_OF_CONDUCT.md, or similar documentation files.
53+
54+
- Read existing lint configs, tsconfig, and CI workflows to find any style or layout rules.
55+
- Search for "TODO", "HACK", "FIXME", "don't", "never", "always" in comments.
56+
- Examine test files for patterns.
57+
- Read PR templates and issue templates if they exist.
58+
- Check for existing CONTRIBUTING.md, CODE_OF_CONDUCT.md, or similar documentation files.
5159

5260
Some writing tips:
61+
5362
- Each "do X" should have a corresponding "don't Y" where applicable.
5463
- Commands should be easily copy-pastable and tested.
5564
- Terms or phrases specific to this project should be explained on first use.
5665
- Anything that is against the norm should be explicitly highlighted and called out.
5766

5867
Above all things:
68+
5969
- The document must be clear and concise. Simple projects should need less than 400 words, but larger and more mature codebases will likely need 700+. Prioritize completeness over brevity.
6070
- Don't include useless fluff.
6171
- The document must be in Markdown format and use headings for structure.
@@ -64,14 +74,16 @@ Above all things:
6474
- Maintain a professional, instructional tone.
6575

6676
If the workspace is empty or sparse, ask the user for more information. Avoid hallucinating important decisions. You can provide suggestions to the user for language/technology/tool choices, but always respect the user's decision.
77+
6778
- Project description and goals.
6879
- Language(s).
6980
- Technologies (database?), frameworks, libraries.
7081
- Tools.
7182
- Any other questions as you deem necessary.
7283

7384
For empty or sparse workspaces ONLY, when finished writing/updating AGENTS.md, ask the user if they would like you to do the following:
85+
7486
- initialize git IF it's not already set up (e.g., `git init`, `git remote add`, etc.)
7587
- write a concise README.md file
7688
- generate the bare minimum project scaffolding (e.g., initializing the package manager, writing a minimal build tool config)
77-
</system>
89+
</system>

0 commit comments

Comments
 (0)