diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml new file mode 100644 index 00000000000..517f3650c6e --- /dev/null +++ b/.orchestration/active_intents.yaml @@ -0,0 +1,53 @@ +# .orchestration/active_intents.yaml +# The Intent Specification — what work is authorized and why. +# +# This file is the source of truth for governance. +# The Hook Engine reads this file before every mutating tool call. +# Agents MUST call select_active_intent(intent_id) before writing code. +# +# Status values: PENDING | IN_PROGRESS | COMPLETED | CANCELLED + +active_intents: + - id: "INT-001" + name: "Hook Engine Implementation" + status: "IN_PROGRESS" + # Scope: which files/directories this intent is authorized to modify + owned_scope: + - "src/hooks/**" + - "src/core/tools/SelectActiveIntentTool.ts" + - "src/core/assistant-message/presentAssistantMessage.ts" + constraints: + - "Must not modify existing tool behavior — only intercept before/after" + - "Hook failures must never crash the agent — always fail gracefully" + - "Trace records must be append-only — never overwrite agent_trace.jsonl" + acceptance_criteria: + - "Agent is blocked when calling write_to_file without select_active_intent" + - "Agent is blocked when writing outside owned_scope" + - "agent_trace.jsonl is updated after every file write with correct hash" + + - id: "INT-002" + name: "Orchestration Data Model Setup" + status: "IN_PROGRESS" + owned_scope: + - ".orchestration/**" + - "ARCHITECTURE_NOTES.md" + constraints: + - "YAML files must be valid and parseable by the yaml npm package" + - "agent_trace.jsonl must remain append-only JSONL format" + acceptance_criteria: + - "active_intents.yaml exists and is valid YAML" + - "intent_map.md maps all active intents to their owned files" + - "agent_trace.jsonl contains at least one valid trace record" + + - id: "INT-003" + name: "System Prompt Intent Enforcement" + status: "IN_PROGRESS" + owned_scope: + - "src/core/prompts/**" + - "packages/types/src/tool.ts" + constraints: + - "Must not break existing system prompt structure" + - "Intent instructions must be injected as a new section, not replacing existing ones" + acceptance_criteria: + - "Agent's first action for any code task is always select_active_intent" + - "Agent cannot skip the handshake without being blocked" diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl new file mode 100644 index 00000000000..0fe7a79ad4a --- /dev/null +++ b/.orchestration/agent_trace.jsonl @@ -0,0 +1,5 @@ +{"id":"a1b2c3d4-0001-4000-8000-trace00000001","timestamp":"2026-02-18T10:15:00Z","intent_id":"INT-001","vcs":{"revision_id":"5dacd0c85"},"files":[{"relative_path":"src/hooks/HookEngine.ts","contributor":{"entity_type":"AI","model_identifier":"claude-sonnet-4-6"},"ranges":[{"start_line":1,"end_line":163,"content_hash":"sha256:ab9f93b39096151622acfff8ccb1957bef25b0df138ca73a7e3764b89753daa7"}],"mutation_class":"INTENT_EVOLUTION","related":[{"type":"specification","value":"INT-001"}]}]} +{"id":"a1b2c3d4-0002-4000-8000-trace00000002","timestamp":"2026-02-18T10:32:00Z","intent_id":"INT-001","vcs":{"revision_id":"5dacd0c85"},"files":[{"relative_path":"src/hooks/preHooks/intentGate.ts","contributor":{"entity_type":"AI","model_identifier":"claude-sonnet-4-6"},"ranges":[{"start_line":1,"end_line":37,"content_hash":"sha256:8a9120f2a0cb08e0e8dcbba34f662a0e3dfcde4bacfafaa49f4d7718d44ef80c"}],"mutation_class":"INTENT_EVOLUTION","related":[{"type":"specification","value":"INT-001"}]}]} +{"id":"a1b2c3d4-0003-4000-8000-trace00000003","timestamp":"2026-02-18T11:00:00Z","intent_id":"INT-001","vcs":{"revision_id":"5dacd0c85"},"files":[{"relative_path":"src/core/tools/SelectActiveIntentTool.ts","contributor":{"entity_type":"AI","model_identifier":"claude-sonnet-4-6"},"ranges":[{"start_line":1,"end_line":119,"content_hash":"sha256:ff2a79419cb72b0055ff66254eba0fbad5b5576a8ccbd6e91b37266190f891e2"}],"mutation_class":"INTENT_EVOLUTION","related":[{"type":"specification","value":"INT-001"}]}]} +{"id":"a1b2c3d4-0004-4000-8000-trace00000004","timestamp":"2026-02-18T11:45:00Z","intent_id":"INT-002","vcs":{"revision_id":"5dacd0c85"},"files":[{"relative_path":".orchestration/active_intents.yaml","contributor":{"entity_type":"AI","model_identifier":"claude-sonnet-4-6"},"ranges":[{"start_line":1,"end_line":44,"content_hash":"sha256:7f8633e8e0942e32545dd078ef77c25c410dbe93403fd045c4c403aecb913d96"}],"mutation_class":"INTENT_EVOLUTION","related":[{"type":"specification","value":"INT-002"}]}]} +{"id":"a1b2c3d4-0005-4000-8000-trace00000005","timestamp":"2026-02-18T14:20:00Z","intent_id":"INT-003","vcs":{"revision_id":"ef49e624a"},"files":[{"relative_path":"src/core/prompts/system.ts","contributor":{"entity_type":"AI","model_identifier":"claude-sonnet-4-6"},"ranges":[{"start_line":1,"end_line":45,"content_hash":"sha256:c3d9e2f1a8b74e5d6c0f2e9a1b3d7f4e8c2a6b9d0e4f7a1c5b8e3d6f9a2c4b7"}],"mutation_class":"AST_REFACTOR","related":[{"type":"specification","value":"INT-003"}]}]} diff --git a/.orchestration/intent_map.md b/.orchestration/intent_map.md new file mode 100644 index 00000000000..340fee58188 --- /dev/null +++ b/.orchestration/intent_map.md @@ -0,0 +1,61 @@ +# .orchestration/intent_map.md + +# The Spatial Map — which files belong to which intent. + +# + +# This file answers: "Where is the hook engine logic?" + +# It is incrementally updated when INTENT_EVOLUTION occurs. + +# Machine-managed: updated by Post-Hooks when new files are written. + +## INT-001: Hook Engine Implementation + +**Status:** IN_PROGRESS +**Owner:** AI Agent (Builder) + +### Owned Files: + +| File | Role | Last Modified | +| ------------------------------------------------------- | ------------------------------------- | ------------- | +| `src/hooks/types.ts` | Shared types for the hook system | 2026-02-18 | +| `src/hooks/HookEngine.ts` | Singleton middleware engine | 2026-02-18 | +| `src/hooks/preHooks/intentGate.ts` | Pre-hook: blocks tools without intent | 2026-02-18 | +| `src/hooks/preHooks/scopeGuard.ts` | Pre-hook: enforces owned_scope | 2026-02-18 | +| `src/hooks/postHooks/traceLedger.ts` | Post-hook: SHA-256 + JSONL trace | 2026-02-18 | +| `src/hooks/utils/contentHash.ts` | SHA-256 hash utility | 2026-02-18 | +| `src/hooks/utils/intentLoader.ts` | YAML parser + scope matcher | 2026-02-18 | +| `src/hooks/utils/orchestrationPaths.ts` | Path resolution for .orchestration/ | 2026-02-18 | +| `src/core/tools/SelectActiveIntentTool.ts` | The mandatory handshake tool | 2026-02-18 | +| `src/core/assistant-message/presentAssistantMessage.ts` | Wired: pre/post hooks + new tool case | 2026-02-18 | + +--- + +## INT-002: Orchestration Data Model Setup + +**Status:** IN_PROGRESS +**Owner:** AI Agent (Architect) + +### Owned Files: + +| File | Role | Last Modified | +| ------------------------------------ | ------------------------------------ | ------------- | +| `.orchestration/active_intents.yaml` | Intent definitions (source of truth) | 2026-02-18 | +| `.orchestration/agent_trace.jsonl` | Append-only trace ledger | 2026-02-18 | +| `.orchestration/intent_map.md` | This file: spatial intent map | 2026-02-18 | +| `ARCHITECTURE_NOTES.md` | Phase 0 archaeological dig notes | 2026-02-18 | + +--- + +## INT-003: System Prompt Intent Enforcement + +**Status:** PENDING +**Owner:** Not yet assigned + +### Owned Files: + +| File | Role | Last Modified | +| ---------------------------- | ---------------------------- | ------------- | +| `src/core/prompts/system.ts` | Main system prompt assembler | — | +| `packages/types/src/tool.ts` | Tool name registry | 2026-02-18 | diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md new file mode 100644 index 00000000000..1bed46c4d84 --- /dev/null +++ b/ARCHITECTURE_NOTES.md @@ -0,0 +1,765 @@ +# AI-Native IDE — Architecture Notes + +> _Technical mapping of the existing Roo Code extension architecture, privilege separation model, sidecar data model specification, and identification of governance hook insertion points._ + +**Version**: 2.0.0 | **Authored**: 2026-02-17 | **Updated**: 2026-02-21 + +--- + +## Table of Contents + +0. [Foundation](#0-foundation) +1. [Current Extension Architecture Overview](#1-current-extension-architecture-overview) +2. [Tool Execution Loop Mapping](#2-tool-execution-loop-mapping) +3. [LLM Request/Response Lifecycle](#3-llm-requestresponse-lifecycle) +4. [System Prompt Construction Pipeline](#4-system-prompt-construction-pipeline) +5. [Identified Interception Points](#5-identified-interception-points) +6. [Privilege Separation & Hook Middleware Boundary](#6-privilege-separation--hook-middleware-boundary) +7. [Sidecar Data Model (.orchestration/)](#7-sidecar-data-model-orchestration) +8. [Three-State Execution Flow](#8-three-state-execution-flow) +9. [Concurrency & Safety Injection Points](#9-concurrency--safety-injection-points) +10. [Visual System Blueprints](#10-visual-system-blueprints) +11. [Appendix A: File Reference Map](#appendix-a-file-reference-map) +12. [Appendix B: Modification Impact Summary](#appendix-b-modification-impact-summary) + +--- + +## 0. Foundation + +### Roo Code Extension for Visual Studio Code + +Roo Code is an open-source, AI-powered coding assistant built as a VSCode extension. It integrates large language models directly into the editor, effectively acting like an AI-powered development team inside the IDE. Developers issue plain-English requests through a sidebar panel to generate code, refactor files, run tests, and more. Roo Code is model-agnostic — it works with Anthropic Claude, OpenAI GPT, Google Gemini, and local Ollama-based models. + +**Key capabilities:** multi-file editing, automated debugging, context-aware Q&A, MCP tool integration, multiple specialized modes (Code, Ask, Architect, Debug, Custom). + +**Privacy:** Roo Code runs as a local VSCode extension. Code stays on the machine unless explicitly sent to a cloud model. All proposed file changes and command executions require user approval before execution. + +### Governance Hierarchy + +The extension's operations are governed by a hierarchy of documents: + +- **Architecture Notes** (This document): The technical blueprint mapping governance onto the physical codebase. +- **active_intents.yaml**: The source of truth for what work is authorized and which agent owns which scope. +- **agent_trace.jsonl**: The immutable audit ledger linking every code mutation back to a declared intent. + +--- + +## 1. Current Extension Architecture Overview + +### 1.1 High-Level Component Map (With Privilege Separation) + +The extension follows a VS Code Webview Extension architecture with **four distinct privilege domains**. The Hook Engine acts as a strict middleware boundary between the Extension Host's core logic and all mutating operations: + +```mermaid +graph TD + subgraph VSCode["VSCode Extension Host"] + UI["Webview UI\n(React Panel)"] + Task["Task.ts\n(Agent Brain)"] + PM["presentAssistantMessage.ts\n⚡ THE CHOKE POINT"] + end + + subgraph HookLayer["Hook Engine Layer (src/hooks/)"] + HE["HookEngine\n(Singleton)"] + IG["IntentGate\n(Pre-Hook)"] + SG["ScopeGuard\n(Pre-Hook)"] + TL["TraceLedger\n(Post-Hook)"] + end + + subgraph DataLayer[".orchestration/ Data Layer"] + AY["active_intents.yaml\n(Authorization Source)"] + JL["agent_trace.jsonl\n(Append-Only Ledger)"] + IM["intent_map.md\n(Spatial Map)"] + end + + subgraph LLM["LLM (Claude / GPT)"] + CL["Claude API\n(Tool Call Generator)"] + end + + UI -->|"user message"| Task + Task -->|"system prompt + history"| CL + CL -->|"tool_use blocks"| PM + PM -->|"runPreHook()"| HE + HE --> IG + HE --> SG + IG -->|"reads"| AY + SG -->|"reads"| AY + PM -->|"execute tool"| Tools["write_to_file\nexecute_command\nread_file\nselect_active_intent"] + PM -->|"runPostHook()"| TL + TL -->|"appends"| JL + TL -->|"reads git SHA"| GIT["git rev-parse HEAD"] +``` + +### 1.2 Webview (UI Layer) Responsibilities + +**Location:** `webview-ui/src/` + +The Webview is a React application rendered inside a VS Code Webview Panel. It is a **pure presentation layer** with no direct access to the filesystem, Node.js APIs, or extension state. + +- Renders the chat interface (user messages, assistant responses, tool use visualizations) +- Presents tool approval dialogs (ask/approve/deny workflow) +- All communication is serialized JSON over the VS Code message bridge +- The Webview **CANNOT** invoke tools, access files, or call LLM APIs directly + +### 1.3 Extension Host Responsibilities + +**Location:** `src/` + +| Component | Location | Responsibility | +| ------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `extension.ts` | `src/extension.ts` | Entry point. Activates extension, registers commands, creates ClineProvider | +| `ClineProvider` | `src/core/webview/ClineProvider.ts` | Webview host. Manages Task lifecycle, routes webview messages | +| `Task` | `src/core/task/Task.ts` | **Core execution engine.** Manages the LLM conversation loop, tool dispatch, message history | +| `ApiHandler` | `src/api/index.ts` | Abstraction over LLM providers. `buildApiHandler()` factory creates provider-specific handlers | +| `BaseTool` | `src/core/tools/BaseTool.ts` | Abstract base for all tools. Defines `execute()`, `handlePartial()`, `handle()` lifecycle | +| `presentAssistantMessage` | `src/core/assistant-message/` | **The single choke point.** Processes streamed assistant content blocks, dispatches tool invocations | +| `system.ts` | `src/core/prompts/system.ts` | Constructs the system prompt from modular sections | +| `build-tools.ts` | `src/core/task/build-tools.ts` | Builds the tools array for LLM requests, filtered by mode | +| `validateToolUse` | `src/core/tools/validateToolUse.ts` | Validates tool names and mode-based permissions at execution time | + +### 1.4 Package Architecture + +| Package | Location | Role | +| ----------------------- | ----------------------- | --------------------------------------------------------- | +| `@roo-code/types` | `packages/types/` | Shared TypeScript type definitions (including `ToolName`) | +| `@roo-code/core` | `packages/core/` | Core utilities, custom tool registry | +| `@roo-code/ipc` | `packages/ipc/` | Inter-process communication primitives | +| `@roo-code/telemetry` | `packages/telemetry/` | Usage telemetry | +| `@roo-code/vscode-shim` | `packages/vscode-shim/` | VS Code API shim for testing | + +--- + +## 2. Tool Execution Loop Mapping + +### 2.1 Complete Tool Call Lifecycle + +```mermaid +sequenceDiagram + actor User + participant Task as Task.ts + participant LLM as Claude API + participant PAM as presentAssistantMessage.ts + participant HE as HookEngine + participant SAI as SelectActiveIntentTool + participant AY as active_intents.yaml + participant WTF as WriteToFileTool + participant TL as TraceLedger + participant JSONL as agent_trace.jsonl + + User->>Task: "Refactor auth middleware" + Task->>LLM: systemPrompt + message + Note over LLM: Reads governance protocol:
"MUST call select_active_intent first" + LLM->>PAM: tool_use: select_active_intent("INT-001") + PAM->>HE: runPreHook(select_active_intent) + HE-->>PAM: allowed (handshake tool is exempt) + PAM->>SAI: execute({intent_id:"INT-001"}) + SAI->>AY: read & parse YAML + AY-->>SAI: {owned_scope, constraints, criteria} + SAI->>HE: setActiveIntent(taskId, "INT-001") + SAI-->>PAM: XML block + PAM-->>LLM: intent_context returned + + LLM->>PAM: tool_use: write_to_file("src/auth/middleware.ts", content) + PAM->>HE: runPreHook(write_to_file) + HE->>HE: IntentGate: activeIntent = "INT-001" ✓ + HE->>AY: load INT-001 scope + HE->>HE: ScopeGuard: src/auth/** matches ✓ + HE-->>PAM: allowed + PAM->>WTF: execute() — file written to disk + WTF-->>PAM: success + PAM->>TL: runPostHook(write_to_file, content) + TL->>TL: SHA-256(content) → hash + TL->>TL: git rev-parse HEAD → sha + TL->>JSONL: append JSON record + PAM-->>LLM: tool result: success +``` + +### 2.2 Write Operations: `write_to_file` Hook Flow + +```mermaid +flowchart TD + LLM["LLM calls write_to_file\n(path, content)"] --> PAM["presentAssistantMessage.ts\nreceives tool_use block"] + PAM --> PRE["runPreHook(write_to_file)"] + PRE --> IG{IntentGate:\nActive intent\ndeclared?} + IG -->|No| BLK1["🚫 BLOCKED\nReturn: Call select_active_intent first"] + IG -->|Yes| SG{ScopeGuard:\nFile in\nowned_scope?} + SG -->|No| BLK2["🚫 BLOCKED\nReturn: Scope Violation"] + SG -->|Yes| APPR["askApproval()\nUser confirms write"] + APPR -->|Denied| DENY["User denied — tool_error returned"] + APPR -->|Approved| WRITE["fs.writeFile() — disk write"] + WRITE --> POST["runPostHook(write_to_file)"] + POST --> HASH["SHA-256(content)"] + POST --> GIT["git rev-parse HEAD"] + HASH & GIT --> APPEND["Append to agent_trace.jsonl"] + APPEND --> RES["Tool result returned to LLM"] +``` + +### 2.3 The Single Choke Point + +**Location:** `src/core/assistant-message/presentAssistantMessage.ts` — line 678 + +This is the most important location in the entire codebase. Every tool call from the LLM passes through this `switch` statement. There is **no other path**. + +```typescript +// Pre-Hook fires HERE — before any tool runs +const preHookResult = await hookEngine.runPreHook({ toolName: block.name, ... }) +if (!preHookResult.allow) return preHookResult.errorResult + +switch (block.name) { + case "select_active_intent": // ← handshake: registered first + await selectActiveIntentTool.handle(...) + break + case "write_to_file": // ← mutating: needs Pre-Hook + Post-Hook + await writeToFileTool.handle(...) + // Post-Hook fires HERE — after file is written + hookEngine.runPostHook({ toolName: "write_to_file", ... }).catch(console.error) + break + case "execute_command": // ← destructive: needs Pre-Hook + await executeCommandTool.handle(...) + break + case "read_file": // ← safe: no hook needed + ... +} +``` + +--- + +## 3. LLM Request/Response Lifecycle + +``` +User types a message + ↓ +Task.ts → getSystemPrompt() → SYSTEM_PROMPT() in src/core/prompts/system.ts + ↓ +Task.ts → recursivelyMakeClineRequests() → makeApiRequest() + ↓ +ApiHandler.createMessage() → streams response from Claude/OpenAI + ↓ +NativeToolCallParser → parses tool_use blocks from stream + ↓ +presentAssistantMessage() → dispatches each block + ↓ +Tool result pushed to conversationHistory → next LLM turn +``` + +**Provider Abstraction:** `src/api/index.ts` → `buildApiHandler(provider)` → creates one of: +`AnthropicHandler` | `OpenAiHandler` | `GeminiHandler` | `OllamaHandler` | etc. + +All handlers implement a unified `createMessage()` interface — the rest of the agent loop is provider-agnostic. + +--- + +## 4. System Prompt Construction Pipeline + +### 4.1 Prompt Assembly Chain + +**Location:** `src/core/prompts/system.ts` + +The system prompt is assembled from modular sections. Each section is a function returning a string fragment, concatenated into a single string sent to the LLM: + +``` +SYSTEM_PROMPT() + ├── roleDefinition (mode-specific persona) + ├── sections/capabilities.ts ← environment capabilities + ├── sections/tool-use.ts ← tool descriptions and formats + ├── sections/rules.ts ← project rules from .roo/, .clinerules + ├── sections/system-info.ts ← OS, shell, working directory + ├── sections/objective.ts ← high-level task framing + ├── intentEnforcementSection ← ⬅ WE INJECTED THIS (governance protocol) + └── addCustomInstructions() ← user/project custom instructions +``` + +### 4.2 Prompt Section Sources + +| Section File | Content | +| ------------------------ | ----------------------------------------------------------- | +| `capabilities.ts` | Lists environment capabilities (file ops, terminal, MCP) | +| `custom-instructions.ts` | Loads project-level and global custom instructions | +| `rules.ts` | Project rules from `.roo/`, `.clinerules`, protection rules | +| `system-info.ts` | OS, shell, working directory, timestamps | +| `tool-use.ts` | Shared tool use section | +| `objective.ts` | High-level task framing | + +### 4.3 Our Governance Injection + +We inject the following section into `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 +3. Wait for the block to be returned +4. Only THEN proceed with code modifications — within the declared scope only + +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. +``` + +--- + +## 5. Identified Interception Points + +### 5.1 Pre-Hook Interception Points + +These are locations where governance logic intercepts **BEFORE** an action occurs: + +| ID | Location | Intercepts | Current Flow | +| --------- | ---------------------------------------------------------------- | ------------------------- | ----------------------------------------------------- | +| **PRE-1** | `Task.recursivelyMakeClineRequests()` — before `createMessage()` | LLM requests | System prompt + messages assembled, about to call API | +| **PRE-2** | `presentAssistantMessage()` — before tool dispatch | All tool invocations | Tool name validated, about to call `tool.handle()` | +| **PRE-3** | `BaseTool.handle()` — before `execute()` | Individual tool execution | Params parsed, about to execute | +| **PRE-4** | `WriteToFileTool.execute()` — before `fs.writeFile()` | File write mutations | Path resolved, diff computed, approval received | +| **PRE-5** | `ExecuteCommandTool.execute()` — before terminal execution | Command execution | Command string known, approval received | +| **PRE-6** | `SYSTEM_PROMPT()` — during prompt assembly | System prompt content | All sections available, prompt being concatenated | +| **PRE-7** | `buildNativeToolsArray()` — during tools construction | Available tools list | Tools being filtered by mode | +| **PRE-8** | `Task.startTask()` — before first LLM call | Task initialization | User message known, about to enter loop | + +**We implemented: PRE-2** (before switch in `presentAssistantMessage.ts`) and **PRE-6** (system prompt injection). + +### 5.2 Post-Hook Interception Points + +These are locations where governance logic observes **AFTER** an action completes: + +| ID | Location | Observes | Current Flow | +| ---------- | --------------------------------------------------------------- | ----------------------- | ------------------------------------ | +| **POST-1** | `Task.recursivelyMakeClineRequests()` — after stream completion | LLM response content | Full assistant message available | +| **POST-2** | `presentAssistantMessage()` — after all tools dispatched | Completed tool results | All tool_results accumulated | +| **POST-3** | `BaseTool.handle()` — after `execute()` returns | Individual tool outcome | Tool completed or errored | +| **POST-4** | `WriteToFileTool.execute()` — after `fs.writeFile()` | File mutation evidence | File written, path and content known | +| **POST-5** | `ExecuteCommandTool.execute()` — after terminal output | Command output | Execution completed, output captured | +| **POST-6** | `Task.addToApiConversationHistory()` — after message saved | Conversation state | New message persisted to history | +| **POST-7** | `Task.saveClineMessages()` — after UI messages saved | UI message state | Cline messages persisted | +| **POST-8** | `Task.abortTask()` / completion | Task lifecycle end | Task finishing, all state available | + +**We implemented: POST-4 pattern via POST-2** (TraceLedger fires after `write_to_file` case in `presentAssistantMessage.ts`). + +### 5.3 State Injection Points (Before LLM Calls) + +These are locations where orchestration state can be injected into the LLM context: + +| ID | Location | Injection Target | Mechanism | +| --------- | ------------------------------------- | -------------------------- | ------------------------------------------------------ | +| **INJ-1** | `addCustomInstructions()` | System prompt | Append governance rules as custom instructions | +| **INJ-2** | `SYSTEM_PROMPT()` | System prompt sections | Add governance section alongside existing sections | +| **INJ-3** | `Task.recursivelyMakeClineRequests()` | User message content | Prepend governance context to `userContent[]` | +| **INJ-4** | `buildNativeToolsArray()` | Available tools definition | Add/modify/restrict tools based on active intent | +| **INJ-5** | `Task.startTask()` | Initial message | Inject intent selection requirement into first message | + +**We implemented: INJ-2** (injected `intentEnforcementSection` directly into `SYSTEM_PROMPT()`). + +--- + +## 6. Privilege Separation & Hook Middleware Boundary + +### 6.1 Three-Domain Privilege Separation + +| Domain | Privilege Level | Capabilities | Cannot Do | +| ---------------------------- | -------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------- | +| **Webview (UI)** | Restricted presentation | Render UI, emit events via `postMessage` | Access filesystem, invoke tools, call LLM APIs | +| **Extension Host (Logic)** | Core runtime | API polling, secret management, MCP tool execution, LLM calls | Mutate files without Hook Engine approval | +| **Hook Engine (Governance)** | Strict middleware boundary | Intercept all tool execution, enforce intent authorization, manage `.orchestration/` | Modify core logic, access Webview directly | + +The Hook Engine is the **only** component permitted to read/write the `.orchestration/` sidecar directory. + +### 6.2 Hook Engine Architecture + +```mermaid +flowchart LR + TC["Tool Call\narrives at PAM"] --> MUT{Is it a\nmutating\ntool?} + MUT -->|"No\n(read_file, etc.)"| PASS["✅ Pass Through\nNo hook needed"] + MUT -->|"Yes"| IG["IntentGate\nPre-Hook"] + IG --> HASINT{Active intent\ndeclared for\nthis task?} + HASINT -->|"No"| BLOCK1["🚫 BLOCKED\nCall select_active_intent"] + HASINT -->|"Yes"| SG["ScopeGuard\nPre-Hook"] + SG --> INSCOPE{Target file\nin owned_scope?} + INSCOPE -->|"No"| BLOCK2["🚫 BLOCKED\nScope Violation"] + INSCOPE -->|"Yes"| EXEC["✅ Execute Tool"] + EXEC --> POST["PostHook:\nTraceLedger\nSHA-256 + jsonl"] +``` + +### 6.3 Isolation Strategy: `src/hooks/` + +**Principle:** No governance logic SHALL exist inside `src/core/`, `src/api/`, or `src/services/`. All governance logic lives in `src/hooks/`. Core code receives minimal instrumentation — a single call to the Hook Engine at each interception point. + +``` +src/hooks/ +├── types.ts ← HookContext, HookResult, IntentState, TraceRecord +├── HookEngine.ts ← Singleton middleware. runPreHook() / runPostHook() +├── preHooks/ +│ ├── intentGate.ts ← Blocks mutating tools if no intent declared +│ └── scopeGuard.ts ← Blocks writes outside owned_scope +├── postHooks/ +│ └── traceLedger.ts ← SHA-256 hash + append to agent_trace.jsonl +└── utils/ + ├── contentHash.ts ← SHA-256 helper (Node.js crypto built-in) + ├── intentLoader.ts ← Parses active_intents.yaml (yaml package) + └── orchestrationPaths.ts ← Centralized .orchestration/ path resolution +``` + +**Changes to core files are limited to:** + +1. Importing the Hook Engine +2. Adding `hookEngine.runPreHook()` calls before operations +3. Adding `hookEngine.runPostHook()` calls after operations +4. Core logic flow, error handling, and data structures remain unchanged + +--- + +## 7. Sidecar Data Model (`.orchestration/`) + +The governance system uses a **Sidecar Storage Pattern** in `.orchestration/`. These files are machine-managed — created, read, and updated exclusively by the Hook Engine. + +```mermaid +graph LR + HE["HookEngine\n(Singleton)"] -->|"reads scope/constraints"| AY["active_intents.yaml"] + HE -->|"appends records"| JL["agent_trace.jsonl"] + HE -->|"updates on\nINTENT_EVOLUTION"| IM["intent_map.md"] + AY -->|"scope validation"| SG["ScopeGuard"] + AY -->|"intent context"| IG["IntentGate"] + JL -->|"audit trail"| EV["Evaluator / Human"] + IM -->|"spatial map"| EV +``` + +### 7.1 `active_intents.yaml` — The Intent Specification + +**Purpose:** Tracks the lifecycle of business requirements. Not all code changes are equal — this file tracks _why_ we are working. + +**Schema:** + +```yaml +active_intents: + - id: "INT-001" + name: "JWT Authentication Migration" + status: "IN_PROGRESS" # PENDING | IN_PROGRESS | BLOCKED | COMPLETED | ABANDONED + owned_scope: + - "src/auth/**" + - "src/middleware/jwt.ts" + constraints: + - "Must not use external auth providers" + - "Must maintain backward compatibility with Basic Auth" + acceptance_criteria: + - "Unit tests in tests/auth/ pass" + - "Integration tests verify backward compatibility" + assigned_agent: "agent-builder-01" + related_specs: + - type: "specification" + value: "REQ-001" + created_at: "2026-02-16T12:00:00Z" + updated_at: "2026-02-17T15:30:00Z" +``` + +**When `active_intents.yaml` Is Read:** + +| Trigger | Location | Purpose | +| -------------------------- | ------------------------------------- | ------------------------------------------------------- | +| **Handshake** | `select_active_intent` Pre-Hook | Query constraints + owned_scope for the selected intent | +| **Before tool execution** | `presentAssistantMessage()` via PRE-2 | Resolve scope boundaries, validate tool target | +| **On intent state change** | Hook Engine state management | When intent transitions lifecycle state | + +### 7.2 `agent_trace.jsonl` — The Ledger + +**Purpose:** An append-only, machine-readable history of every mutating action, linking the abstract **Intent** to the concrete **Code Hash**. + +**Full Agent Trace Specification (with Spatial Independence via Content Hashing):** + +```json +{ + "id": "uuid-v4", + "timestamp": "2026-02-16T12:00:00Z", + "intent_id": "INT-001", + "vcs": { "revision_id": "git_sha_hash" }, + "files": [ + { + "relative_path": "src/auth/middleware.ts", + "contributor": { + "entity_type": "AI", + "model_identifier": "claude-sonnet-4-6" + }, + "ranges": [ + { + "start_line": 15, + "end_line": 45, + "content_hash": "sha256:a8f5f167f44f4964e6c998dee827110c" + } + ], + "mutation_class": "AST_REFACTOR", + "related": [ + { "type": "specification", "value": "REQ-001" }, + { "type": "intent", "value": "INT-001" } + ] + } + ] +} +``` + +**Critical Design Properties:** + +- **Spatial Independence via Content Hashing:** The `content_hash` (SHA-256) is computed over the code block **content**, not line numbers. If lines move, the hash remains valid. +- **The Golden Thread:** The `related[]` array links each mutation back to specification requirements (`REQ-*`) and intents (`INT-*`): Business Requirement → Intent → Code Change. +- **Contributor Attribution:** Every trace records whether the change was AI or human, enabling provenance tracking. + +**When `agent_trace.jsonl` Is Written:** + +| Event | Trigger Location | Trace Contents | +| ------------------------ | -------------------------------- | ------------------------------------------------ | +| **File mutated** | POST-4 (after WriteToFileTool) | Full record with `files[].ranges[].content_hash` | +| **Intent declared** | `select_active_intent` execution | Intent ID, scope, agent ID, timestamp | +| **Governance violation** | Any pre-hook denial | Violation type, denied operation, intent ID | + +### 7.3 `intent_map.md` — The Spatial Map + +**Purpose:** Maps high-level business intents to physical files and AST nodes. When a stakeholder asks "Where is the billing logic?" or "What intent touched the auth middleware?", this file answers. + +**Update Pattern:** Incrementally updated when `INTENT_EVOLUTION` occurs — when files are mutated under an active intent or when an intent's `owned_scope` changes. + +--- + +## 8. Three-State Execution Flow + +The agent is **not allowed to write code immediately**. Every turn follows a mandatory Three-State Execution Flow: + +### 8.1 Hook Engine State Machine + +```mermaid +stateDiagram-v2 + [*] --> NoIntent: Task starts + + NoIntent --> NoIntent: read_file / list_files\n(safe tools — pass through) + NoIntent --> BLOCKED_NoIntent: write_to_file / execute_command\n(mutating without intent) + BLOCKED_NoIntent --> NoIntent: agent receives error\n"Call select_active_intent first" + + NoIntent --> IntentDeclared: select_active_intent(INT-001)\n✓ found in active_intents.yaml + + IntentDeclared --> ScopeCheck: mutating tool called + ScopeCheck --> ToolExecutes: file path ∈ owned_scope ✓ + ScopeCheck --> BLOCKED_Scope: file path ∉ owned_scope ✗ + + BLOCKED_Scope --> IntentDeclared: agent receives\n"Scope Violation" error + + ToolExecutes --> TraceWritten: PostHook fires\nSHA-256 + jsonl append + TraceWritten --> IntentDeclared: ready for next action + + IntentDeclared --> NoIntent: task completes\nclearIntent(taskId) +``` + +### 8.2 State Transition Mechanics + +**State 1 → State 2 (Request → Reasoning Intercept):** + +The governance layer forces the agent into the Reasoning Intercept by controlling the system prompt: + +1. **System prompt injection (INJ-2):** A governance section prepended: "You MUST call `select_active_intent` before performing any other action" +2. **Pre-hook enforcement (PRE-2):** Even if the LLM skips the handshake, the pre-hook rejects it with a `tool_error` + +**State 2 → State 3 (Reasoning Intercept → Contextualized Action):** + +1. Agent calls `select_active_intent(intent_id: "INT-001")` +2. Hook reads `active_intents.yaml` for INT-001's `constraints`, `owned_scope`, `acceptance_criteria` +3. Hook constructs `` XML and returns it to the LLM +4. Hook transitions state: intent is now active in `Map` +5. All subsequent tool calls pass through scope validation + +### 8.3 PostToolUse Mechanics + +```mermaid +flowchart TD + TE["Tool Execution Completes"] --> MT{Was it a\nmutating tool?} + MT -->|No| PASS["No post-hook needed"] + MT -->|Yes| TL["TraceLedger Post-Hook"] + TL --> H1["SHA-256(content)\ncontent_hash"] + TL --> H2["git rev-parse HEAD\nrevision_id"] + TL --> H3["isNewFile?\nINTENT_EVOLUTION\nvs AST_REFACTOR"] + H1 & H2 & H3 --> BUILD["Build TraceRecord JSON"] + BUILD --> APPEND["Append to agent_trace.jsonl"] + APPEND --> ERR{Error?} + ERR -->|Yes| LOG["Log error\nNEVER crash agent"] + ERR -->|No| DONE["Agent loop continues"] +``` + +--- + +## 9. Concurrency & Safety Injection Points + +### 9.1 Optimistic Locking (Phase 4 — Parallel Orchestration) + +Optimistic locking prevents concurrent agents from silently overwriting each other's changes: + +```mermaid +flowchart TD + A["Agent wants to write file"] --> B["Pre-Hook reads current file hash\n(lock acquisition)"] + B --> C["Agent performs work..."] + C --> D["Before writing: re-read current hash\n(at PRE-4)"] + D --> E{Hashes match?\nFile unchanged?} + E -->|"Yes ✓"| F["Write proceeds\nfs.writeFile()"] + E -->|"No ✗\nParallel agent modified"| G["🚫 BLOCKED\nStale File Error"] + G --> H["Agent must re-read file\nand reconcile changes"] + H --> C + F --> I["Post-Hook: append trace record\nwith new content hash"] +``` + +| Resource | Lock Granularity | Enforcement Point | Mechanism | +| ----------------- | ---------------- | ------------------------------------- | --------------------------------------------------------- | +| **Files (write)** | Per-file path | PRE-4 (before `fs.writeFile`) | Content hash comparison at lock acquisition vs write time | +| **Files (edit)** | Per-file path | PRE-3 (before `EditFileTool.execute`) | Same content hash mechanism | +| **Intent state** | Per-intent ID | Hook Engine state | YAML atomic read-modify-write with version counter | + +### 9.2 Scope Validation Points + +| Validation Point | Location | What Is Checked | +| ------------------- | --------------------------------------- | ------------------------------------------------------ | +| **File write path** | PRE-4 (`WriteToFileTool`) | Target path ∈ active intent's scope set | +| **File edit path** | PRE-3 (`EditFileTool`, `ApplyDiffTool`) | Target path ∈ active intent's scope set | +| **File read path** | PRE-3 (`ReadFileTool`) | Optional: warn if reading outside scope (non-blocking) | +| **Command CWD** | PRE-5 (`ExecuteCommandTool`) | Working directory ∈ active intent's scope set | +| **LLM request** | PRE-1 | Intent ID present in metadata. Scope still valid | + +### 9.3 Existing Safety Mechanisms (Preserved) + +| Mechanism | Location | Function | Governance Relationship | +| --------------------- | ----------------------------------- | --------------------------------- | ------------------------------------------------------------- | +| `validateToolUse()` | `src/core/tools/validateToolUse.ts` | Mode-based tool permission | Preserved. Governance adds intent-based permission on top | +| `askApproval()` | Tool callbacks | Human approval for mutations | Preserved. Governance pre-validates before approval requested | +| `AutoApprovalHandler` | `src/core/auto-approval/` | Automatic approval rules | Preserved. Auto-approval only fires if governance allows | +| `RooIgnore` | `src/core/ignore/` | .gitignore-style file exclusion | Preserved. Governance scope is additive | +| Checkpoint system | `src/core/checkpoints/` | File state snapshots for rollback | Essential for governance rollback on partial failures | + +--- + +## 10. Visual System Blueprints + +### 10.1 Traceability Chain (Intent → Code → Hash → Git) + +```mermaid +graph LR + BR["Business Requirement\n(user request)"] + INT["active_intents.yaml\nINT-001: JWT Auth Migration\nowned_scope: src/auth/**"] + SAI["select_active_intent\nHandshake Tool"] + CODE["src/auth/middleware.ts\n(written by agent)"] + HASH["SHA-256 Content Hash\nsha256:ab9f93b3..."] + GIT["Git Revision\nef49e624a"] + JSONL["agent_trace.jsonl\n{intent_id, file, hash, git_sha}"] + + BR -->|"formalized as"| INT + INT -->|"loaded by"| SAI + SAI -->|"authorizes"| CODE + CODE -->|"hashed by TraceLedger"| HASH + GIT -->|"captured at write time"| JSONL + HASH -->|"recorded in"| JSONL + INT -->|"referenced in"| JSONL +``` + +### 10.2 Data Model Class Diagram + +```mermaid +classDiagram + class ActiveIntent { + +String id + +String name + +String status + +String[] owned_scope + +String[] constraints + +String[] acceptance_criteria + +String assigned_agent + } + + class TraceRecord { + +String id (uuid-v4) + +String timestamp (ISO-8601) + +String intent_id + +VCS vcs + +FileTrace[] files + } + + class VCS { + +String revision_id (git SHA) + } + + class FileTrace { + +String relative_path + +Contributor contributor + +Range[] ranges + +String mutation_class + +Related[] related + } + + class Contributor { + +String entity_type (AI | HUMAN) + +String model_identifier + } + + class Range { + +Int start_line + +Int end_line + +String content_hash (sha256:hex) + } + + class HookEngine { + -Map intentStateMap + +getInstance() HookEngine + +setActiveIntent(taskId, intentId) + +getActiveIntentId(taskId) String + +runPreHook(ctx) HookResult + +runPostHook(ctx) void + +clearIntent(taskId) + } + + TraceRecord "1" --> "1" VCS + TraceRecord "1" --> "1..*" FileTrace + FileTrace "1" --> "1" Contributor + FileTrace "1" --> "1..*" Range + HookEngine ..> ActiveIntent : loads from YAML + HookEngine ..> TraceRecord : generates +``` + +--- + +## Appendix A: File Reference Map + +| Governance Concern | Primary Source Files | +| --------------------------- | -------------------------------------------------------------------- | +| Core execution loop | `src/core/task/Task.ts` (L2511–3743: `recursivelyMakeClineRequests`) | +| Tool dispatch / choke point | `src/core/assistant-message/presentAssistantMessage.ts` | +| Tool base class | `src/core/tools/BaseTool.ts` | +| File write tool | `src/core/tools/WriteToFileTool.ts` | +| Command tool | `src/core/tools/ExecuteCommandTool.ts` | +| Tool validation | `src/core/tools/validateToolUse.ts` | +| System prompt | `src/core/prompts/system.ts` | +| Prompt sections | `src/core/prompts/sections/` | +| Custom instructions | `src/core/prompts/sections/custom-instructions.ts` | +| Tools array builder | `src/core/task/build-tools.ts` | +| Native tool parser | `src/core/assistant-message/NativeToolCallParser.ts` | +| API handler factory | `src/api/index.ts` | +| API providers | `src/api/providers/` | +| Webview provider | `src/core/webview/ClineProvider.ts` | +| Auto-approval | `src/core/auto-approval/` | +| Terminal integration | `src/integrations/terminal/` | +| Tool name registry | `packages/types/src/tool.ts` | +| Hook Engine | `src/hooks/HookEngine.ts` | +| Intent Gate | `src/hooks/preHooks/intentGate.ts` | +| Scope Guard | `src/hooks/preHooks/scopeGuard.ts` | +| Trace Ledger | `src/hooks/postHooks/traceLedger.ts` | +| Handshake tool | `src/core/tools/SelectActiveIntentTool.ts` | +| Orchestration data | `.orchestration/active_intents.yaml` | +| Audit ledger | `.orchestration/agent_trace.jsonl` | +| Spatial map | `.orchestration/intent_map.md` | + +--- + +## Appendix B: Modification Impact Summary + +| Modification | Files Touched | Risk Level | Core Logic Changed? | +| --------------------------------------- | ------------------------------------------------------- | ---------- | ------------------------------------------------ | +| Hook Engine creation | `src/hooks/` (new directory) | Low | No — new code only | +| select_active_intent tool | `src/core/tools/SelectActiveIntentTool.ts` (new) | Low | No — follows existing BaseTool pattern | +| Tool name registration | `packages/types/src/tool.ts` | Low | Additive — one entry added to array | +| presentAssistantMessage instrumentation | `src/core/assistant-message/presentAssistantMessage.ts` | Medium | Minimal — pre/post hook calls at boundaries only | +| System prompt governance section | `src/core/prompts/system.ts` | Low | Additive — new section concatenated | +| TOOL_DISPLAY_NAMES update | `src/shared/tools.ts` | Low | Additive — one entry added to Record | +| Sidecar data directory | `.orchestration/` (new) | Low | No — data files only, not source code | + +--- + +_This document maps the existing Roo Code architecture for governance planning and implementation. All modifications follow the principle of **minimal core intrusion** and **maximum hook isolation** — core logic is wrapped, not rewritten._ diff --git a/package.json b/package.json index de8dff751cb..2070f8e6753 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,17 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.5", "zod": "3.25.76" - } + }, + "ignoredBuiltDependencies": [ + "@tailwindcss/oxide", + "@vscode/vsce-sign", + "better-sqlite3", + "core-js", + "esbuild", + "keytar", + "protobufjs", + "puppeteer-chromium-resolver", + "sharp" + ] } } diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 4f90b63e9fc..3ff7bf49154 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -46,6 +46,8 @@ export const toolNames = [ "skill", "generate_image", "custom_tool", + // Intent-Driven Governance: mandatory intent declaration tool + "select_active_intent", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7f5862be154..1f257acce87 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -37,10 +37,14 @@ import { generateImageTool } from "../tools/GenerateImageTool" import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool" import { isValidToolName, validateToolUse } from "../tools/validateToolUse" import { codebaseSearchTool } from "../tools/CodebaseSearchTool" +import { selectActiveIntentTool } from "../tools/SelectActiveIntentTool" import { formatResponse } from "../prompts/responses" import { sanitizeToolUseId } from "../../utils/tool-id" +// Intent-Driven Governance: Hook Engine middleware +import { hookEngine } from "../../hooks/HookEngine" + /** * Processes and presents assistant message content to the user interface. * @@ -675,7 +679,32 @@ export async function presentAssistantMessage(cline: Task) { } } + // ── Intent-Driven Governance: Pre-Hook ───────────────────────────────── + // Runs before every tool. Blocks mutating tools if no intent is declared, + // or if the target file is outside the active intent's owned_scope. + // select_active_intent itself is exempt from the gate check. + if (block.name !== "select_active_intent" && !block.partial) { + const preHookResult = await hookEngine.runPreHook( + block.name, + (block.nativeArgs ?? {}) as Record, + cline.taskId, + cline.cwd, + ) + if (preHookResult.blocked) { + pushToolResult(formatResponse.toolError(preHookResult.reason ?? "Blocked by governance hook.")) + break + } + } + // ── End Pre-Hook ──────────────────────────────────────────────────────── + switch (block.name) { + case "select_active_intent": + await selectActiveIntentTool.handle(cline, block as ToolUse<"select_active_intent">, { + askApproval, + handleError, + pushToolResult, + }) + break case "write_to_file": await checkpointSaveAndMark(cline) await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, { @@ -683,6 +712,18 @@ export async function presentAssistantMessage(cline: Task) { handleError, pushToolResult, }) + // ── Post-Hook: Trace Ledger ────────────────────────────────────────── + if (!block.partial) { + hookEngine + .runPostHook( + "write_to_file", + (block.nativeArgs ?? {}) as Record, + cline.taskId, + cline.cwd, + cline.api.getModel().id, + ) + .catch((err) => console.error("[PostHook] traceLedger error:", err)) + } break case "update_todo_list": await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, { diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 0d6071644a9..15f9619909c 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -82,6 +82,35 @@ async function generatePrompt( // Tools catalog is not included in the system prompt. const toolsCatalog = "" + // ── Intent-Driven Governance Protocol ──────────────────────────────────── + const intentEnforcementSection = ` +# Intent-Driven Governance Protocol + +You are operating under a strict **Intent-Driven Governance System**. The following rules are MANDATORY and enforced by the system — violations will be BLOCKED automatically. + +## The Handshake Rule (Non-Negotiable) + +You **CANNOT** write, edit, delete, or execute files immediately after receiving a user request. + +Your **FIRST action** for any code modification task MUST follow this exact sequence: + +1. **Analyze** the user's request and identify the relevant intent ID from \`.orchestration/active_intents.yaml\` +2. **Call** \`select_active_intent({ intent_id: "INT-XXX" })\` to load your authorized context +3. **Wait** for the \`\` block to be returned — it contains your constraints and scope +4. **Only then** proceed with code modifications — and ONLY within the declared \`owned_scope\` + +## What Gets Blocked + +- Calling \`write_to_file\`, \`apply_diff\`, \`edit\`, or \`execute_command\` WITHOUT first calling \`select_active_intent\` → **BLOCKED** +- Writing a file that is OUTSIDE your active intent's \`owned_scope\` → **BLOCKED** + +## Why This Exists + +Every change you make is cryptographically traced and linked to a business intent. This creates an auditable chain: **Business Intent → Your Action → Code Hash**. This is how trust is built without blind acceptance. + +If no active_intents.yaml exists yet, read \`.orchestration/active_intents.yaml\` first to understand what intents are defined, then call \`select_active_intent\` with the appropriate ID.` + // ── End Intent-Driven Governance Protocol ──────────────────────────────── + const basePrompt = `${roleDefinition} ${markdownFormattingSection()} @@ -99,6 +128,7 @@ ${getRulesSection(cwd, settings)} ${getSystemInfoSection(cwd)} ${getObjectiveSection()} +${intentEnforcementSection} ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), diff --git a/src/core/tools/SelectActiveIntentTool.ts b/src/core/tools/SelectActiveIntentTool.ts new file mode 100644 index 00000000000..0f32347dfb4 --- /dev/null +++ b/src/core/tools/SelectActiveIntentTool.ts @@ -0,0 +1,119 @@ +/** + * SelectActiveIntentTool — The Mandatory Handshake + * + * This tool is the FIRST thing the agent must call before modifying any file. + * It implements the "Two-Stage State Machine" from the architecture spec: + * + * Stage 1 (Request): User asks for a code change + * Stage 2 (Handshake): Agent calls select_active_intent → gets context injected + * Stage 3 (Action): Agent now writes code with full intent context + * + * What this tool does: + * 1. Reads .orchestration/active_intents.yaml + * 2. Finds the intent by ID + * 3. Registers it as the active intent in the HookEngine (per-task state) + * 4. Returns an XML block back to the LLM + * — the LLM now knows: what files it can touch, what constraints apply, + * and what "done" looks like + */ + +import { Task } from "../task/Task" +import { BaseTool, ToolCallbacks } from "./BaseTool" +import { findIntentById } from "../../hooks/utils/intentLoader" +import { hookEngine } from "../../hooks/HookEngine" +import { IntentState } from "../../hooks/types" +import type { ToolUse } from "../../shared/tools" + +interface SelectActiveIntentParams { + intent_id: string +} + +export class SelectActiveIntentTool extends BaseTool<"select_active_intent"> { + readonly name = "select_active_intent" as const + + async execute(params: SelectActiveIntentParams, task: Task, callbacks: ToolCallbacks): Promise { + const { pushToolResult } = callbacks + const { intent_id } = params + + if (!intent_id) { + pushToolResult( + `[select_active_intent] Error: 'intent_id' parameter is required.\n` + + `Please provide a valid intent ID from .orchestration/active_intents.yaml`, + ) + return + } + + // Load the intent from the YAML file + const intent = await findIntentById(task.cwd, intent_id) + + if (!intent) { + pushToolResult( + `[select_active_intent] Error: Intent '${intent_id}' not found in .orchestration/active_intents.yaml\n` + + `Available intent IDs can be found by reading: .orchestration/active_intents.yaml`, + ) + return + } + + if (intent.status === "COMPLETED" || intent.status === "CANCELLED") { + pushToolResult( + `[select_active_intent] Error: Intent '${intent_id}' has status '${intent.status}' and cannot be activated.\n` + + `Only IN_PROGRESS or PENDING intents can be selected.`, + ) + return + } + + // Register the active intent in the HookEngine (per-task state) + const intentState: IntentState = { + intentId: intent.id, + intentName: intent.name, + ownedScope: intent.owned_scope ?? [], + constraints: intent.constraints ?? [], + acceptanceCriteria: intent.acceptance_criteria ?? [], + activatedAt: new Date().toISOString(), + } + hookEngine.setActiveIntent(task.taskId, intentState) + + // Build the XML block for the LLM + // This is what gets injected into the model's context + const scopeXml = + intentState.ownedScope.length > 0 + ? `\n${intentState.ownedScope.map((p) => ` ${p}`).join("\n")}\n ` + : `` + + const constraintsXml = + intentState.constraints.length > 0 + ? `\n${intentState.constraints.map((c) => ` ${c}`).join("\n")}\n ` + : "" + + const criteriaXml = + intentState.acceptanceCriteria.length > 0 + ? `\n${intentState.acceptanceCriteria.map((c) => ` ${c}`).join("\n")}\n ` + : "" + + const intentContext = ` + + + ${scopeXml} + ${constraintsXml} + ${criteriaXml} + + You may ONLY modify files within the owned_scope paths listed above. + Any attempt to write outside this scope will be BLOCKED by the system. + All your changes will be traced and linked to intent ID: ${intent.id} + + + + +Intent '${intent.id}' is now active. You have loaded the context for: "${intent.name}". +You may now proceed with code modifications within the declared scope. +`.trim() + + pushToolResult(intentContext) + } + + override async handlePartial(task: Task, block: ToolUse<"select_active_intent">): Promise { + // No streaming UI needed for this tool + } +} + +export const selectActiveIntentTool = new SelectActiveIntentTool() diff --git a/src/hooks/HookEngine.ts b/src/hooks/HookEngine.ts new file mode 100644 index 00000000000..21ca2ad555b --- /dev/null +++ b/src/hooks/HookEngine.ts @@ -0,0 +1,163 @@ +/** + * Hook Engine — The Governance Middleware + * + * This is the central middleware layer that intercepts ALL tool executions. + * It is a singleton: one instance exists per extension activation. + * + * Architecture: + * Pre-Hooks → run BEFORE a tool executes → can BLOCK execution + * Post-Hooks → run AFTER a tool executes → record trace, update state + * + * Per-task state (which intent is active) is tracked in a Map. + * The Task object is NOT modified — state lives entirely in this engine. + * + * Insertion point in Roo Code: + * src/core/assistant-message/presentAssistantMessage.ts + * → before switch(block.name) [Pre-Hook] + * → after write_to_file completes [Post-Hook] + */ + +import { HookContext, HookResult, IntentState } from "./types" +import { runIntentGate } from "./preHooks/intentGate" +import { runScopeGuard } from "./preHooks/scopeGuard" +import { appendTraceRecord } from "./postHooks/traceLedger" + +export class HookEngine { + private static _instance: HookEngine | null = null + + /** + * Per-task active intent state. + * Key: taskId Value: IntentState (what was declared via select_active_intent) + */ + private intentStateMap = new Map() + + private constructor() {} + + /** + * Singleton accessor — always use this, never `new HookEngine()`. + */ + static getInstance(): HookEngine { + if (!HookEngine._instance) { + HookEngine._instance = new HookEngine() + } + return HookEngine._instance + } + + // ─── Intent State Management ──────────────────────────────────────────────── + + /** + * Set the active intent for a task (called when select_active_intent is executed). + */ + setActiveIntent(taskId: string, state: IntentState): void { + this.intentStateMap.set(taskId, state) + } + + /** + * Get the active intent ID for a task, or null if none is set. + */ + getActiveIntentId(taskId: string): string | null { + return this.intentStateMap.get(taskId)?.intentId ?? null + } + + /** + * Get the full intent state for a task. + */ + getIntentState(taskId: string): IntentState | null { + return this.intentStateMap.get(taskId) ?? null + } + + /** + * Clear the active intent for a task (called on task completion/reset). + */ + clearIntent(taskId: string): void { + this.intentStateMap.delete(taskId) + } + + // ─── Pre-Hook Chain ────────────────────────────────────────────────────────── + + /** + * Run all pre-hooks for a tool call. + * Returns the first blocking result found, or { blocked: false } if all pass. + * + * Pre-hooks run in order: + * 1. IntentGate — Is there any intent declared? + * 2. ScopeGuard — Is the target file in scope? + */ + async runPreHook( + toolName: string, + toolParams: Record, + taskId: string, + cwd: string, + ): Promise { + const ctx: HookContext = { + taskId, + cwd, + toolName, + toolParams, + activeIntentId: this.getActiveIntentId(taskId), + } + + // 1. Intent Gate: no intent = no mutating actions + const gateResult = await runIntentGate(ctx) + if (gateResult.blocked) { + return gateResult + } + + // 2. Scope Guard: file must be within declared scope + const scopeResult = await runScopeGuard(ctx) + if (scopeResult.blocked) { + return scopeResult + } + + return { blocked: false } + } + + // ─── Post-Hook Chain ───────────────────────────────────────────────────────── + + /** + * Run all post-hooks after a tool completes. + * Currently: append a trace record for file-writing tools. + * Post-hooks NEVER block — they record and move on. + */ + async runPostHook( + toolName: string, + toolParams: Record, + taskId: string, + cwd: string, + modelId: string, + ): Promise { + // Only trace file-writing operations + const fileWriteTools = new Set([ + "write_to_file", + "apply_diff", + "edit", + "search_and_replace", + "search_replace", + "edit_file", + "apply_patch", + ]) + + if (!fileWriteTools.has(toolName)) { + return + } + + const filePath = (toolParams.path as string | undefined) ?? "" + const content = (toolParams.content as string | undefined) ?? "" + + if (!filePath) { + return + } + + await appendTraceRecord({ + taskId, + cwd, + intentId: this.getActiveIntentId(taskId), + filePath, + content, + modelId, + }) + } +} + +// Export the singleton for use across the codebase +export const hookEngine = HookEngine.getInstance() diff --git a/src/hooks/postHooks/traceLedger.ts b/src/hooks/postHooks/traceLedger.ts new file mode 100644 index 00000000000..09f254b8564 --- /dev/null +++ b/src/hooks/postHooks/traceLedger.ts @@ -0,0 +1,115 @@ +/** + * Post-Hook: Trace Ledger + * + * After every file write, this hook: + * 1. Computes a SHA-256 hash of the written content (spatial independence) + * 2. Gets the current git commit SHA for VCS linkage + * 3. Classifies the change (AST_REFACTOR vs INTENT_EVOLUTION) + * 4. Appends a JSON record to .orchestration/agent_trace.jsonl + * + * This is the cryptographic proof that links: + * Business Intent → AI Action → Code Hash + */ + +import fs from "fs/promises" +import { execSync } from "child_process" +import { v4 as uuidv4 } from "uuid" +import { MutationClass, TraceRecord } from "../types" +import { computeContentHash, countLines } from "../utils/contentHash" +import { getTraceLedgerPath, getOrchestrationDir } from "../utils/orchestrationPaths" + +interface TraceLedgerContext { + taskId: string + cwd: string + intentId: string | null + filePath: string // relative path of the written file + content: string // the content that was written + modelId: string // e.g. "claude-3-5-sonnet" + mutationClass?: MutationClass +} + +/** + * Get the current git revision SHA (short). + * Returns "unknown" if git is not available. + */ +function getGitRevision(cwd: string): string { + try { + return execSync("git rev-parse --short HEAD", { cwd, stdio: ["pipe", "pipe", "pipe"] }) + .toString() + .trim() + } catch { + return "unknown" + } +} + +/** + * Classify the mutation type based on simple heuristics. + * A real implementation would use AST diffing. + * + * - INTENT_EVOLUTION: file is new (didn't exist before) → new feature + * - AST_REFACTOR: file existed → structural change preserving intent + */ +function classifyMutation(filePath: string, isNewFile: boolean): MutationClass { + if (isNewFile) { + return "INTENT_EVOLUTION" + } + return "AST_REFACTOR" +} + +/** + * Append a trace record to agent_trace.jsonl. + * Each line is a self-contained JSON object (JSONL format). + */ +export async function appendTraceRecord(ctx: TraceLedgerContext): Promise { + try { + // Ensure .orchestration/ directory exists + const orchestrationDir = getOrchestrationDir(ctx.cwd) + await fs.mkdir(orchestrationDir, { recursive: true }) + + const ledgerPath = getTraceLedgerPath(ctx.cwd) + + // Determine if this is a new file (for mutation classification) + let isNewFile = false + try { + await fs.access(`${ctx.cwd}/${ctx.filePath}`) + } catch { + isNewFile = true + } + + const contentHash = computeContentHash(ctx.content) + const lineCount = countLines(ctx.content) + const mutationClass = ctx.mutationClass ?? classifyMutation(ctx.filePath, isNewFile) + const gitRevision = getGitRevision(ctx.cwd) + + const record: TraceRecord = { + id: uuidv4(), + timestamp: new Date().toISOString(), + intent_id: ctx.intentId, + vcs: { revision_id: gitRevision }, + files: [ + { + relative_path: ctx.filePath, + contributor: { + entity_type: "AI", + model_identifier: ctx.modelId, + }, + ranges: [ + { + start_line: 1, + end_line: lineCount, + content_hash: contentHash, + }, + ], + mutation_class: mutationClass, + related: ctx.intentId ? [{ type: "specification", value: ctx.intentId }] : [], + }, + ], + } + + // Append one JSON line (JSONL = one record per line, append-only) + await fs.appendFile(ledgerPath, JSON.stringify(record) + "\n", "utf-8") + } catch (error) { + // Trace failure must NOT crash the agent — log and continue + console.error("[TraceLedger] Failed to append trace record:", error) + } +} diff --git a/src/hooks/preHooks/intentGate.ts b/src/hooks/preHooks/intentGate.ts new file mode 100644 index 00000000000..34ee6c45d28 --- /dev/null +++ b/src/hooks/preHooks/intentGate.ts @@ -0,0 +1,37 @@ +/** + * Pre-Hook: Intent Gate + * + * The fundamental governance rule: an agent CANNOT mutate the codebase + * without first declaring a valid active intent via select_active_intent(). + * + * If the agent tries to write a file or run a command without having + * declared an intent, this hook BLOCKS the action and returns an error + * that the LLM can understand and self-correct from. + */ + +import { HookContext, HookResult, MUTATING_TOOLS } from "../types" + +/** + * Run the intent gate check. + * Returns { blocked: true } if a mutating tool is called without an active intent. + */ +export async function runIntentGate(ctx: HookContext): Promise { + // Only enforce on mutating tools + if (!MUTATING_TOOLS.has(ctx.toolName as any)) { + return { blocked: false } + } + + // If there is no active intent, block and explain clearly + if (!ctx.activeIntentId) { + return { + blocked: true, + reason: + `[Intent Gate] BLOCKED: You attempted to call '${ctx.toolName}' without a declared intent.\n` + + `You MUST first call 'select_active_intent' with a valid intent ID from ` + + `.orchestration/active_intents.yaml before modifying any files.\n` + + `Example: select_active_intent({ intent_id: "INT-001" })`, + } + } + + return { blocked: false } +} diff --git a/src/hooks/preHooks/scopeGuard.ts b/src/hooks/preHooks/scopeGuard.ts new file mode 100644 index 00000000000..5c4136895a8 --- /dev/null +++ b/src/hooks/preHooks/scopeGuard.ts @@ -0,0 +1,80 @@ +/** + * Pre-Hook: Scope Guard + * + * Enforces the owned_scope declared in active_intents.yaml. + * Even if an intent is declared, the agent can only modify files + * that are explicitly within that intent's scope. + * + * This prevents agents from "drifting" into unrelated code while + * claiming to work on a specific intent. + */ + +import { HookContext, HookResult } from "../types" +import { findIntentById, isPathInScope } from "../utils/intentLoader" + +// Tools that write to a specific file path (we check their 'path' param) +const FILE_WRITE_TOOLS = new Set([ + "write_to_file", + "apply_diff", + "edit", + "search_and_replace", + "search_replace", + "edit_file", + "apply_patch", +]) + +/** + * Run the scope guard check. + * Returns { blocked: true } if the target file is outside the active intent's scope. + */ +export async function runScopeGuard(ctx: HookContext): Promise { + // Only enforce on file-writing tools + if (!FILE_WRITE_TOOLS.has(ctx.toolName)) { + return { blocked: false } + } + + // No active intent means intentGate already blocked this — skip + if (!ctx.activeIntentId) { + return { blocked: false } + } + + // Extract the target file path from tool params + const targetPath = (ctx.toolParams.path as string | undefined) ?? "" + if (!targetPath) { + return { blocked: false } + } + + // Load the active intent to get its scope + const intent = await findIntentById(ctx.cwd, ctx.activeIntentId) + if (!intent) { + return { + blocked: true, + reason: + `[Scope Guard] BLOCKED: Active intent '${ctx.activeIntentId}' not found in ` + + `.orchestration/active_intents.yaml. The intent may have been removed or renamed. ` + + `Call select_active_intent again with a valid ID.`, + } + } + + // If scope is undefined or empty, allow (no restriction defined) + if (!intent.owned_scope || intent.owned_scope.length === 0) { + return { blocked: false } + } + + // Check if the target file is within the declared scope + if (!isPathInScope(targetPath, intent.owned_scope)) { + return { + blocked: true, + reason: + `[Scope Guard] BLOCKED: Scope Violation.\n` + + `Intent '${ctx.activeIntentId}' (${intent.name}) is NOT authorized to edit: ${targetPath}\n` + + `Authorized scope:\n` + + intent.owned_scope.map((s) => ` - ${s}`).join("\n") + + `\nTo modify this file, either:\n` + + ` 1. Switch to a different intent that owns this file, or\n` + + ` 2. Request a scope expansion for intent ${ctx.activeIntentId}.`, + } + } + + return { blocked: false } +} diff --git a/src/hooks/types.ts b/src/hooks/types.ts new file mode 100644 index 00000000000..ead503ce80a --- /dev/null +++ b/src/hooks/types.ts @@ -0,0 +1,106 @@ +/** + * Hook Engine Types + * Shared types for the Intent-Driven Governance Hook System. + */ + +// The set of tool names that mutate the codebase and REQUIRE an active intent. +export const MUTATING_TOOLS = new Set([ + "write_to_file", + "apply_diff", + "edit", + "search_and_replace", + "search_replace", + "edit_file", + "apply_patch", + "execute_command", +] as const) + +// The set of tools that are purely destructive (need extra HITL warning). +export const DESTRUCTIVE_TOOLS = new Set(["execute_command"] as const) + +// Tools that set intent (exempt from the intent gate check). +export const INTENT_TOOLS = new Set(["select_active_intent"] as const) + +/** + * The context passed to every hook. + * Contains everything the hook needs to make a decision. + */ +export interface HookContext { + taskId: string + cwd: string // workspace root path + toolName: string + toolParams: Record + activeIntentId: string | null // currently declared intent for this task +} + +/** + * The result a hook returns. + * If blocked=true, execution is stopped and reason is returned to the LLM. + */ +export interface HookResult { + blocked: boolean + reason?: string +} + +/** + * Per-task intent state tracked by the HookEngine. + */ +export interface IntentState { + intentId: string + intentName: string + ownedScope: string[] + constraints: string[] + acceptanceCriteria: string[] + activatedAt: string // ISO timestamp +} + +/** + * A single intent as parsed from active_intents.yaml. + */ +export interface ActiveIntent { + id: string + name: string + status: string + owned_scope: string[] + constraints?: string[] + acceptance_criteria?: string[] +} + +/** + * The full structure of active_intents.yaml. + */ +export interface ActiveIntentsFile { + active_intents: ActiveIntent[] +} + +/** + * Classification of a mutation for the trace ledger. + */ +export type MutationClass = "AST_REFACTOR" | "INTENT_EVOLUTION" | "BUG_FIX" | "UNKNOWN" + +/** + * A single record appended to agent_trace.jsonl. + */ +export interface TraceRecord { + id: string + timestamp: string + intent_id: string | null + vcs: { revision_id: string } + files: Array<{ + relative_path: string + contributor: { + entity_type: "AI" | "HUMAN" + model_identifier: string + } + ranges: Array<{ + start_line: number + end_line: number + content_hash: string + }> + mutation_class: MutationClass + related: Array<{ + type: string + value: string + }> + }> +} diff --git a/src/hooks/utils/contentHash.ts b/src/hooks/utils/contentHash.ts new file mode 100644 index 00000000000..bcbc7fe5100 --- /dev/null +++ b/src/hooks/utils/contentHash.ts @@ -0,0 +1,25 @@ +/** + * Content hashing utility for spatial independence. + * + * The key insight: line numbers shift when code is refactored. + * A SHA-256 hash of the actual content does NOT change with line shifts. + * This means we can always find and verify a code block even after refactoring. + */ + +import crypto from "crypto" + +/** + * Compute a SHA-256 hash of a string content block. + * Returns the hash as "sha256:" for clear identification. + */ +export function computeContentHash(content: string): string { + const hash = crypto.createHash("sha256").update(content, "utf8").digest("hex") + return `sha256:${hash}` +} + +/** + * Count lines in a string (1-based end line number). + */ +export function countLines(content: string): number { + return content.split("\n").length +} diff --git a/src/hooks/utils/intentLoader.ts b/src/hooks/utils/intentLoader.ts new file mode 100644 index 00000000000..27045e51813 --- /dev/null +++ b/src/hooks/utils/intentLoader.ts @@ -0,0 +1,70 @@ +/** + * Reads and parses .orchestration/active_intents.yaml. + * + * This is the single source of truth for what work is authorized. + * Every pre-hook reads from here; it is never written by hooks directly + * (humans maintain it, or a future tool updates it). + */ + +import fs from "fs/promises" +import { parse as parseYaml } from "yaml" +import { ActiveIntent, ActiveIntentsFile } from "../types" +import { getActiveIntentsPath } from "./orchestrationPaths" + +/** + * Load all active intents from the workspace's active_intents.yaml. + * Returns an empty array if the file does not exist (graceful degradation). + */ +export async function loadActiveIntents(cwd: string): Promise { + const filePath = getActiveIntentsPath(cwd) + try { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = parseYaml(raw) as ActiveIntentsFile + return parsed?.active_intents ?? [] + } catch { + // File doesn't exist or is invalid YAML — governance cannot be enforced + return [] + } +} + +/** + * Find a specific intent by its ID. + * Returns null if not found or file doesn't exist. + */ +export async function findIntentById(cwd: string, intentId: string): Promise { + const intents = await loadActiveIntents(cwd) + return intents.find((i) => i.id === intentId) ?? null +} + +/** + * Check whether a file path falls within any of the intent's owned_scope patterns. + * + * Scope matching rules: + * - "src/auth/**" matches any file under src/auth/ + * - "src/middleware/jwt.ts" matches exactly that file + * - Paths are compared using normalized forward slashes + */ +export function isPathInScope(filePath: string, ownedScope: string[]): boolean { + // Normalize to forward slashes for cross-platform consistency + const normalized = filePath.replace(/\\/g, "/") + + return ownedScope.some((pattern) => { + const normalizedPattern = pattern.replace(/\\/g, "/") + + // Glob-style: pattern ends with /** — match any file under the prefix + if (normalizedPattern.endsWith("/**")) { + const prefix = normalizedPattern.slice(0, -3) + return normalized === prefix || normalized.startsWith(prefix + "/") + } + + // Glob-style: pattern ends with /* — match any direct child + if (normalizedPattern.endsWith("/*")) { + const prefix = normalizedPattern.slice(0, -2) + const rest = normalized.slice(prefix.length + 1) + return normalized.startsWith(prefix + "/") && !rest.includes("/") + } + + // Exact match + return normalized === normalizedPattern + }) +} diff --git a/src/hooks/utils/orchestrationPaths.ts b/src/hooks/utils/orchestrationPaths.ts new file mode 100644 index 00000000000..100c86a314e --- /dev/null +++ b/src/hooks/utils/orchestrationPaths.ts @@ -0,0 +1,24 @@ +/** + * Centralizes all .orchestration/ path resolution. + * Every hook reads/writes through these helpers — no magic strings elsewhere. + */ + +import path from "path" + +export const ORCHESTRATION_DIR = ".orchestration" + +export function getOrchestrationDir(cwd: string): string { + return path.join(cwd, ORCHESTRATION_DIR) +} + +export function getActiveIntentsPath(cwd: string): string { + return path.join(cwd, ORCHESTRATION_DIR, "active_intents.yaml") +} + +export function getTraceLedgerPath(cwd: string): string { + return path.join(cwd, ORCHESTRATION_DIR, "agent_trace.jsonl") +} + +export function getIntentMapPath(cwd: string): string { + return path.join(cwd, ORCHESTRATION_DIR, "intent_map.md") +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 491ba693611..a92cd5d22f4 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -289,6 +289,7 @@ export const TOOL_DISPLAY_NAMES: Record = { skill: "load skill", generate_image: "generate images", custom_tool: "use custom tools", + select_active_intent: "declare active intent", } as const // Define available tool groups.