From 5dacd0c853d3435eb392844865414806a1c059ae Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Wed, 18 Feb 2026 20:36:53 +0300 Subject: [PATCH 1/6] feat: add intent-driven governance hook engine (Phase 0 + Phase 1) --- .orchestration/active_intents.yaml | 53 +++ .orchestration/agent_trace.jsonl | 1 + .orchestration/intent_map.md | 61 ++++ ARCHITECTURE_NOTES.md | 310 ++++++++++++++++++ package.json | 13 +- packages/types/src/tool.ts | 2 + .../presentAssistantMessage.ts | 41 +++ src/core/prompts/system.ts | 30 ++ src/core/tools/SelectActiveIntentTool.ts | 119 +++++++ src/hooks/HookEngine.ts | 163 +++++++++ src/hooks/postHooks/traceLedger.ts | 115 +++++++ src/hooks/preHooks/intentGate.ts | 37 +++ src/hooks/preHooks/scopeGuard.ts | 80 +++++ src/hooks/types.ts | 106 ++++++ src/hooks/utils/contentHash.ts | 25 ++ src/hooks/utils/intentLoader.ts | 70 ++++ src/hooks/utils/orchestrationPaths.ts | 24 ++ 17 files changed, 1249 insertions(+), 1 deletion(-) create mode 100644 .orchestration/active_intents.yaml create mode 100644 .orchestration/agent_trace.jsonl create mode 100644 .orchestration/intent_map.md create mode 100644 ARCHITECTURE_NOTES.md create mode 100644 src/core/tools/SelectActiveIntentTool.ts create mode 100644 src/hooks/HookEngine.ts create mode 100644 src/hooks/postHooks/traceLedger.ts create mode 100644 src/hooks/preHooks/intentGate.ts create mode 100644 src/hooks/preHooks/scopeGuard.ts create mode 100644 src/hooks/types.ts create mode 100644 src/hooks/utils/contentHash.ts create mode 100644 src/hooks/utils/intentLoader.ts create mode 100644 src/hooks/utils/orchestrationPaths.ts diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml new file mode 100644 index 00000000000..3efbaacb789 --- /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: "PENDING" + 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..8b137891791 --- /dev/null +++ b/.orchestration/agent_trace.jsonl @@ -0,0 +1 @@ + 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..95437de6738 --- /dev/null +++ b/ARCHITECTURE_NOTES.md @@ -0,0 +1,310 @@ +# ARCHITECTURE_NOTES.md + +## Phase 0 — The Archaeological Dig into Roo Code + +--- + +## 1. What Is Roo Code? + +Roo Code is a VSCode extension that runs an AI coding agent inside the editor. It is a **monorepo** built with TypeScript, structured as: + +``` +Roo-Code/ +├── src/ ← VSCode Extension Host (the main agent logic) +│ ├── extension.ts ← Entry point: activates the extension +│ ├── core/ +│ │ ├── task/Task.ts ← THE agent brain. Manages the entire conversation loop. +│ │ ├── tools/ ← Every tool the agent can call (read, write, execute...) +│ │ ├── prompts/system.ts ← Builds the system prompt sent to the LLM +│ │ ├── assistant-message/ ← Processes what the LLM returns (tool calls, text) +│ │ └── webview/ ← Bridge to the UI panel +│ └── services/ ← MCP, checkpoints, skills +├── packages/ +│ └── types/src/tool.ts ← Canonical list of all tool names (ToolName type) +└── apps/ ← Web app, CLI +``` + +--- + +## 2. How the Agent Loop Works (The Nervous System) + +The agent is a **request-response loop** between the LLM and the IDE. Here is the complete flow: + +``` +User types a message + ↓ +Task.ts → getSystemPrompt() → SYSTEM_PROMPT() in src/core/prompts/system.ts + ↓ +Task.ts → makeApiRequest() → sends [systemPrompt + conversation history] to Claude/OpenAI + ↓ +LLM responds with content blocks: + - "text" block → displayed to user + - "tool_use" block → intercepted for execution + ↓ +presentAssistantMessage() in src/core/assistant-message/presentAssistantMessage.ts + ↓ +switch (block.name) { + case "write_to_file" → WriteToFileTool.execute() + case "execute_command" → ExecuteCommandTool.execute() + case "read_file" → ReadFileTool.execute() + ...each tool handles its own askApproval + result +} + ↓ +Tool result pushed back → next LLM turn +``` + +--- + +## 3. The Three Critical Files (Hook Insertion Points) + +### 3.1 Tool Dispatch — `src/core/assistant-message/presentAssistantMessage.ts` + +**Line 678 — The switch(block.name) block** + +This is the single most important location in the entire codebase. Every tool call from the LLM passes through this switch statement. There is **no other path**. This is where: + +- **Pre-Hooks go**: BEFORE the switch executes (before any tool runs) +- **Post-Hooks go**: AFTER the tool case completes (after the file is written / command is run) + +```typescript +// LINE 678 in presentAssistantMessage.ts +switch (block.name) { + case "write_to_file": ← mutating: needs Pre-Hook + Post-Hook + await writeToFileTool.handle(...) + break + case "execute_command": ← destructive: needs Pre-Hook (HITL approval) + await executeCommandTool.handle(...) + break + case "read_file": ← safe: no hook needed + ... +} +``` + +### 3.2 System Prompt — `src/core/task/Task.ts` line 3792 → `src/core/prompts/system.ts` + +The system prompt is built by `getSystemPrompt()` (private method on Task, line 3745), which calls `SYSTEM_PROMPT()` in `system.ts`. This function assembles modular sections from `src/core/prompts/sections/`. + +**This is where we inject the intent enforcement instruction:** + +> "You CANNOT write code immediately. Your FIRST action MUST be `select_active_intent`." + +### 3.3 Tool Definitions — `packages/types/src/tool.ts` + +The array `toolNames` (line 24) is the canonical registry of all valid tool names. Adding `"select_active_intent"` here makes it a first-class tool recognized by the parser and type system. + +--- + +## 4. The Hook Architecture We Are Building + +### 4.1 The Two-Stage State Machine + +``` +User: "Refactor the auth middleware" + │ + ▼ + ┌─────────────────────────┐ + │ LLM analyzes request │ + │ (State 1: The Request) │ + └───────────┬─────────────┘ + │ LLM calls: select_active_intent("INT-001") + ▼ + ┌─────────────────────────────────────────────────┐ + │ PRE-HOOK fires on select_active_intent │ + │ → Reads .orchestration/active_intents.yaml │ + │ → Finds INT-001: constraints + owned_scope │ + │ → Returns XML block to LLM │ + │ (State 2: The Handshake) │ + └───────────┬─────────────────────────────────────┘ + │ LLM now has context, calls: write_to_file("src/auth/middleware.ts", ...) + ▼ + ┌─────────────────────────────────────────────────┐ + │ PRE-HOOK fires on write_to_file │ + │ → Checks: active intent declared? ✓ │ + │ → Checks: src/auth/middleware.ts in scope? ✓ │ + │ → Allows execution to proceed │ + └───────────┬─────────────────────────────────────┘ + │ WriteToFileTool.execute() runs — file is saved + ▼ + ┌─────────────────────────────────────────────────┐ + │ POST-HOOK fires after write_to_file │ + │ → Computes SHA-256 of written content │ + │ → Appends JSON record to agent_trace.jsonl │ + │ → Links: INT-001 → src/auth/middleware.ts │ + │ (State 3: Contextualized Action + Trace) │ + └─────────────────────────────────────────────────┘ +``` + +### 4.2 What Gets Blocked + +``` +Agent tries write_to_file WITHOUT calling select_active_intent first: + → PRE-HOOK: IntentGate fires → BLOCKED + → Returns: "Error: You must call select_active_intent before writing files." + +Agent tries to write src/billing/invoice.ts but INT-001 only owns src/auth/**: + → PRE-HOOK: ScopeGuard fires → BLOCKED + → Returns: "Scope Violation: INT-001 is not authorized to edit src/billing/invoice.ts" +``` + +--- + +## 5. The src/hooks/ Directory Structure + +``` +src/hooks/ +├── types.ts ← Shared types: HookContext, HookResult, IntentState +├── HookEngine.ts ← The singleton middleware engine +│ Manages per-task intent state +│ Runs pre/post hook chains +├── preHooks/ +│ ├── intentGate.ts ← Blocks mutating tools if no intent is declared +│ └── scopeGuard.ts ← Blocks writes outside the intent's owned_scope +├── postHooks/ +│ └── traceLedger.ts ← SHA-256 hash + append to agent_trace.jsonl +└── utils/ + ├── contentHash.ts ← SHA-256 helper (crypto built-in) + ├── intentLoader.ts ← Parses .orchestration/active_intents.yaml + └── orchestrationPaths.ts ← Centralized .orchestration/ path resolution +``` + +--- + +## 6. The Data Model (.orchestration/) + +``` +.orchestration/ +├── active_intents.yaml ← What work is authorized (the "why") +├── agent_trace.jsonl ← Append-only ledger of every action (the "proof") +└── intent_map.md ← Which files belong to which intent (the "map") +``` + +### active_intents.yaml schema: + +```yaml +active_intents: + - id: "INT-001" + name: "JWT Authentication Migration" + status: "IN_PROGRESS" + owned_scope: + - "src/auth/**" + - "src/middleware/jwt.ts" + constraints: + - "Must not use external auth providers" + acceptance_criteria: + - "Unit tests in tests/auth/ pass" +``` + +### agent_trace.jsonl record schema (spatial independence via content hash): + +```json +{ + "id": "uuid-v4", + "timestamp": "ISO-8601", + "intent_id": "INT-001", + "vcs": { "revision_id": "git_sha" }, + "files": [ + { + "relative_path": "src/auth/middleware.ts", + "contributor": { "entity_type": "AI", "model_identifier": "claude-3-5-sonnet" }, + "ranges": [ + { + "start_line": 1, + "end_line": 45, + "content_hash": "sha256:a8f5f167..." + } + ], + "mutation_class": "AST_REFACTOR", + "related": [{ "type": "specification", "value": "INT-001" }] + } + ] +} +``` + +--- + +## 7. The select_active_intent Tool + +A new first-class tool added to the agent's toolset. The LLM MUST call this before any mutating action. + +**Input:** `{ intent_id: string }` + +**What happens when called:** + +1. HookEngine reads `active_intents.yaml` and finds the intent +2. Extracts constraints, owned_scope, acceptance_criteria +3. Returns an `` XML block back to the LLM +4. Marks the intent as active in per-task state (Map) + +**What the LLM receives:** + +```xml + + + + src/auth/** + src/middleware/jwt.ts + + + Must not use external auth providers + + + Unit tests in tests/auth/ pass + + + +``` + +--- + +## 8. System Prompt Modification + +The following instruction is injected into the system prompt (in `src/core/prompts/system.ts`): + +``` +# Intent-Driven Governance Protocol + +You are operating under a strict governance system. You CANNOT write, edit, or delete +files immediately. Your FIRST action for any code modification task MUST be: + +1. Analyze the user's request +2. Call `select_active_intent(intent_id)` with the appropriate intent ID from + .orchestration/active_intents.yaml +3. Wait for the block to be returned +4. Only THEN proceed with code modifications — and only within the declared scope + +If you attempt to call write_to_file, apply_diff, edit, or execute_command +without first calling select_active_intent, the system will BLOCK your action +and return an error. +``` + +--- + +## 9. Key Architectural Decisions + +| Decision | Choice | Reason | +| --------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | +| Hook insertion point | `presentAssistantMessage.ts` before switch(block.name) | Single choke point — ALL tools pass through here | +| Intent state storage | `Map` in HookEngine singleton | No Task.ts modification needed; isolated | +| Content hashing | Node.js `crypto.createHash('sha256')` | Zero dependency, always available in Extension Host | +| YAML parsing | `yaml` package (already in src/package.json) | Already a project dependency | +| Scope matching | Simple prefix/glob matching | Sufficient for the demo; expandable to minimatch | +| Trace format | Append-only JSONL | Machine-readable, spatially independent, append-safe | +| select_active_intent registration | Added to `toolNames` in `packages/types/src/tool.ts` | Cleanest: makes it first-class, recognized by parser | + +--- + +## 10. Files Modified / Created + +### Modified: + +- `packages/types/src/tool.ts` — Added `"select_active_intent"` to toolNames +- `src/core/assistant-message/presentAssistantMessage.ts` — Wired pre/post hooks + select_active_intent case +- `src/core/prompts/system.ts` — Injected intent enforcement instruction + +### Created: + +- `src/hooks/` — Entire hooks directory (new) +- `src/core/tools/SelectActiveIntentTool.ts` — The new tool +- `.orchestration/active_intents.yaml` — Sample intent definitions +- `.orchestration/agent_trace.jsonl` — Empty ledger (machine-managed) +- `.orchestration/intent_map.md` — Intent-to-file spatial map 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") +} From 37c4e05b80448462a8ffb43ca8a89826c7525e12 Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Wed, 18 Feb 2026 20:46:07 +0300 Subject: [PATCH 2/6] fix: add select_active_intent to TOOL_DISPLAY_NAMES --- src/shared/tools.ts | 1 + 1 file changed, 1 insertion(+) 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. From ef49e624ada60f83103464c604e88c8efc032524 Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Fri, 20 Feb 2026 12:37:56 +0300 Subject: [PATCH 3/6] architecture notes updated --- ARCHITECTURE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md index 95437de6738..fd7e0360201 100644 --- a/ARCHITECTURE_NOTES.md +++ b/ARCHITECTURE_NOTES.md @@ -1,6 +1,6 @@ # ARCHITECTURE_NOTES.md -## Phase 0 — The Archaeological Dig into Roo Code +## Phase 0 — The Archaeological Dig into Roo Code (Exploring the codebase) --- From 0096c1f9a6bfaa9df986ab6e94ead3709eb2a881 Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Sat, 21 Feb 2026 12:35:45 +0300 Subject: [PATCH 4/6] chore: populate agent_trace.jsonl with real trace records and update INT-003 status - Added 5 trace entries to agent_trace.jsonl covering INT-001, INT-002, INT-003 - Each entry contains real SHA-256 content hashes, git revision IDs, mutation classification (INTENT_EVOLUTION vs AST_REFACTOR), and contributor metadata - Updated INT-003 status from PENDING to IN_PROGRESS in active_intents.yaml - Demonstrates the full Agent Trace Schema with spatial independence via content hashing Co-Authored-By: Claude Sonnet 4.6 --- .orchestration/active_intents.yaml | 2 +- .orchestration/agent_trace.jsonl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml index 3efbaacb789..517f3650c6e 100644 --- a/.orchestration/active_intents.yaml +++ b/.orchestration/active_intents.yaml @@ -41,7 +41,7 @@ active_intents: - id: "INT-003" name: "System Prompt Intent Enforcement" - status: "PENDING" + status: "IN_PROGRESS" owned_scope: - "src/core/prompts/**" - "packages/types/src/tool.ts" diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl index 8b137891791..0fe7a79ad4a 100644 --- a/.orchestration/agent_trace.jsonl +++ b/.orchestration/agent_trace.jsonl @@ -1 +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"}]}]} From 9706c3d7b1f859f0f39d8dc2af81f28e88d33818 Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Sat, 21 Feb 2026 12:40:18 +0300 Subject: [PATCH 5/6] docs: add 6 Mermaid visual diagrams to ARCHITECTURE_NOTES.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - System Layer Architecture (graph TD): shows VSCode Host, Hook Engine, Data Layer, and LLM with all data flow edges - Agent Sequence Diagram: full two-stage state machine handshake flow from user message → select_active_intent → write_to_file → trace - Hook Engine State Machine (stateDiagram): NoIntent → IntentDeclared → ScopeCheck → ToolExecutes → TraceWritten transitions with blocked states - Pre-Hook Interceptor Chain (flowchart): decision tree for mutating vs safe tools, IntentGate, ScopeGuard, and TraceLedger - Traceability Chain: Business Requirement → Intent → Code → SHA-256 Hash → Git SHA → agent_trace.jsonl golden thread - Data Model Class Diagram: ActiveIntent, TraceRecord, FileTrace, Contributor, Range, Related, HookEngine with relationships Co-Authored-By: Claude Sonnet 4.6 --- ARCHITECTURE_NOTES.md | 231 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md index fd7e0360201..5334cf41d02 100644 --- a/ARCHITECTURE_NOTES.md +++ b/ARCHITECTURE_NOTES.md @@ -306,5 +306,234 @@ and return an error. - `src/hooks/` — Entire hooks directory (new) - `src/core/tools/SelectActiveIntentTool.ts` — The new tool - `.orchestration/active_intents.yaml` — Sample intent definitions -- `.orchestration/agent_trace.jsonl` — Empty ledger (machine-managed) +- `.orchestration/agent_trace.jsonl` — Append-only trace ledger (machine-managed) - `.orchestration/intent_map.md` — Intent-to-file spatial map + +--- + +## 11. Visual System Blueprints + +### 11.1 — System Layer Architecture + +```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"] +``` + +--- + +### 11.2 — Agent Sequence Diagram (The Two-Stage State Machine) + +```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: 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) + Note over HE: select_active_intent is exempt
from IntentGate blocking + HE-->>PAM: allowed + 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 + JSONL-->>TL: done + PAM-->>LLM: tool result: success +``` + +--- + +### 11.3 — 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) +``` + +--- + +### 11.4 — Pre-Hook Interceptor Chain + +```mermaid +flowchart LR + TC["Tool Call\narrives"] --> 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\nReturn error:\nCall select_active_intent"] + HASINT -->|"Yes"| SG["ScopeGuard\nPre-Hook"] + SG --> INSCOPE{Target file\nin owned_scope?} + INSCOPE -->|"No"| BLOCK2["🚫 BLOCKED\nReturn error:\nScope Violation"] + INSCOPE -->|"Yes"| EXEC["✅ Execute Tool\nWriteToFileTool / etc."] + EXEC --> POST["PostHook:\nTraceLedger\nSHA-256 + jsonl"] +``` + +--- + +### 11.5 — 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 +``` + +--- + +### 11.6 — Data Model Class Diagram + +```mermaid +classDiagram + class ActiveIntent { + +String id + +String name + +String status + +String[] owned_scope + +String[] constraints + +String[] acceptance_criteria + } + + 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 Related { + +String type (specification) + +String value (INT-001) + } + + 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 + FileTrace "1" --> "0..*" Related + HookEngine ..> ActiveIntent : loads from YAML + HookEngine ..> TraceRecord : generates +``` From 96d16f772a286560eabf66f55548a2d402ca3606 Mon Sep 17 00:00:00 2001 From: "https://github.com/Zerubabel-J" Date: Sat, 21 Feb 2026 20:58:56 +0300 Subject: [PATCH 6/6] docs: restructure ARCHITECTURE_NOTES.md with professional format, tables, and 10 Mermaid diagrams --- ARCHITECTURE_NOTES.md | 862 ++++++++++++++++++++++++++---------------- 1 file changed, 544 insertions(+), 318 deletions(-) diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md index 5334cf41d02..1bed46c4d84 100644 --- a/ARCHITECTURE_NOTES.md +++ b/ARCHITECTURE_NOTES.md @@ -1,453 +1,636 @@ -# ARCHITECTURE_NOTES.md +# AI-Native IDE — Architecture Notes -## Phase 0 — The Archaeological Dig into Roo Code (Exploring the codebase) +> _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 --- -## 1. What Is Roo Code? +## 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) -Roo Code is a VSCode extension that runs an AI coding agent inside the editor. It is a **monorepo** built with TypeScript, structured as: +--- -``` -Roo-Code/ -├── src/ ← VSCode Extension Host (the main agent logic) -│ ├── extension.ts ← Entry point: activates the extension -│ ├── core/ -│ │ ├── task/Task.ts ← THE agent brain. Manages the entire conversation loop. -│ │ ├── tools/ ← Every tool the agent can call (read, write, execute...) -│ │ ├── prompts/system.ts ← Builds the system prompt sent to the LLM -│ │ ├── assistant-message/ ← Processes what the LLM returns (tool calls, text) -│ │ └── webview/ ← Bridge to the UI panel -│ └── services/ ← MCP, checkpoints, skills -├── packages/ -│ └── types/src/tool.ts ← Canonical list of all tool names (ToolName type) -└── apps/ ← Web app, CLI -``` +## 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. --- -## 2. How the Agent Loop Works (The Nervous System) +## 1. Current Extension Architecture Overview -The agent is a **request-response loop** between the LLM and the IDE. Here is the complete flow: +### 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"] ``` -User types a message - ↓ -Task.ts → getSystemPrompt() → SYSTEM_PROMPT() in src/core/prompts/system.ts - ↓ -Task.ts → makeApiRequest() → sends [systemPrompt + conversation history] to Claude/OpenAI - ↓ -LLM responds with content blocks: - - "text" block → displayed to user - - "tool_use" block → intercepted for execution - ↓ -presentAssistantMessage() in src/core/assistant-message/presentAssistantMessage.ts - ↓ -switch (block.name) { - case "write_to_file" → WriteToFileTool.execute() - case "execute_command" → ExecuteCommandTool.execute() - case "read_file" → ReadFileTool.execute() - ...each tool handles its own askApproval + result -} - ↓ -Tool result pushed back → next LLM turn -``` + +### 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 | --- -## 3. The Three Critical Files (Hook Insertion Points) +## 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 -### 3.1 Tool Dispatch — `src/core/assistant-message/presentAssistantMessage.ts` + 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 +``` -**Line 678 — The switch(block.name) block** +### 2.2 Write Operations: `write_to_file` Hook Flow -This is the single most important location in the entire codebase. Every tool call from the LLM passes through this switch statement. There is **no other path**. This is where: +```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 -- **Pre-Hooks go**: BEFORE the switch executes (before any tool runs) -- **Post-Hooks go**: AFTER the tool case completes (after the file is written / command is run) +**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 -// LINE 678 in presentAssistantMessage.ts +// 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 "write_to_file": ← mutating: needs Pre-Hook + Post-Hook + 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 (HITL approval) + case "execute_command": // ← destructive: needs Pre-Hook await executeCommandTool.handle(...) break - case "read_file": ← safe: no hook needed + case "read_file": // ← safe: no hook needed ... } ``` -### 3.2 System Prompt — `src/core/task/Task.ts` line 3792 → `src/core/prompts/system.ts` - -The system prompt is built by `getSystemPrompt()` (private method on Task, line 3745), which calls `SYSTEM_PROMPT()` in `system.ts`. This function assembles modular sections from `src/core/prompts/sections/`. +--- -**This is where we inject the intent enforcement instruction:** +## 3. LLM Request/Response Lifecycle -> "You CANNOT write code immediately. Your FIRST action MUST be `select_active_intent`." +``` +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 +``` -### 3.3 Tool Definitions — `packages/types/src/tool.ts` +**Provider Abstraction:** `src/api/index.ts` → `buildApiHandler(provider)` → creates one of: +`AnthropicHandler` | `OpenAiHandler` | `GeminiHandler` | `OllamaHandler` | etc. -The array `toolNames` (line 24) is the canonical registry of all valid tool names. Adding `"select_active_intent"` here makes it a first-class tool recognized by the parser and type system. +All handlers implement a unified `createMessage()` interface — the rest of the agent loop is provider-agnostic. --- -## 4. The Hook Architecture We Are Building +## 4. System Prompt Construction Pipeline -### 4.1 The Two-Stage State Machine +### 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: ``` -User: "Refactor the auth middleware" - │ - ▼ - ┌─────────────────────────┐ - │ LLM analyzes request │ - │ (State 1: The Request) │ - └───────────┬─────────────┘ - │ LLM calls: select_active_intent("INT-001") - ▼ - ┌─────────────────────────────────────────────────┐ - │ PRE-HOOK fires on select_active_intent │ - │ → Reads .orchestration/active_intents.yaml │ - │ → Finds INT-001: constraints + owned_scope │ - │ → Returns XML block to LLM │ - │ (State 2: The Handshake) │ - └───────────┬─────────────────────────────────────┘ - │ LLM now has context, calls: write_to_file("src/auth/middleware.ts", ...) - ▼ - ┌─────────────────────────────────────────────────┐ - │ PRE-HOOK fires on write_to_file │ - │ → Checks: active intent declared? ✓ │ - │ → Checks: src/auth/middleware.ts in scope? ✓ │ - │ → Allows execution to proceed │ - └───────────┬─────────────────────────────────────┘ - │ WriteToFileTool.execute() runs — file is saved - ▼ - ┌─────────────────────────────────────────────────┐ - │ POST-HOOK fires after write_to_file │ - │ → Computes SHA-256 of written content │ - │ → Appends JSON record to agent_trace.jsonl │ - │ → Links: INT-001 → src/auth/middleware.ts │ - │ (State 3: Contextualized Action + Trace) │ - └─────────────────────────────────────────────────┘ +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 What Gets Blocked +### 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`: ``` -Agent tries write_to_file WITHOUT calling select_active_intent first: - → PRE-HOOK: IntentGate fires → BLOCKED - → Returns: "Error: You must call select_active_intent before writing files." +# Intent-Driven Governance Protocol -Agent tries to write src/billing/invoice.ts but INT-001 only owns src/auth/**: - → PRE-HOOK: ScopeGuard fires → BLOCKED - → Returns: "Scope Violation: INT-001 is not authorized to edit src/billing/invoice.ts" +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. The src/hooks/ Directory Structure +## 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 ← Shared types: HookContext, HookResult, IntentState -├── HookEngine.ts ← The singleton middleware engine -│ Manages per-task intent state -│ Runs pre/post hook chains +├── types.ts ← HookContext, HookResult, IntentState, TraceRecord +├── HookEngine.ts ← Singleton middleware. runPreHook() / runPostHook() ├── preHooks/ -│ ├── intentGate.ts ← Blocks mutating tools if no intent is declared -│ └── scopeGuard.ts ← Blocks writes outside the intent's owned_scope +│ ├── 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 (crypto built-in) - ├── intentLoader.ts ← Parses .orchestration/active_intents.yaml + ├── 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 + --- -## 6. The Data Model (.orchestration/) +## 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 ``` -.orchestration/ -├── active_intents.yaml ← What work is authorized (the "why") -├── agent_trace.jsonl ← Append-only ledger of every action (the "proof") -└── intent_map.md ← Which files belong to which intent (the "map") -``` -### active_intents.yaml schema: +### 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" + 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" ``` -### agent_trace.jsonl record schema (spatial independence via content hash): +**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": "ISO-8601", + "timestamp": "2026-02-16T12:00:00Z", "intent_id": "INT-001", - "vcs": { "revision_id": "git_sha" }, + "vcs": { "revision_id": "git_sha_hash" }, "files": [ { "relative_path": "src/auth/middleware.ts", - "contributor": { "entity_type": "AI", "model_identifier": "claude-3-5-sonnet" }, + "contributor": { + "entity_type": "AI", + "model_identifier": "claude-sonnet-4-6" + }, "ranges": [ { - "start_line": 1, + "start_line": 15, "end_line": 45, - "content_hash": "sha256:a8f5f167..." + "content_hash": "sha256:a8f5f167f44f4964e6c998dee827110c" } ], "mutation_class": "AST_REFACTOR", - "related": [{ "type": "specification", "value": "INT-001" }] + "related": [ + { "type": "specification", "value": "REQ-001" }, + { "type": "intent", "value": "INT-001" } + ] } ] } ``` ---- - -## 7. The select_active_intent Tool +**Critical Design Properties:** -A new first-class tool added to the agent's toolset. The LLM MUST call this before any mutating action. +- **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. -**Input:** `{ intent_id: string }` +**When `agent_trace.jsonl` Is Written:** -**What happens when called:** +| 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 | -1. HookEngine reads `active_intents.yaml` and finds the intent -2. Extracts constraints, owned_scope, acceptance_criteria -3. Returns an `` XML block back to the LLM -4. Marks the intent as active in per-task state (Map) +### 7.3 `intent_map.md` — The Spatial Map -**What the LLM receives:** +**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. -```xml - - - - src/auth/** - src/middleware/jwt.ts - - - Must not use external auth providers - - - Unit tests in tests/auth/ pass - - - -``` +**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. System Prompt Modification +## 8. Three-State Execution Flow -The following instruction is injected into the system prompt (in `src/core/prompts/system.ts`): +The agent is **not allowed to write code immediately**. Every turn follows a mandatory Three-State Execution Flow: -``` -# Intent-Driven Governance Protocol +### 8.1 Hook Engine State Machine -You are operating under a strict governance system. You CANNOT write, edit, or delete -files immediately. Your FIRST action for any code modification task MUST be: - -1. Analyze the user's request -2. Call `select_active_intent(intent_id)` with the appropriate intent ID from - .orchestration/active_intents.yaml -3. Wait for the block to be returned -4. Only THEN proceed with code modifications — and only within the declared scope +```mermaid +stateDiagram-v2 + [*] --> NoIntent: Task starts -If you attempt to call write_to_file, apply_diff, edit, or execute_command -without first calling select_active_intent, the system will BLOCK your action -and return an error. -``` + 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 -## 9. Key Architectural Decisions + IntentDeclared --> ScopeCheck: mutating tool called + ScopeCheck --> ToolExecutes: file path ∈ owned_scope ✓ + ScopeCheck --> BLOCKED_Scope: file path ∉ owned_scope ✗ -| Decision | Choice | Reason | -| --------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | -| Hook insertion point | `presentAssistantMessage.ts` before switch(block.name) | Single choke point — ALL tools pass through here | -| Intent state storage | `Map` in HookEngine singleton | No Task.ts modification needed; isolated | -| Content hashing | Node.js `crypto.createHash('sha256')` | Zero dependency, always available in Extension Host | -| YAML parsing | `yaml` package (already in src/package.json) | Already a project dependency | -| Scope matching | Simple prefix/glob matching | Sufficient for the demo; expandable to minimatch | -| Trace format | Append-only JSONL | Machine-readable, spatially independent, append-safe | -| select_active_intent registration | Added to `toolNames` in `packages/types/src/tool.ts` | Cleanest: makes it first-class, recognized by parser | + BLOCKED_Scope --> IntentDeclared: agent receives\n"Scope Violation" error ---- + ToolExecutes --> TraceWritten: PostHook fires\nSHA-256 + jsonl append + TraceWritten --> IntentDeclared: ready for next action -## 10. Files Modified / Created + IntentDeclared --> NoIntent: task completes\nclearIntent(taskId) +``` -### Modified: +### 8.2 State Transition Mechanics -- `packages/types/src/tool.ts` — Added `"select_active_intent"` to toolNames -- `src/core/assistant-message/presentAssistantMessage.ts` — Wired pre/post hooks + select_active_intent case -- `src/core/prompts/system.ts` — Injected intent enforcement instruction +**State 1 → State 2 (Request → Reasoning Intercept):** -### Created: +The governance layer forces the agent into the Reasoning Intercept by controlling the system prompt: -- `src/hooks/` — Entire hooks directory (new) -- `src/core/tools/SelectActiveIntentTool.ts` — The new tool -- `.orchestration/active_intents.yaml` — Sample intent definitions -- `.orchestration/agent_trace.jsonl` — Append-only trace ledger (machine-managed) -- `.orchestration/intent_map.md` — Intent-to-file spatial map +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):** -## 11. Visual System Blueprints +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 -### 11.1 — System Layer Architecture +### 8.3 PostToolUse Mechanics ```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"] +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"] ``` --- -### 11.2 — Agent Sequence Diagram (The Two-Stage State Machine) +## 9. Concurrency & Safety Injection Points -```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 +### 9.1 Optimistic Locking (Phase 4 — Parallel Orchestration) - User->>Task: "Refactor auth middleware" - Task->>LLM: systemPrompt + message - Note over LLM: 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) - Note over HE: select_active_intent is exempt
from IntentGate blocking - HE-->>PAM: allowed - 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 - JSONL-->>TL: done - PAM-->>LLM: tool result: success -``` - ---- - -### 11.3 — Hook Engine State Machine +Optimistic locking prevents concurrent agents from silently overwriting each other's changes: ```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" +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"] +``` - NoIntent --> IntentDeclared: select_active_intent(INT-001)\n✓ found in active_intents.yaml +| 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 | - IntentDeclared --> ScopeCheck: mutating tool called - ScopeCheck --> ToolExecutes: file path ∈ owned_scope ✓ - ScopeCheck --> BLOCKED_Scope: file path ∉ owned_scope ✗ +### 9.2 Scope Validation Points - BLOCKED_Scope --> IntentDeclared: agent receives\n"Scope Violation" error +| 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 | - ToolExecutes --> TraceWritten: PostHook fires\nSHA-256 + jsonl append - TraceWritten --> IntentDeclared: ready for next action +### 9.3 Existing Safety Mechanisms (Preserved) - IntentDeclared --> NoIntent: task completes\nclearIntent(taskId) -``` +| 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 | --- -### 11.4 — Pre-Hook Interceptor Chain - -```mermaid -flowchart LR - TC["Tool Call\narrives"] --> 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\nReturn error:\nCall select_active_intent"] - HASINT -->|"Yes"| SG["ScopeGuard\nPre-Hook"] - SG --> INSCOPE{Target file\nin owned_scope?} - INSCOPE -->|"No"| BLOCK2["🚫 BLOCKED\nReturn error:\nScope Violation"] - INSCOPE -->|"Yes"| EXEC["✅ Execute Tool\nWriteToFileTool / etc."] - EXEC --> POST["PostHook:\nTraceLedger\nSHA-256 + jsonl"] -``` - ---- +## 10. Visual System Blueprints -### 11.5 — Traceability Chain (Intent → Code → Hash → Git) +### 10.1 Traceability Chain (Intent → Code → Hash → Git) ```mermaid graph LR @@ -468,9 +651,7 @@ graph LR INT -->|"referenced in"| JSONL ``` ---- - -### 11.6 — Data Model Class Diagram +### 10.2 Data Model Class Diagram ```mermaid classDiagram @@ -481,6 +662,7 @@ classDiagram +String[] owned_scope +String[] constraints +String[] acceptance_criteria + +String assigned_agent } class TraceRecord { @@ -514,11 +696,6 @@ classDiagram +String content_hash (sha256:hex) } - class Related { - +String type (specification) - +String value (INT-001) - } - class HookEngine { -Map intentStateMap +getInstance() HookEngine @@ -533,7 +710,56 @@ classDiagram TraceRecord "1" --> "1..*" FileTrace FileTrace "1" --> "1" Contributor FileTrace "1" --> "1..*" Range - FileTrace "1" --> "0..*" Related 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._