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.