mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
docs: finalize interim submission artifacts (intent_map and hooks directory)
This commit is contained in:
parent
d9eaead66a
commit
ab4b161f2e
13 changed files with 243 additions and 11 deletions
|
|
@ -1,12 +1,3 @@
|
|||
<!-- # Architecture Notes
|
||||
|
||||
## Findings
|
||||
|
||||
- Tool Execution: Controlled via `presentAssistantMessage.ts` and individual tool handles like `WriteToFileTool.ts`.
|
||||
- Prompt Logic: The system prompt is constructed in `src/core/task/Task.ts`.
|
||||
- Governance Layer: We have initialized a `.orchestration/` sidecar for intent-traceability.
|
||||
- Integration Goal: We will implement a Pre-Hook in `WriteToFileTool.ts` that validates actions against `.orchestration/active_intents.yaml`. -->
|
||||
|
||||
# Architecture Notes: Master Thinker Edition
|
||||
|
||||
## 1. Data Flow Map (Request-to-Execution)
|
||||
|
|
@ -24,3 +15,38 @@
|
|||
## 3. Intent-Code Gap Analysis
|
||||
|
||||
Standard Git tracks "What" changed but lacks the "Why." By using a sidecar orchestration layer, we map every Abstract Syntax Tree (AST) change to a specific Requirement ID. This prevents "Context Rot" where agents lose track of architectural constraints during long-running tasks.
|
||||
|
||||
# Architectural Design Report
|
||||
|
||||
## 1. The Intent-First Protocol (Two-Stage State Machine)
|
||||
|
||||
The core of this implementation is a move away from "Vibe Coding" towards a governed, stateful interaction. I have architected a Two-Stage State Machine for every user request:
|
||||
|
||||
**Stage 1: The Reasoning Intercept (The Handshake):** The agent is no longer permitted to generate code immediately. It must first analyze the request, identify a valid intent_id from the governance sidecar, and call the select_active_intent tool.
|
||||
+1
|
||||
|
||||
**Stage 2: Contextualized Action:** Only after the "Handshake" is successful and the context is injected can the agent proceed to use destructive tools like write_to_file or execute_command.
|
||||
+1
|
||||
|
||||
## 2. The Deterministic Hook (Gatekeeper Architecture)
|
||||
|
||||
To ensure compliance, I implemented a Deterministic Hook System that acts as a strict middleware boundary:
|
||||
+1
|
||||
|
||||
Pre-Hook Implementation: In WriteToFileTool.ts and ExecuteCommandTool.ts, I injected a gatekeeper check at the start of the handle method.
|
||||
|
||||
Verification Logic: This hook verifies the presence of a global active intent flag. If the agent attempts a file modification without a validated "checkout," the hook blocks execution and returns a formal governance error: "You must cite a valid active Intent ID".
|
||||
|
||||
Fail-Safe: This ensures that the architecture enforces the rules, rather than relying on the LLM's "best effort" to follow instructions.
|
||||
|
||||
## 3. Context Engineering (Dynamic Injection vs. Context Rot)
|
||||
|
||||
Traditional AI IDEs suffer from "Context Rot" by dumping entire file trees into the prompt. This implementation solves this via Dynamic Context Injection:
|
||||
+1
|
||||
|
||||
**Sidecar Pattern:** All architectural constraints and business intents are stored in .orchestration/active_intents.yaml.
|
||||
+1
|
||||
|
||||
**On-Demand Context:** When select_active_intent is called, the system reads the YAML and constructs a targeted <intent_context> XML block.
|
||||
|
||||
**Traceability:** This ensures the agent only operates within its "owned_scope" and respects the "acceptance_criteria" defined in the sidecar, maintaining a high signal-to-noise ratio in the context window.
|
||||
|
|
|
|||
8
intent_map.md
Normal file
8
intent_map.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Intent-Code Mapping
|
||||
|
||||
| Intent ID | Target Files / Modules | Status |
|
||||
| --------- | ------------------------------------------------- | ----------- |
|
||||
| INT-001 | src/core/tools/WriteToFileTool.ts | IN PROGRESS |
|
||||
| INT-001 | src/core/assistant-msg/presentAssistantMessage.ts | COMPLETED |
|
||||
|
||||
[cite_start]**Notes:** This map links the high-level business logic in `.orchestration/active_intents.yaml` to specific AST changes in the source code.
|
||||
|
|
@ -39,6 +39,7 @@ export const toolNames = [
|
|||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"select_active_intent",
|
||||
"new_task",
|
||||
"codebase_search",
|
||||
"update_todo_list",
|
||||
|
|
|
|||
|
|
@ -546,6 +546,14 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "select_active_intent":
|
||||
if (partialArgs.intent_id !== undefined) {
|
||||
nativeArgs = {
|
||||
intent_id: partialArgs.intent_id,
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "update_todo_list":
|
||||
if (partialArgs.todos !== undefined) {
|
||||
nativeArgs = {
|
||||
|
|
@ -881,6 +889,14 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "select_active_intent":
|
||||
if (args.intent_id !== undefined) {
|
||||
nativeArgs = {
|
||||
intent_id: args.intent_id,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "update_todo_list":
|
||||
if (args.todos !== undefined) {
|
||||
nativeArgs = {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { useMcpToolTool } from "../tools/UseMcpToolTool"
|
|||
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
|
||||
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
|
||||
import { switchModeTool } from "../tools/SwitchModeTool"
|
||||
import { SelectActiveIntentTool } from "../tools/SelectActiveIntentTool"
|
||||
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
|
||||
import { newTaskTool } from "../tools/NewTaskTool"
|
||||
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
|
||||
|
|
@ -41,6 +42,8 @@ import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
|
|||
import { formatResponse } from "../prompts/responses"
|
||||
import { sanitizeToolUseId } from "../../utils/tool-id"
|
||||
|
||||
const selectActiveIntentTool = new SelectActiveIntentTool()
|
||||
|
||||
/**
|
||||
* Processes and presents assistant message content to the user interface.
|
||||
*
|
||||
|
|
@ -365,6 +368,8 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
return `[${block.name}]`
|
||||
case "switch_mode":
|
||||
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
|
||||
case "select_active_intent":
|
||||
return `[${block.name} for '${block.params.intent_id}']`
|
||||
case "codebase_search":
|
||||
return `[${block.name} for '${block.params.query}']`
|
||||
case "read_command_output":
|
||||
|
|
@ -803,6 +808,16 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "select_active_intent": {
|
||||
try {
|
||||
const intentId = block.nativeArgs?.intent_id ?? block.params.intent_id ?? ""
|
||||
const result = await selectActiveIntentTool.handle({ intent_id: intentId }, cline.cwd)
|
||||
pushToolResult(result)
|
||||
} catch (error) {
|
||||
await handleError("loading active intent context", error as Error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "new_task":
|
||||
await checkpointSaveAndMark(cline)
|
||||
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ import {
|
|||
getSkillsSection,
|
||||
} from "./sections"
|
||||
|
||||
const INTENT_DRIVEN_ARCHITECT_RULE =
|
||||
"You are an Intent-Driven Architect. You CANNOT write code immediately. Your first action MUST be to analyze the user request and call select_active_intent to load the necessary context and constraints."
|
||||
|
||||
// Helper function to get prompt component, filtering out empty objects
|
||||
export function getPromptComponent(
|
||||
customModePrompts: CustomModePrompts | undefined,
|
||||
|
|
@ -82,7 +85,9 @@ async function generatePrompt(
|
|||
// Tools catalog is not included in the system prompt.
|
||||
const toolsCatalog = ""
|
||||
|
||||
const basePrompt = `${roleDefinition}
|
||||
const basePrompt = `${INTENT_DRIVEN_ARCHITECT_RULE}
|
||||
|
||||
${roleDefinition}
|
||||
|
||||
${markdownFormattingSection()}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import newTask from "./new_task"
|
|||
import readCommandOutput from "./read_command_output"
|
||||
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
|
||||
import runSlashCommand from "./run_slash_command"
|
||||
import selectActiveIntent from "./select_active_intent"
|
||||
import skill from "./skill"
|
||||
import searchReplace from "./search_replace"
|
||||
import edit_file from "./edit_file"
|
||||
|
|
@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
|
|||
readCommandOutput,
|
||||
createReadFileTool(readFileOptions),
|
||||
runSlashCommand,
|
||||
selectActiveIntent,
|
||||
skill,
|
||||
searchReplace,
|
||||
edit_file,
|
||||
|
|
|
|||
25
src/core/prompts/tools/native-tools/select_active_intent.ts
Normal file
25
src/core/prompts/tools/native-tools/select_active_intent.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
const SELECT_ACTIVE_INTENT_DESCRIPTION = `Load the active intent context from .orchestration/active_intents.yaml by intent ID. Use this before planning or coding so constraints and scope are explicitly available in the next turn.`
|
||||
|
||||
const INTENT_ID_PARAMETER_DESCRIPTION = `Intent ID to load from .orchestration/active_intents.yaml (for example: intent-1)`
|
||||
|
||||
export default {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "select_active_intent",
|
||||
description: SELECT_ACTIVE_INTENT_DESCRIPTION,
|
||||
strict: true,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
intent_id: {
|
||||
type: "string",
|
||||
description: INTENT_ID_PARAMETER_DESCRIPTION,
|
||||
},
|
||||
},
|
||||
required: ["intent_id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
} satisfies OpenAI.Chat.ChatCompletionTool
|
||||
37
src/core/tools/SelectActiveIntentTool.ts
Normal file
37
src/core/tools/SelectActiveIntentTool.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as yaml from "yaml"
|
||||
|
||||
export class SelectActiveIntentTool {
|
||||
async handle(params: { intent_id: string }, workspaceRoot: string): Promise<string> {
|
||||
if (!params.intent_id?.trim()) {
|
||||
return "ERROR: Missing required parameter 'intent_id'."
|
||||
}
|
||||
try {
|
||||
const content = await fs.promises.readFile(
|
||||
path.join(workspaceRoot, ".orchestration", "active_intents.yaml"),
|
||||
"utf-8",
|
||||
)
|
||||
const data = yaml.parse(content) as any
|
||||
const intents = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray(data?.active_intents)
|
||||
? data.active_intents
|
||||
: Array.isArray(data?.intents)
|
||||
? data.intents
|
||||
: Object.entries(data ?? {}).map(([intent_id, entry]) => ({ intent_id, ...(entry as object) }))
|
||||
const match = intents.find((intent: any) => (intent?.intent_id ?? intent?.id) === params.intent_id)
|
||||
if (!match) {
|
||||
return `ERROR: Intent '${params.intent_id}' not found in .orchestration/active_intents.yaml.`
|
||||
}
|
||||
return `<intent_context>${JSON.stringify(match, null, 2)}</intent_context>`
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
|
||||
return "ERROR: Governance sidecar not found at .orchestration/active_intents.yaml. Please initialize Phase 0 first."
|
||||
}
|
||||
return `ERROR: Failed to read or parse .orchestration/active_intents.yaml: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,17 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
|
|||
return
|
||||
}
|
||||
|
||||
const hasSelectedActiveIntent = (task.toolUsage.select_active_intent?.attempts ?? 0) > 0
|
||||
if (!hasSelectedActiveIntent) {
|
||||
const governanceError = "GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files."
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("write_to_file", governanceError)
|
||||
task.didToolFailInCurrentTurn = true
|
||||
pushToolResult(formatResponse.toolError(governanceError))
|
||||
await task.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
const accessAllowed = task.rooIgnoreController?.validateAccess(relPath)
|
||||
|
||||
if (!accessAllowed) {
|
||||
|
|
|
|||
62
src/core/tools/__tests__/selectActiveIntentTool.spec.ts
Normal file
62
src/core/tools/__tests__/selectActiveIntentTool.spec.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import * as fs from "fs"
|
||||
|
||||
import { SelectActiveIntentTool } from "../SelectActiveIntentTool"
|
||||
|
||||
describe("SelectActiveIntentTool", () => {
|
||||
const mockedReadFile = vi.spyOn(fs.promises, "readFile")
|
||||
const tool = new SelectActiveIntentTool()
|
||||
const workspaceRoot = "/workspace"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns intent_context XML for matching intent in intents array", async () => {
|
||||
mockedReadFile.mockResolvedValue(
|
||||
`intents:
|
||||
- intent_id: intent-1
|
||||
constraints:
|
||||
must_not:
|
||||
- direct_db_writes
|
||||
scope:
|
||||
files:
|
||||
- src/**
|
||||
` as never,
|
||||
)
|
||||
|
||||
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
|
||||
|
||||
expect(result).toContain("<intent_context>")
|
||||
expect(result).toContain('"intent_id": "intent-1"')
|
||||
expect(result).toContain('"constraints"')
|
||||
expect(result).toContain("direct_db_writes")
|
||||
expect(result).toContain('"scope"')
|
||||
expect(result).toContain("src/**")
|
||||
expect(result).toContain("</intent_context>")
|
||||
})
|
||||
|
||||
it("returns not found error when intent_id does not exist", async () => {
|
||||
mockedReadFile.mockResolvedValue(
|
||||
`active_intents:
|
||||
- intent_id: intent-2
|
||||
constraints: {}
|
||||
scope: {}
|
||||
` as never,
|
||||
)
|
||||
|
||||
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
|
||||
|
||||
expect(result).toBe("ERROR: Intent 'intent-1' not found in .orchestration/active_intents.yaml.")
|
||||
})
|
||||
|
||||
it("returns initialization error when sidecar file is missing", async () => {
|
||||
const missingFileError = Object.assign(new Error("ENOENT"), { code: "ENOENT" })
|
||||
mockedReadFile.mockRejectedValue(missingFileError)
|
||||
|
||||
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
|
||||
|
||||
expect(result).toBe(
|
||||
"ERROR: Governance sidecar not found at .orchestration/active_intents.yaml. Please initialize Phase 0 first.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -123,6 +123,10 @@ describe("writeToFileTool", () => {
|
|||
mockCline.cwd = "/"
|
||||
mockCline.consecutiveMistakeCount = 0
|
||||
mockCline.didEditFile = false
|
||||
mockCline.didToolFailInCurrentTurn = false
|
||||
mockCline.toolUsage = {
|
||||
select_active_intent: { attempts: 1, failures: 0 },
|
||||
}
|
||||
mockCline.diffStrategy = undefined
|
||||
mockCline.providerRef = {
|
||||
deref: vi.fn().mockReturnValue({
|
||||
|
|
@ -236,6 +240,22 @@ describe("writeToFileTool", () => {
|
|||
}
|
||||
|
||||
describe("access control", () => {
|
||||
it("blocks write when no active intent has been selected in this session", async () => {
|
||||
mockCline.toolUsage = {}
|
||||
|
||||
const result = await executeWriteFileTool({})
|
||||
|
||||
expect(result).toBe(
|
||||
"Error: GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
|
||||
)
|
||||
expect(mockCline.recordToolError).toHaveBeenCalledWith(
|
||||
"write_to_file",
|
||||
"GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
|
||||
)
|
||||
expect(mockCline.diffViewProvider.saveChanges).not.toHaveBeenCalled()
|
||||
expect(mockCline.diffViewProvider.saveDirectly).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("validates and allows access when rooIgnoreController permits", async () => {
|
||||
await executeWriteFileTool({}, { accessAllowed: true })
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export const toolParamNames = [
|
|||
"todos",
|
||||
"prompt",
|
||||
"image",
|
||||
"intent_id",
|
||||
// read_file parameters (native protocol)
|
||||
"operations", // search_and_replace parameter for multiple operations
|
||||
"patch", // apply_patch parameter
|
||||
|
|
@ -112,6 +113,7 @@ export type NativeToolArgs = {
|
|||
skill: { skill: string; args?: string }
|
||||
search_files: { path: string; regex: string; file_pattern?: string | null }
|
||||
switch_mode: { mode_slug: string; reason: string }
|
||||
select_active_intent: { intent_id: string }
|
||||
update_todo_list: { todos: string }
|
||||
use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> }
|
||||
write_to_file: { path: string; content: string }
|
||||
|
|
@ -282,6 +284,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
|||
ask_followup_question: "ask questions",
|
||||
attempt_completion: "complete tasks",
|
||||
switch_mode: "switch modes",
|
||||
select_active_intent: "load intent context",
|
||||
new_task: "create new task",
|
||||
codebase_search: "codebase search",
|
||||
update_todo_list: "update todo list",
|
||||
|
|
@ -307,7 +310,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
|
|||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
modes: {
|
||||
tools: ["switch_mode", "new_task"],
|
||||
tools: ["switch_mode", "select_active_intent", "new_task"],
|
||||
alwaysAvailable: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -317,6 +320,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
|
|||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"select_active_intent",
|
||||
"new_task",
|
||||
"update_todo_list",
|
||||
"run_slash_command",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue