Skip to content

Commit 14230e7

Browse files
authored
newrule (RooCodeInc#3180)
* base * words * changeset
1 parent 4b697d8 commit 14230e7

File tree

8 files changed

+172
-14
lines changed

8 files changed

+172
-14
lines changed

.changeset/rare-ladybugs-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
add newrule slash command

src/core/assistant-message/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const toolUseNames = [
2525
"attempt_completion",
2626
"new_task",
2727
"condense",
28+
"new_rule",
2829
] as const
2930

3031
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)

src/core/assistant-message/parse-assistant-message.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export function parseAssistantMessage(assistantMessage: string) {
5555

5656
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
5757
const contentParamName: ToolParamName = "content"
58-
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
58+
if (
59+
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
60+
accumulator.endsWith(`</${contentParamName}>`)
61+
) {
5962
const toolContent = accumulator.slice(currentToolUseStartIndex)
6063
const contentStartTag = `<${contentParamName}>`
6164
const contentEndTag = `</${contentParamName}>`

src/core/context/instructions/user-instructions/cline-rules.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,45 @@ import { ClineRulesToggles } from "@shared/cline-rules"
77
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
88
import * as vscode from "vscode"
99

10+
/**
11+
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
12+
* Doesn't do anything if .clinerules dir already exists or doesn't exist
13+
* Returns whether there are any uncaught errors
14+
*/
15+
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
16+
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
17+
const defaultRuleFilename = "default-rules.md"
18+
19+
try {
20+
const exists = await fileExistsAtPath(clinerulePath)
21+
22+
if (exists && !(await isDirectory(clinerulePath))) {
23+
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
24+
const content = await fs.readFile(clinerulePath, "utf8")
25+
const tempPath = clinerulePath + ".bak"
26+
await fs.rename(clinerulePath, tempPath) // create backup
27+
try {
28+
await fs.mkdir(clinerulePath, { recursive: true })
29+
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
30+
await fs.unlink(tempPath).catch(() => {}) // delete backup
31+
32+
return false // conversion successful with no errors
33+
} catch (conversionError) {
34+
// attempt to restore backup on conversion failure
35+
try {
36+
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
37+
await fs.rename(tempPath, clinerulePath) // restore backup
38+
} catch (restoreError) {}
39+
return true // in either case here we consider this an error
40+
}
41+
}
42+
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
43+
return false
44+
} catch (error) {
45+
return true
46+
}
47+
}
48+
1049
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
1150
if (await fileExistsAtPath(globalClineRulesFilePath)) {
1251
if (await isDirectory(globalClineRulesFilePath)) {

src/core/prompts/commands.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,61 @@ Example:
8787
8888
</explicit_instructions>\n
8989
`
90+
91+
export const newRuleToolResponse = () =>
92+
`<explicit_instructions type="new_rule">
93+
The user has explicitly asked you to help them create a new Cline rule file inside the .clinerules top-level directory based on the conversation up to this point in time. The user may have provided instructions or additional information for you to consider when creating the new Cline rule.
94+
When creating a new Cline rule file, you should NOT overwrite or alter an existing Cline rule file. To create the Cline rule file you MUST use the new_rule tool. The new_rule tool can be used in either of the PLAN or ACT modes.
95+
96+
The new_rule tool is defined below:
97+
98+
Description:
99+
Your task is to create a new Cline rule file which includes guidelines on how to approach developing code in tandem with the user, which can be either project specific or cover more global rules. This includes but is not limited to: desired conversational style, favorite project dependencies, coding styles, naming conventions, architectural choices, ui/ux preferences, etc.
100+
The Cline rule file must be formatted as markdown and be a '.md' file. The name of the file you generate must be as succinct as possible and be encompassing the main overarching concept of the rules you added to the file (e.g., 'memory-bank.md' or 'project-overview.md').
101+
102+
Parameters:
103+
- Path: (required) The path of the file to write to (relative to the current working directory). This will be the Cline rule file you create, and it must be placed inside the .clinerules top-level directory (create this if it doesn't exist). The filename created CANNOT be "default-clineignore.md". For filenames, use hyphens ("-") instead of underscores ("_") to separate words.
104+
- Content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. The content for the Cline rule file MUST be created according to the following instructions:
105+
1. Format the Cline rule file to have distinct guideline sections, each with their own markdown heading, starting with "## Brief overview". Under each of these headings, include bullet points fully fleshing out the details, with examples and/or trigger cases ONLY when applicable.
106+
2. These guidelines can be specific to the task(s) or project worked on thus far, or cover more high-level concepts. Guidelines can include coding conventions, general design patterns, preferred tech stack including favorite libraries and language, communication style with Cline (verbose vs concise), prompting strategies, naming conventions, testing strategies, comment verbosity, time spent on architecting prior to development, and other preferences.
107+
3. When creating guidelines, you should not invent preferences or make assumptions based on what you think a typical user might want. These should be specific to the conversation you had with the user. Your guidelines / rules should not be overly verbose.
108+
4. Your guidelines should NOT be a recollection of the conversation up to this point in time, meaning you should NOT be including arbitrary details of the conversation.
109+
110+
Usage:
111+
<new_rule>
112+
<path>.clinerules/{file name}.md</path>
113+
<content>Cline rule file content here</content>
114+
</new_rule>
115+
116+
Example:
117+
<new_rule>
118+
<path>.clinerules/project-preferences.md</path>
119+
<content>
120+
## Brief overview
121+
[Brief description of the rules, including if this set of guidelines is project-specific or global]
122+
123+
## Communication style
124+
- [Description, rule, preference, instruction]
125+
- [...]
126+
127+
## Development workflow
128+
- [Description, rule, preference, instruction]
129+
- [...]
130+
131+
## Coding best practices
132+
- [Description, rule, preference, instruction]
133+
- [...]
134+
135+
## Project context
136+
- [Description, rule, preference, instruction]
137+
- [...]
138+
139+
## Other guidelines
140+
- [Description, rule, preference, instruction]
141+
- [...]
142+
</content>
143+
</new_rule>
144+
145+
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
146+
</explicit_instructions>\n
147+
`

src/core/slash-commands/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { newTaskToolResponse, condenseToolResponse } from "../prompts/commands"
1+
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
22

33
/**
44
* Processes text for slash commands and transforms them with appropriate instructions
55
* This is called after parseMentions() to process any slash commands in the user's message
66
*/
7-
export function parseSlashCommands(text: string): string {
8-
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact"]
7+
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
8+
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
99

1010
const commandReplacements: Record<string, string> = {
1111
newtask: newTaskToolResponse(),
1212
smol: condenseToolResponse(),
1313
compact: condenseToolResponse(),
14+
newrule: newRuleToolResponse(),
1415
}
1516

1617
// this currently allows matching prepended whitespace prior to /slash-command
@@ -47,11 +48,11 @@ export function parseSlashCommands(text: string): string {
4748
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
4849
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
4950

50-
return processedText
51+
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
5152
}
5253
}
5354
}
5455

5556
// if no supported commands are found, return the original text
56-
return text
57+
return { processedText: text, needsClinerulesFileCheck: false }
5758
}

src/core/task/index.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {
8686
getGlobalClineRules,
8787
getLocalClineRules,
8888
refreshClineRulesToggles,
89+
ensureLocalClinerulesDirExists,
8990
} from "@core/context/instructions/user-instructions/cline-rules"
9091
import { getGlobalState } from "@core/storage/state"
9192
import { parseSlashCommands } from "@core/slash-commands"
@@ -1361,6 +1362,7 @@ export class Task {
13611362
this.autoApprovalSettings.actions.readFiles,
13621363
this.autoApprovalSettings.actions.readFilesExternally ?? false,
13631364
]
1365+
case "new_rule":
13641366
case "write_to_file":
13651367
case "replace_in_file":
13661368
return [
@@ -1686,6 +1688,8 @@ export class Task {
16861688
return `[${block.name} for creating a new task]`
16871689
case "condense":
16881690
return `[${block.name}]`
1691+
case "new_rule":
1692+
return `[${block.name} for '${block.params.path}']`
16891693
}
16901694
}
16911695

@@ -1825,6 +1829,7 @@ export class Task {
18251829
}
18261830

18271831
switch (block.name) {
1832+
case "new_rule":
18281833
case "write_to_file":
18291834
case "replace_in_file": {
18301835
const relPath: string | undefined = block.params.path
@@ -1970,6 +1975,13 @@ export class Task {
19701975

19711976
break
19721977
}
1978+
if (block.name === "new_rule" && !content) {
1979+
this.consecutiveMistakeCount++
1980+
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
1981+
await this.diffViewProvider.reset()
1982+
1983+
break
1984+
}
19731985

19741986
this.consecutiveMistakeCount = 0
19751987

@@ -3482,7 +3494,16 @@ export class Task {
34823494
}
34833495
}
34843496

3485-
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
3497+
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
3498+
3499+
// error handling if the user uses the /newrule command & their .clinerules is a file, for file read operations didnt work properly
3500+
if (clinerulesError === true) {
3501+
await this.say(
3502+
"error",
3503+
"Issue with processing the /newrule command. Double check that, if '.clinerules' already exists, it's a directory and not a file. Otherwise there was an issue referencing this file/directory.",
3504+
)
3505+
}
3506+
34863507
userContent = parsedUserContent
34873508
// add environment details as its own text block, separate from tool results
34883509
userContent.push({ type: "text", text: environmentDetails })
@@ -3767,11 +3788,14 @@ export class Task {
37673788
}
37683789
}
37693790

3770-
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
3771-
return await Promise.all([
3791+
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
3792+
// Track if we need to check clinerulesFile
3793+
let needsClinerulesFileCheck = false
3794+
3795+
const processUserContent = async () => {
37723796
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
37733797
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
3774-
Promise.all(
3798+
return await Promise.all(
37753799
userContent.map(async (block) => {
37763800
if (block.type === "text") {
37773801
// We need to ensure any user generated content is wrapped in one of these tags so that we know to parse mentions
@@ -3782,22 +3806,45 @@ export class Task {
37823806
block.text.includes("<task>") ||
37833807
block.text.includes("<user_message>")
37843808
) {
3785-
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
3809+
const parsedText = await parseMentions(
3810+
block.text,
3811+
cwd,
3812+
this.urlContentFetcher,
3813+
this.fileContextTracker,
3814+
)
37863815

37873816
// when parsing slash commands, we still want to allow the user to provide their desired context
3788-
parsedText = parseSlashCommands(parsedText)
3817+
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
3818+
3819+
if (needsCheck) {
3820+
needsClinerulesFileCheck = true
3821+
}
37893822

37903823
return {
37913824
...block,
3792-
text: parsedText,
3825+
text: processedText,
37933826
}
37943827
}
37953828
}
37963829
return block
37973830
}),
3798-
),
3831+
)
3832+
}
3833+
3834+
// Run initial promises in parallel
3835+
const [processedUserContent, environmentDetails] = await Promise.all([
3836+
processUserContent(),
37993837
this.getEnvironmentDetails(includeFileDetails),
38003838
])
3839+
3840+
// After processing content, check clinerulesData if needed
3841+
let clinerulesError = false
3842+
if (needsClinerulesFileCheck) {
3843+
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
3844+
}
3845+
3846+
// Return all results
3847+
return [processedUserContent, environmentDetails, clinerulesError]
38013848
}
38023849

38033850
async getEnvironmentDetails(includeFileDetails: boolean = false) {

webview-ui/src/utils/slash-commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
1212
name: "smol",
1313
description: "Condenses your current context window",
1414
},
15+
{
16+
name: "newrule",
17+
description: "Create a new Cline rule based on your conversation",
18+
},
1519
]
1620

1721
// Regex for detecting slash commands in text

0 commit comments

Comments
 (0)