12 KiB
ARCHITECTURE_NOTES.md
Phase 0 — The Archaeological Dig into Roo Code
1. What Is Roo Code?
Roo Code is a VSCode extension that runs an AI coding agent inside the editor. It is a monorepo built with TypeScript, structured as:
Roo-Code/
├── src/ ← VSCode Extension Host (the main agent logic)
│ ├── extension.ts ← Entry point: activates the extension
│ ├── core/
│ │ ├── task/Task.ts ← THE agent brain. Manages the entire conversation loop.
│ │ ├── tools/ ← Every tool the agent can call (read, write, execute...)
│ │ ├── prompts/system.ts ← Builds the system prompt sent to the LLM
│ │ ├── assistant-message/ ← Processes what the LLM returns (tool calls, text)
│ │ └── webview/ ← Bridge to the UI panel
│ └── services/ ← MCP, checkpoints, skills
├── packages/
│ └── types/src/tool.ts ← Canonical list of all tool names (ToolName type)
└── apps/ ← Web app, CLI
2. How the Agent Loop Works (The Nervous System)
The agent is a request-response loop between the LLM and the IDE. Here is the complete flow:
User types a message
↓
Task.ts → getSystemPrompt() → SYSTEM_PROMPT() in src/core/prompts/system.ts
↓
Task.ts → makeApiRequest() → sends [systemPrompt + conversation history] to Claude/OpenAI
↓
LLM responds with content blocks:
- "text" block → displayed to user
- "tool_use" block → intercepted for execution
↓
presentAssistantMessage() in src/core/assistant-message/presentAssistantMessage.ts
↓
switch (block.name) {
case "write_to_file" → WriteToFileTool.execute()
case "execute_command" → ExecuteCommandTool.execute()
case "read_file" → ReadFileTool.execute()
...each tool handles its own askApproval + result
}
↓
Tool result pushed back → next LLM turn
3. The Three Critical Files (Hook Insertion Points)
3.1 Tool Dispatch — src/core/assistant-message/presentAssistantMessage.ts
Line 678 — The switch(block.name) block
This is the single most important location in the entire codebase. Every tool call from the LLM passes through this switch statement. There is no other path. This is where:
- Pre-Hooks go: BEFORE the switch executes (before any tool runs)
- Post-Hooks go: AFTER the tool case completes (after the file is written / command is run)
// LINE 678 in presentAssistantMessage.ts
switch (block.name) {
case "write_to_file": ← mutating: needs Pre-Hook + Post-Hook
await writeToFileTool.handle(...)
break
case "execute_command": ← destructive: needs Pre-Hook (HITL approval)
await executeCommandTool.handle(...)
break
case "read_file": ← safe: no hook needed
...
}
3.2 System Prompt — src/core/task/Task.ts line 3792 → src/core/prompts/system.ts
The system prompt is built by getSystemPrompt() (private method on Task, line 3745), which calls SYSTEM_PROMPT() in system.ts. This function assembles modular sections from src/core/prompts/sections/.
This is where we inject the intent enforcement instruction:
"You CANNOT write code immediately. Your FIRST action MUST be
select_active_intent."
3.3 Tool Definitions — packages/types/src/tool.ts
The array toolNames (line 24) is the canonical registry of all valid tool names. Adding "select_active_intent" here makes it a first-class tool recognized by the parser and type system.
4. The Hook Architecture We Are Building
4.1 The Two-Stage State Machine
User: "Refactor the auth middleware"
│
▼
┌─────────────────────────┐
│ LLM analyzes request │
│ (State 1: The Request) │
└───────────┬─────────────┘
│ LLM calls: select_active_intent("INT-001")
▼
┌─────────────────────────────────────────────────┐
│ PRE-HOOK fires on select_active_intent │
│ → Reads .orchestration/active_intents.yaml │
│ → Finds INT-001: constraints + owned_scope │
│ → Returns <intent_context> XML block to LLM │
│ (State 2: The Handshake) │
└───────────┬─────────────────────────────────────┘
│ LLM now has context, calls: write_to_file("src/auth/middleware.ts", ...)
▼
┌─────────────────────────────────────────────────┐
│ PRE-HOOK fires on write_to_file │
│ → Checks: active intent declared? ✓ │
│ → Checks: src/auth/middleware.ts in scope? ✓ │
│ → Allows execution to proceed │
└───────────┬─────────────────────────────────────┘
│ WriteToFileTool.execute() runs — file is saved
▼
┌─────────────────────────────────────────────────┐
│ POST-HOOK fires after write_to_file │
│ → Computes SHA-256 of written content │
│ → Appends JSON record to agent_trace.jsonl │
│ → Links: INT-001 → src/auth/middleware.ts │
│ (State 3: Contextualized Action + Trace) │
└─────────────────────────────────────────────────┘
4.2 What Gets Blocked
Agent tries write_to_file WITHOUT calling select_active_intent first:
→ PRE-HOOK: IntentGate fires → BLOCKED
→ Returns: "Error: You must call select_active_intent before writing files."
Agent tries to write src/billing/invoice.ts but INT-001 only owns src/auth/**:
→ PRE-HOOK: ScopeGuard fires → BLOCKED
→ Returns: "Scope Violation: INT-001 is not authorized to edit src/billing/invoice.ts"
5. The src/hooks/ Directory Structure
src/hooks/
├── types.ts ← Shared types: HookContext, HookResult, IntentState
├── HookEngine.ts ← The singleton middleware engine
│ Manages per-task intent state
│ Runs pre/post hook chains
├── preHooks/
│ ├── intentGate.ts ← Blocks mutating tools if no intent is declared
│ └── scopeGuard.ts ← Blocks writes outside the intent's owned_scope
├── postHooks/
│ └── traceLedger.ts ← SHA-256 hash + append to agent_trace.jsonl
└── utils/
├── contentHash.ts ← SHA-256 helper (crypto built-in)
├── intentLoader.ts ← Parses .orchestration/active_intents.yaml
└── orchestrationPaths.ts ← Centralized .orchestration/ path resolution
6. The Data Model (.orchestration/)
.orchestration/
├── active_intents.yaml ← What work is authorized (the "why")
├── agent_trace.jsonl ← Append-only ledger of every action (the "proof")
└── intent_map.md ← Which files belong to which intent (the "map")
active_intents.yaml schema:
active_intents:
- id: "INT-001"
name: "JWT Authentication Migration"
status: "IN_PROGRESS"
owned_scope:
- "src/auth/**"
- "src/middleware/jwt.ts"
constraints:
- "Must not use external auth providers"
acceptance_criteria:
- "Unit tests in tests/auth/ pass"
agent_trace.jsonl record schema (spatial independence via content hash):
{
"id": "uuid-v4",
"timestamp": "ISO-8601",
"intent_id": "INT-001",
"vcs": { "revision_id": "git_sha" },
"files": [
{
"relative_path": "src/auth/middleware.ts",
"contributor": { "entity_type": "AI", "model_identifier": "claude-3-5-sonnet" },
"ranges": [
{
"start_line": 1,
"end_line": 45,
"content_hash": "sha256:a8f5f167..."
}
],
"mutation_class": "AST_REFACTOR",
"related": [{ "type": "specification", "value": "INT-001" }]
}
]
}
7. The select_active_intent Tool
A new first-class tool added to the agent's toolset. The LLM MUST call this before any mutating action.
Input: { intent_id: string }
What happens when called:
- HookEngine reads
active_intents.yamland finds the intent - Extracts constraints, owned_scope, acceptance_criteria
- Returns an
<intent_context>XML block back to the LLM - Marks the intent as active in per-task state (Map<taskId, activeIntentId>)
What the LLM receives:
<intent_context>
<intent id="INT-001" name="JWT Authentication Migration">
<owned_scope>
<path>src/auth/**</path>
<path>src/middleware/jwt.ts</path>
</owned_scope>
<constraints>
<constraint>Must not use external auth providers</constraint>
</constraints>
<acceptance_criteria>
<criterion>Unit tests in tests/auth/ pass</criterion>
</acceptance_criteria>
</intent>
</intent_context>
8. System Prompt Modification
The following instruction is injected into the system prompt (in src/core/prompts/system.ts):
# Intent-Driven Governance Protocol
You are operating under a strict governance system. You CANNOT write, edit, or delete
files immediately. Your FIRST action for any code modification task MUST be:
1. Analyze the user's request
2. Call `select_active_intent(intent_id)` with the appropriate intent ID from
.orchestration/active_intents.yaml
3. Wait for the <intent_context> block to be returned
4. Only THEN proceed with code modifications — and only within the declared scope
If you attempt to call write_to_file, apply_diff, edit, or execute_command
without first calling select_active_intent, the system will BLOCK your action
and return an error.
9. Key Architectural Decisions
| Decision | Choice | Reason |
|---|---|---|
| Hook insertion point | presentAssistantMessage.ts before switch(block.name) |
Single choke point — ALL tools pass through here |
| Intent state storage | Map<taskId, string> in HookEngine singleton |
No Task.ts modification needed; isolated |
| Content hashing | Node.js crypto.createHash('sha256') |
Zero dependency, always available in Extension Host |
| YAML parsing | yaml package (already in src/package.json) |
Already a project dependency |
| Scope matching | Simple prefix/glob matching | Sufficient for the demo; expandable to minimatch |
| Trace format | Append-only JSONL | Machine-readable, spatially independent, append-safe |
| select_active_intent registration | Added to toolNames in packages/types/src/tool.ts |
Cleanest: makes it first-class, recognized by parser |
10. Files Modified / Created
Modified:
packages/types/src/tool.ts— Added"select_active_intent"to toolNamessrc/core/assistant-message/presentAssistantMessage.ts— Wired pre/post hooks + select_active_intent casesrc/core/prompts/system.ts— Injected intent enforcement instruction
Created:
src/hooks/— Entire hooks directory (new)src/core/tools/SelectActiveIntentTool.ts— The new tool.orchestration/active_intents.yaml— Sample intent definitions.orchestration/agent_trace.jsonl— Empty ledger (machine-managed).orchestration/intent_map.md— Intent-to-file spatial map