This commit is contained in:
Yakob Dereje 2026-04-09 02:31:29 +00:00 committed by GitHub
commit 10b5afb71a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1813 additions and 13 deletions

0
.github/copilot-instructions.md vendored Normal file
View file

View file

@ -0,0 +1,31 @@
active_intents:
- id: "INT-001"
name: "Weather API Implementation"
status: "IN_PROGRESS"
owned_scope:
- "src/weather/**"
- "*.js"
- "*.ts"
constraints:
- "Use async/await for all async operations"
- "Include error handling with try/catch"
- "Return JSON format responses"
- "Add input validation"
acceptance_criteria:
- "Unit tests pass with >80% coverage"
- "API returns correct weather data"
- "Error cases handled gracefully"
- id: "INT-002"
name: "Authentication Middleware"
status: "PLANNED"
owned_scope:
- "src/auth/**"
- "src/middleware/auth.ts"
constraints:
- "Use JWT tokens"
- "Implement rate limiting"
- "Log all auth attempts"
acceptance_criteria:
- "Auth tests pass"
- "Security audit passed"

View file

@ -0,0 +1,2 @@
{"timestamp":"2026-02-21T20:41:12.120Z","intent_id":"INT-001","file_path":"src/core/tools/WriteToFileTool.ts","content_sha256":"5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","semantic_change":"EVOLUTION","tool":"write_to_file"}
{"timestamp":"2026-02-21T20:52:45.918Z","intent_id":"INT-001","file_path":"src/core/assistant-message/presentAssistantMessage.ts","content_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","semantic_change":"REFACTOR","tool":"apply_patch"}

View file

@ -0,0 +1,29 @@
# Intent Map
## INT-001: Weather API Implementation
- **Status:** IN_PROGRESS
- **Files:**
- `src/weather/api.js` - Main API endpoints
- `src/weather/service.js` - Weather service logic
- `src/weather/utils.js` - Helper functions
- **AST Nodes:**
- `WeatherService` class
- `getWeatherData()` function
- `formatWeatherResponse()` function
- **Dependencies:** OpenWeatherMap API
- **Last Updated:** 2026-02-21
## INT-002: Authentication Middleware
- **Status:** PLANNED
- **Files:**
- `src/auth/jwt.js` - JWT handling
- `src/middleware/auth.js` - Auth middleware
- `src/auth/rate-limit.js` - Rate limiting
- **AST Nodes:**
- `authenticateToken()` middleware
- `generateToken()` function
- `RateLimiter` class
- **Dependencies:** jsonwebtoken, express-rate-limit
- **Last Updated:** 2026-02-21

1
.vscode/launch.json vendored
View file

@ -9,6 +9,7 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"stopOnEntry": false,
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/src"],
"sourceMaps": true,

13
.vscode/mcp.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"servers": {
"tenxfeedbackanalytics": {
"url": "https://mcppulse.10academy.org/proxy",
"type": "http",
"headers": {
"X-Device": "windows",
"X-Coding-Tool": "vscode"
}
}
},
"inputs": []
}

90
ARCHITECTURE_NOTES.md Normal file
View file

@ -0,0 +1,90 @@
# Architecture Notes: Master Thinker Edition
## 1. Data Flow Map (Request-to-Execution)
1. User Input: Received via the React Webview.
2. Context Assembly: `ClineProvider.ts` calls `src/core/task/Task.ts` to generate the System Prompt.
3. Decision: The LLM selects a tool. `presentAssistantMessage.ts` orchestrates the tool call.
4. Execution: `WriteToFileTool.ts` executes the `handle()` method to modify the file system.
## 2. Governance Interception Points
- **Pre-Hook (Phase 1/2):** Located in `WriteToFileTool.ts` inside `handle()`. This will block execution if `.orchestration/active_intents.yaml` does not have an `in-progress` status.
- **Post-Hook (Phase 3):** Located at the end of `handle()` after a successful write. This will trigger the `agent_trace.jsonl` logger to hash the new content.
## 3. Intent-Code Gap Analysis
Standard Git tracks "What" changed but lacks the "Why." By using a sidecar orchestration layer, we map every Abstract Syntax Tree (AST) change to a specific Requirement ID. This prevents "Context Rot" where agents lose track of architectural constraints during long-running tasks.
# Architectural Design Report
## 1. The Intent-First Protocol (Two-Stage State Machine)
The core of this implementation is a move away from "Vibe Coding" towards a governed, stateful interaction. I have architected a Two-Stage State Machine for every user request:
**Stage 1: The Reasoning Intercept (The Handshake):** The agent is no longer permitted to generate code immediately. It must first analyze the request, identify a valid intent_id from the governance sidecar, and call the select_active_intent tool.
+1
**Stage 2: Contextualized Action:** Only after the "Handshake" is successful and the context is injected can the agent proceed to use destructive tools like write_to_file or execute_command.
+1
## 2. The Deterministic Hook (Gatekeeper Architecture)
To ensure compliance, I implemented a Deterministic Hook System that acts as a strict middleware boundary:
+1
Pre-Hook Implementation: In WriteToFileTool.ts and ExecuteCommandTool.ts, I injected a gatekeeper check at the start of the handle method.
Verification Logic: This hook verifies the presence of a global active intent flag. If the agent attempts a file modification without a validated "checkout," the hook blocks execution and returns a formal governance error: "You must cite a valid active Intent ID".
Fail-Safe: This ensures that the architecture enforces the rules, rather than relying on the LLM's "best effort" to follow instructions.
## 3. Context Engineering (Dynamic Injection vs. Context Rot)
Traditional AI IDEs suffer from "Context Rot" by dumping entire file trees into the prompt. This implementation solves this via Dynamic Context Injection:
+1
**Sidecar Pattern:** All architectural constraints and business intents are stored in .orchestration/active_intents.yaml.
+1
**On-Demand Context:** When select_active_intent is called, the system reads the YAML and constructs a targeted <intent_context> XML block.
**Traceability:** This ensures the agent only operates within its "owned_scope" and respects the "acceptance_criteria" defined in the sidecar, maintaining a high signal-to-noise ratio in the context window.
## 5. Diagrams and Schemas (Required for Interim Submission)
### A. The Two-Stage Handshake (Sequence Diagram)
This diagram illustrates how the Hook Engine intercepts the LLM's request to ensure intent-validation before execution.
```mermaid
sequenceDiagram
participant User
participant LLM as Roo Code Agent
participant Hook as Hook Engine (Middleware)
participant Sidecar as .orchestration/active_intents.yaml
User->>LLM: "Refactor Auth Logic"
Note over LLM: Agent is blocked from writing code
LLM->>Hook: call select_active_intent(INT-001)
Hook->>Sidecar: Validate Intent ID & Status
Sidecar-->>Hook: Return Scope & Constraints
Hook-->>LLM: Inject <intent_context> into prompt
Note over LLM: Agent now has authorization
LLM->>User: "I have loaded intent INT-001. Proceeding..."
```
### B. Intent Context Schema
```xml
<intent_context>
<intent_id>INT-001</intent_id>
<status>IN PROGRESS</status>
<owned_scope>
<file>src/auth/\*\*</file>
<file>src/middleware/jwt.ts</file>
</owned_scope>
<constraints> - Must not use external auth providers - Maintain backward compatibility
</constraints>
</intent_context>
```

40
SUBMISSION_README.md Normal file
View file

@ -0,0 +1,40 @@
# Roo Code Master Thinker - Week 1 Final Submission
## Project Overview
A governed AI-native IDE with intent-code traceability, human-in-the-loop security, and concurrent agent safety. This implementation transforms the Roo Code extension from a simple chatbot into a governed orchestration system where every action requires verified intent.
## Implemented Phases
### ✅ Phase 0: Architecture Mapping
- Mapped tool execution flow in `presentAssistantMessage.ts`
- Located prompt builder in `system.ts`
- Identified webview communication in `ClineProvider.ts`
### ✅ Phase 1: The Handshake
- `select_active_intent` tool for intent selection
- `IntentContextLoader` reads YAML and returns XML context
- System prompt enforces intent-first protocol
- Gatekeeper blocks writes without valid intent
### ✅ Phase 2: Hook Middleware
- Command classification (SAFE vs DESTRUCTIVE)
- HITL authorization dialogs for destructive commands
- Scope enforcement against intent's `owned_scope`
- Autonomous recovery with structured error responses
### ✅ Phase 3: Traceability
- SHA-256 content hashing for spatial independence
- Semantic classification (REFACTOR vs EVOLUTION)
- JSONL trace recording to `agent_trace.jsonl`
- Links intent IDs to content hashes
### ✅ Phase 4: Concurrency
- Optimistic locking with pre-write hash capture
- Stale file detection and blocking
- Clear conflict resolution with re-read guidance

321
docs/submission-report.md Normal file
View file

@ -0,0 +1,321 @@
# Master Thinker Orchestration Architecture Report
## Submission Draft (Markdown → PDF)
**Project:** Roo Code Master Thinker
**Date:** February 21, 2026
**Prepared by:** Yakob
---
## 1) Architecture Overview
This implementation evolves the extension into an intent-governed orchestration system across four phases:
1. **Phase 1 — Handshake:** force intent selection before destructive actions.
2. **Phase 2 — Hook Middleware:** add classification + human authorization + scope checks in the execution path.
3. **Phase 3 — Traceability:** persist intent-linked file-change traces with deterministic content hashing.
4. **Phase 4 — Concurrency:** prevent stale writes using optimistic locking and conflict feedback.
The guiding principle is: **no destructive write without validated intent context**, and **no untraceable mutation**.
---
### Diagram A — Two-Stage Handshake
```mermaid
sequenceDiagram
participant U as User
participant L as LLM Agent
participant T as select_active_intent Tool
participant Y as active_intents.yaml
participant W as Write Tool
U->>L: "Implement change X"
L->>T: select_active_intent(intent_id)
T->>Y: load intent context
Y-->>T: constraints + scope (+ status)
T-->>L: intent_context XML
L->>W: write_to_file/apply_patch
Note over W: Allowed only after valid active intent context
```
---
### Diagram B — Hook Middleware Flow
```mermaid
flowchart TD
A[Tool call emitted] --> B{Command Classification}
B -->|SAFE| C[Continue]
B -->|DESTRUCTIVE| D[HITL Authorization]
D -->|Denied| X[Reject tool_result + stop]
D -->|Approved| E[Intent Context Validation]
E -->|Invalid/Missing| X
E -->|Valid| F[Scope Enforcement]
F -->|Out of scope| X
F -->|In scope| G[Execute tool]
G --> H[Return tool_result]
```
---
### Diagram C — Traceability Pipeline
```mermaid
flowchart LR
A[Successful file write] --> B[Read final content]
B --> C[SHA-256 hash]
C --> D[Semantic classifier: REFACTOR/EVOLUTION]
D --> E[Build trace entry]
E --> F[Append JSONL line to .orchestration/agent_trace.jsonl]
```
---
### Diagram D — Concurrency Control
```mermaid
flowchart TD
A[Before write: capture initial hash] --> B[User approval complete]
B --> C[Recompute current file hash]
C --> D{Hash unchanged?}
D -->|Yes| E[Proceed write]
D -->|No| F[Raise StaleFileError]
F --> G[Return error tool_result]
G --> H[Force re-read before retry]
```
---
## 2) Phase 0: Architecture Notes (Archaeological Dig)
### Key Findings
- **Tool execution choke-point:** assistant tool execution is centralized in [src/core/assistant-message/presentAssistantMessage.ts](../src/core/assistant-message/presentAssistantMessage.ts).
- **Prompt assembly and request orchestration:** handled in [src/core/task/Task.ts](../src/core/task/Task.ts), including pre-hook context injection flow.
- **Intent sidecar source-of-truth:** [.orchestration/active_intents.yaml](../.orchestration/active_intents.yaml).
- **Webview runtime orchestration:** [src/core/webview/ClineProvider.ts](../src/core/webview/ClineProvider.ts).
- **Extension activation/lifecycle root:** [src/extension.ts](../src/extension.ts).
### Tool Execution Locations
- Intent selection tool implementation: [src/core/tools/SelectActiveIntentTool.ts](../src/core/tools/SelectActiveIntentTool.ts)
- Write path and post-write operations: [src/core/tools/WriteToFileTool.ts](../src/core/tools/WriteToFileTool.ts)
- Patch path: [src/core/tools/ApplyPatchTool.ts](../src/core/tools/ApplyPatchTool.ts)
### Prompt Builder Location
- System prompt composition path: [src/core/prompts/system.ts](../src/core/prompts/system.ts)
- Runtime message and request chain: [src/core/task/Task.ts](../src/core/task/Task.ts)
### Webview Communication
- Provider/webview messaging bridge: [src/core/webview/ClineProvider.ts](../src/core/webview/ClineProvider.ts)
- Activation + provider registration: [src/extension.ts](../src/extension.ts)
---
## 3) Phase 1: The Handshake
### Components Implemented
- **Intent Tool:** [src/core/tools/SelectActiveIntentTool.ts](../src/core/tools/SelectActiveIntentTool.ts)
- Resolves intent by ID from sidecar YAML.
- Stores active intent in provider state.
- Returns context payload for subsequent orchestration.
- **Intent Loader:** [src/core/intent/IntentContextLoader.ts](../src/core/intent/IntentContextLoader.ts)
- Parses [.orchestration/active_intents.yaml](../.orchestration/active_intents.yaml).
- Normalizes and resolves intent objects.
- Produces XML fragments for controlled injection.
- **System Prompt Integration:** [src/core/prompts/system.ts](../src/core/prompts/system.ts)
- Includes operational behavior guiding intent-first workflow.
- **Pre-hook Injection in Task Flow:** [src/core/task/Task.ts](../src/core/task/Task.ts)
- Injects intent context into request pipeline before model action execution.
### Gatekeeper Implementation
- **Gate enforcement location:** [src/core/assistant-message/presentAssistantMessage.ts](../src/core/assistant-message/presentAssistantMessage.ts)
- Blocking behavior returns governance error when active intent context is absent/invalid, preventing destructive tool execution.
---
## 4) Phase 2: Hook Middleware
### Command Classification
- Implemented via [src/core/governance/CommandClassifier.ts](../src/core/governance/CommandClassifier.ts)
- Categorizes actions into **SAFE** vs **DESTRUCTIVE** classes for policy routing.
### HITL Authorization
- Implemented via [src/core/governance/HITLAuthorizer.ts](../src/core/governance/HITLAuthorizer.ts)
- Prompts user confirmation on destructive actions and enforces deny-path rejection.
### Scope Enforcement
- Implemented via [src/core/governance/ScopeEnforcer.ts](../src/core/governance/ScopeEnforcer.ts)
- Extracts target paths and validates requested operations against intent-owned scope.
### Autonomous Recovery
- Denied/out-of-scope/invalid-intent cases return structured tool errors so the agent can recover by:
1. selecting valid intent,
2. re-reading context,
3. producing scoped retries.
---
## 5) Phase 3: Traceability
### SHA-256 Content Hashing
- Implemented in [src/core/trace/ContentHasher.ts](../src/core/trace/ContentHasher.ts)
- Deterministic hash of final file content provides immutable change fingerprint.
### Semantic Classification
- Implemented in [src/core/trace/SemanticClassifier.ts](../src/core/trace/SemanticClassifier.ts)
- Lightweight classifier emits:
- `REFACTOR`
- `EVOLUTION`
### Trace Serialization
- Implemented in [src/core/trace/AgentTraceSerializer.ts](../src/core/trace/AgentTraceSerializer.ts)
- Appends JSON Lines entries to [.orchestration/agent_trace.jsonl](../.orchestration/agent_trace.jsonl).
### JSONL Schema (Operational)
```json
{
"timestamp": "2026-02-21T21:00:00.000Z",
"intent_id": "INTENT-001",
"file_path": "src/core/tools/WriteToFileTool.ts",
"content_sha256": "hex_sha256_digest",
"semantic_change": "REFACTOR | EVOLUTION",
"tool": "write_to_file"
}
```
---
## 6) Phase 4: Concurrency
### Optimistic Locking Primitive
- Implemented in [src/core/concurrency/OptimisticLock.ts](../src/core/concurrency/OptimisticLock.ts):
- `getCurrentHash(filePath)`
- `validateLock(expectedHash, filePath)`
- `StaleFileError`
### Write-path Integration
- Integrated (minimally/safely) in [src/core/tools/WriteToFileTool.ts](../src/core/tools/WriteToFileTool.ts):
1. Capture initial hash after file existence resolution.
2. Revalidate hash immediately before final save.
3. On mismatch, block write and emit stale-file error.
4. Clear hash state on completion/failure paths.
### Stale File Detection
If file content changes between read and write windows, write is rejected and the agent is instructed to re-read current content before retry.
### Conflict Resolution
- Returns explicit stale conflict error (`StaleFileError` semantics).
- Prevents silent overwrite.
- Forces reconciliation loop (read → regenerate patch/write).
---
## 7) Evaluation Rubric Self-Assessment
| Metric | How I Achieve Score 5 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Intent-AST Correlation | [.orchestration/agent_trace.jsonl](../.orchestration/agent_trace.jsonl) links `intent_id` to content hash and file path after successful writes. |
| Context Engineering | Dynamic intent context resolution from [.orchestration/active_intents.yaml](../.orchestration/active_intents.yaml), injected before execution. |
| Hook Architecture | Middleware checks in [src/core/assistant-message/presentAssistantMessage.ts](../src/core/assistant-message/presentAssistantMessage.ts) gate destructive paths with policy layering. |
| Orchestration | Intent-first flow + traceability + optimistic locking provide safe multi-agent behavior and conflict-aware writes. |
---
## 8) Screenshots (Submission Section)
Add these 4 screenshots with captions:
1. **Gatekeeper block without intent**
- Show Roo response containing governance error: “You must cite a valid active Intent ID”.
2. **HITL approval dialog**
- Show destructive action confirmation prompt before execution.
3. **Trace output with hash**
- Show [.orchestration/agent_trace.jsonl](../.orchestration/agent_trace.jsonl) containing `intent_id`, `file_path`, and `content_sha256`.
4. **Stale-file conflict error**
- Show rejected write with stale/optimistic-lock error forcing re-read.
---
## Appendix A — Primary Files by Phase
- **Phase 1:**
[src/core/tools/SelectActiveIntentTool.ts](../src/core/tools/SelectActiveIntentTool.ts)
[src/core/intent/IntentContextLoader.ts](../src/core/intent/IntentContextLoader.ts)
[src/core/prompts/system.ts](../src/core/prompts/system.ts)
[src/core/task/Task.ts](../src/core/task/Task.ts)
- **Phase 2:**
[src/core/governance/CommandClassifier.ts](../src/core/governance/CommandClassifier.ts)
[src/core/governance/HITLAuthorizer.ts](../src/core/governance/HITLAuthorizer.ts)
[src/core/governance/ScopeEnforcer.ts](../src/core/governance/ScopeEnforcer.ts)
[src/core/assistant-message/presentAssistantMessage.ts](../src/core/assistant-message/presentAssistantMessage.ts)
- **Phase 3:**
[src/core/trace/ContentHasher.ts](../src/core/trace/ContentHasher.ts)
[src/core/trace/SemanticClassifier.ts](../src/core/trace/SemanticClassifier.ts)
[src/core/trace/AgentTraceSerializer.ts](../src/core/trace/AgentTraceSerializer.ts)
[.orchestration/agent_trace.jsonl](../.orchestration/agent_trace.jsonl)
- **Phase 4:**
[src/core/concurrency/OptimisticLock.ts](../src/core/concurrency/OptimisticLock.ts)
[src/core/tools/WriteToFileTool.ts](../src/core/tools/WriteToFileTool.ts)
---
## Appendix B — PDF Conversion Tips
- Use VS Code Markdown Preview + “Print to PDF”, or Pandoc:
- `pandoc report.md -o report.pdf`
- Ensure Mermaid diagrams are rendered before export (or pre-render to images if needed).
```mermaid
graph TD
A[User Request] --> B{Has Intent?}
B -->|No| C[Call select_active_intent]
C --> D[Load Intent Context]
D --> B
B -->|Yes| E{Command Type?}
E -->|Safe| F[Execute Immediately]
E -->|Destructive| G{In Scope?}
G -->|No| H[Scope Violation Error]
G -->|Yes| I{HITL Approved?}
I -->|No| J[User Rejected Error]
I -->|Yes| K[Execute Tool]
K --> L{File Changed?}
L -->|Yes| M[Stale File Error]
L -->|No| N[Write Success]
N --> O[Record Trace]
```

0
final_lock_test.txt Normal file
View file

0
final_tesst.txt Normal file
View file

0
final_test1.txt Normal file
View file

1
gate_test.txt Normal file
View file

@ -0,0 +1 @@
Locked

1
governance_test.txt Normal file
View file

@ -0,0 +1 @@
Testing the lock

8
intent_map.md Normal file
View file

@ -0,0 +1,8 @@
# Intent-Code Mapping
| Intent ID | Target Files / Modules | Status |
| --------- | ------------------------------------------------- | ----------- |
| INT-001 | src/core/tools/WriteToFileTool.ts | IN PROGRESS |
| INT-001 | src/core/assistant-msg/presentAssistantMessage.ts | COMPLETED |
[cite_start]**Notes:** This map links the high-level business logic in `.orchestration/active_intents.yaml` to specific AST changes in the source code.

View file

@ -39,6 +39,7 @@ export const toolNames = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"select_active_intent",
"new_task",
"codebase_search",
"update_todo_list",

0
please test.txt Normal file
View file

View file

@ -547,6 +547,14 @@ export class NativeToolCallParser {
}
break
case "select_active_intent":
if (partialArgs.intent_id !== undefined) {
nativeArgs = {
intent_id: partialArgs.intent_id,
}
}
break
case "update_todo_list":
if (partialArgs.todos !== undefined) {
nativeArgs = {
@ -883,6 +891,14 @@ export class NativeToolCallParser {
}
break
case "select_active_intent":
if (args.intent_id !== undefined) {
nativeArgs = {
intent_id: args.intent_id,
} as NativeArgsFor<TName>
}
break
case "update_todo_list":
if (args.todos !== undefined) {
nativeArgs = {

View file

@ -28,6 +28,7 @@ import { useMcpToolTool } from "../tools/UseMcpToolTool"
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
import { switchModeTool } from "../tools/SwitchModeTool"
import { SelectActiveIntentTool } from "../tools/SelectActiveIntentTool"
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
import { newTaskTool } from "../tools/NewTaskTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
@ -40,6 +41,21 @@ import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { formatResponse } from "../prompts/responses"
import { sanitizeToolUseId } from "../../utils/tool-id"
import { INVALID_ACTIVE_INTENT_ERROR, loadIntentContext } from "../intent/IntentContextLoader"
import { isDestructiveCommand } from "../governance/CommandClassifier"
import { showHITLApproval } from "../governance/HITLAuthorizer"
import { enforceIntentScope, getToolTargetPaths } from "../governance/ScopeEnforcer"
const selectActiveIntentTool = new SelectActiveIntentTool()
const GOVERNED_WRITE_TOOLS = new Set<ToolName>([
"write_to_file",
"apply_patch",
"edit_file",
"search_replace",
"apply_diff",
"edit",
"search_and_replace",
])
/**
* Processes and presents assistant message content to the user interface.
@ -59,6 +75,9 @@ import { sanitizeToolUseId } from "../../utils/tool-id"
*/
export async function presentAssistantMessage(cline: Task) {
console.log("🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵")
console.log("🔵 presentAssistantMessage.ts LOADED")
console.log("🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵")
if (cline.abort) {
throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
}
@ -365,6 +384,8 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name}]`
case "switch_mode":
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
case "select_active_intent":
return `[${block.name} for '${block.params.intent_id}']`
case "codebase_search":
return `[${block.name} for '${block.params.query}']`
case "read_command_output":
@ -675,6 +696,78 @@ export async function presentAssistantMessage(cline: Task) {
}
}
if (!block.partial && GOVERNED_WRITE_TOOLS.has(block.name as ToolName)) {
const provider = cline.providerRef.deref()
const state = await provider?.getState()
const activeIntentId = String((state as any)?.activeIntentId ?? "").trim()
let activeIntentContext: Awaited<ReturnType<typeof loadIntentContext>> = null
if (activeIntentId) {
try {
activeIntentContext = await loadIntentContext(cline.cwd, activeIntentId)
} catch {
activeIntentContext = null
}
}
if (!activeIntentContext) {
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, INVALID_ACTIVE_INTENT_ERROR)
cline.didToolFailInCurrentTurn = true
cline.pushToolResultToUserContent({
type: "tool_result",
tool_use_id: sanitizeToolUseId(toolCallId),
content: formatResponse.toolError(INVALID_ACTIVE_INTENT_ERROR),
is_error: true,
})
break
}
const targetPaths = getToolTargetPaths(block.name, (block.nativeArgs ?? {}) as Record<string, unknown>)
const scopeCheck = enforceIntentScope(
activeIntentContext.intent_id,
cline.cwd,
activeIntentContext.scope,
targetPaths,
)
if (!scopeCheck.allowed) {
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, scopeCheck.error)
cline.didToolFailInCurrentTurn = true
cline.pushToolResultToUserContent({
type: "tool_result",
tool_use_id: sanitizeToolUseId(toolCallId),
content: formatResponse.toolError(scopeCheck.error),
is_error: true,
})
break
}
}
if (!block.partial && isValidToolName(String(block.name), stateExperiments)) {
if (isDestructiveCommand(block.name as ToolName)) {
const approved = await showHITLApproval(block.name, block.nativeArgs ?? block.params)
if (!approved) {
const rejectionPayload = JSON.stringify({
code: "operation_rejected",
tool: block.name,
message: "Operation rejected by user",
})
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, rejectionPayload)
cline.didToolFailInCurrentTurn = true
cline.pushToolResultToUserContent({
type: "tool_result",
tool_use_id: sanitizeToolUseId(toolCallId),
content: formatResponse.toolError(rejectionPayload),
is_error: true,
})
break
}
}
}
switch (block.name) {
case "write_to_file":
await checkpointSaveAndMark(cline)
@ -803,6 +896,20 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "select_active_intent": {
try {
const intentId = block.nativeArgs?.intent_id ?? block.params.intent_id ?? ""
const result = await selectActiveIntentTool.handle(
{ intent_id: intentId },
cline.cwd,
cline.providerRef.deref(),
)
pushToolResult(result)
} catch (error) {
await handleError("loading active intent context", error as Error)
}
break
}
case "new_task":
await checkpointSaveAndMark(cline)
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {

View file

@ -0,0 +1,37 @@
import fs from "fs/promises"
import { hashContent } from "../trace/ContentHasher"
export class StaleFileError extends Error {
readonly expectedHash: string
readonly currentHash: string
readonly filePath: string
constructor(filePath: string, expectedHash: string, currentHash: string) {
super(
`STALE_FILE_ERROR: File '${filePath}' changed since it was last read (expected=${expectedHash}, current=${currentHash}). Re-read the file before applying changes.`,
)
this.name = "StaleFileError"
this.filePath = filePath
this.expectedHash = expectedHash
this.currentHash = currentHash
}
}
export async function getCurrentHash(filePath: string): Promise<string> {
const content = await fs.readFile(filePath, "utf8")
return hashContent(content)
}
export async function validateLock(expectedHash: string, filePath: string): Promise<boolean> {
if (!expectedHash || !expectedHash.trim()) {
return false
}
try {
const currentHash = await getCurrentHash(filePath)
return currentHash === expectedHash.trim()
} catch {
return false
}
}

View file

@ -0,0 +1,49 @@
import type { ToolName } from "@roo-code/types"
export const SAFE_COMMANDS: ReadonlySet<ToolName> = new Set([
"read_file",
"list_files",
"search_files",
"codebase_search",
"read_command_output",
"ask_followup_question",
"attempt_completion",
"switch_mode",
"select_active_intent",
"update_todo_list",
"run_slash_command",
"skill",
"access_mcp_resource",
])
export const DESTRUCTIVE_COMMANDS: ReadonlySet<ToolName> = new Set([
"write_to_file",
"apply_patch",
"edit_file",
"search_replace",
"apply_diff",
"edit",
"search_and_replace",
"execute_command",
"use_mcp_tool",
"new_task",
"generate_image",
])
export type CommandRisk = "safe" | "destructive"
export function classifyCommand(toolName: ToolName): CommandRisk {
if (DESTRUCTIVE_COMMANDS.has(toolName)) {
return "destructive"
}
if (SAFE_COMMANDS.has(toolName)) {
return "safe"
}
return "destructive"
}
export function isDestructiveCommand(toolName: ToolName): boolean {
return classifyCommand(toolName) === "destructive"
}

View file

@ -0,0 +1,11 @@
import * as vscode from "vscode"
export async function showHITLApproval(toolName: string, payloadPreview?: unknown): Promise<boolean> {
const detail = payloadPreview
? `\n\nPayload Preview:\n${JSON.stringify(payloadPreview, null, 2).slice(0, 500)}`
: ""
const message = `Destructive operation requested: ${toolName}.${detail}`
const approval = await vscode.window.showWarningMessage(message, { modal: true }, "Approve", "Reject")
return approval === "Approve"
}

View file

@ -0,0 +1,90 @@
import path from "path"
interface IntentScope {
files?: string[]
}
const PATCH_HEADER_RE = /^\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)$/
function normalizePathForMatch(input: string): string {
return input.replaceAll("\\", "/").replace(/^\.?\//, "")
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
function globToRegExp(globPattern: string): RegExp {
const normalized = normalizePathForMatch(globPattern)
const withDoubleStar = normalized.replaceAll("**", "__DOUBLE_STAR__")
const withSingleStar = withDoubleStar.replaceAll("*", "__SINGLE_STAR__")
const escaped = escapeRegExp(withSingleStar)
.replaceAll("__DOUBLE_STAR__", ".*")
.replaceAll("__SINGLE_STAR__", "[^/]*")
return new RegExp(`^${escaped}$`)
}
function isPathAllowed(filePath: string, allowedPatterns: string[]): boolean {
if (allowedPatterns.length === 0) {
return false
}
const normalizedPath = normalizePathForMatch(filePath)
return allowedPatterns.some((pattern) => globToRegExp(pattern).test(normalizedPath))
}
function extractPathsFromApplyPatch(patch: string): string[] {
return patch
.split(/\r?\n/)
.map((line) => line.trim())
.map((line) => PATCH_HEADER_RE.exec(line)?.[1])
.filter((value): value is string => !!value)
}
export function getToolTargetPaths(toolName: string, nativeArgs?: Record<string, unknown>): string[] {
if (!nativeArgs) {
return []
}
switch (toolName) {
case "write_to_file":
case "apply_diff":
return typeof nativeArgs.path === "string" ? [nativeArgs.path] : []
case "edit":
case "search_and_replace":
case "search_replace":
case "edit_file":
return typeof nativeArgs.file_path === "string" ? [nativeArgs.file_path] : []
case "apply_patch":
return typeof nativeArgs.patch === "string" ? extractPathsFromApplyPatch(nativeArgs.patch) : []
default:
return []
}
}
export function enforceIntentScope(
intentId: string,
cwd: string,
intentScope: IntentScope | unknown,
targetPaths: string[],
): { allowed: true } | { allowed: false; error: string } {
const filePatterns = Array.isArray((intentScope as IntentScope)?.files)
? ((intentScope as IntentScope).files as string[])
: []
if (targetPaths.length === 0) {
return { allowed: true }
}
for (const targetPath of targetPaths) {
const relativeTarget = path.isAbsolute(targetPath) ? path.relative(cwd, targetPath) : targetPath
if (!isPathAllowed(relativeTarget, filePatterns)) {
return {
allowed: false,
error: `Scope Violation: ${intentId} is not authorized to edit [${relativeTarget}]. Request scope expansion.`,
}
}
}
return { allowed: true }
}

View file

@ -0,0 +1,17 @@
import { classifyCommand, isDestructiveCommand } from "../CommandClassifier"
describe("CommandClassifier", () => {
it("classifies read_file as safe", () => {
expect(classifyCommand("read_file")).toBe("safe")
expect(isDestructiveCommand("read_file")).toBe(false)
})
it("classifies write_to_file as destructive", () => {
expect(classifyCommand("write_to_file")).toBe("destructive")
expect(isDestructiveCommand("write_to_file")).toBe(true)
})
it("classifies execute_command as destructive", () => {
expect(classifyCommand("execute_command")).toBe("destructive")
})
})

View file

@ -0,0 +1,33 @@
import { enforceIntentScope, getToolTargetPaths } from "../ScopeEnforcer"
describe("ScopeEnforcer", () => {
it("extracts target path for write_to_file", () => {
const paths = getToolTargetPaths("write_to_file", { path: "src/core/task/Task.ts" })
expect(paths).toEqual(["src/core/task/Task.ts"])
})
it("extracts target paths from apply_patch headers", () => {
const patch = `*** Begin Patch\n*** Update File: src/core/task/Task.ts\n*** End Patch`
const paths = getToolTargetPaths("apply_patch", { patch })
expect(paths).toEqual(["src/core/task/Task.ts"])
})
it("allows path inside scope", () => {
const result = enforceIntentScope("REQ-001", "/workspace", { files: ["src/core/**"] }, [
"src/core/task/Task.ts",
])
expect(result).toEqual({ allowed: true })
})
it("blocks path outside scope with required message shape", () => {
const result = enforceIntentScope("REQ-001", "/workspace", { files: ["src/core/**"] }, [
"src/webview-ui/App.tsx",
])
expect(result.allowed).toBe(false)
if (!result.allowed) {
expect(result.error).toBe(
"Scope Violation: REQ-001 is not authorized to edit [src/webview-ui/App.tsx]. Request scope expansion.",
)
}
})
})

View file

@ -0,0 +1,131 @@
import fs from "fs/promises"
import path from "path"
import * as yaml from "yaml"
export const ACTIVE_INTENTS_RELATIVE_PATH = path.join(".orchestration", "active_intents.yaml")
export const TRACE_LOG_RELATIVE_PATH = ".roo-tool-trace.log"
export const INVALID_ACTIVE_INTENT_ERROR = "You must cite a valid active Intent ID"
export interface IntentContext {
intent_id: string
constraints: unknown
scope: unknown
relatedTraceEntries: string[]
}
type IntentEntry = Record<string, unknown>
function xmlEscape(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;")
}
function normalizeIntentEntries(data: unknown): IntentEntry[] {
if (Array.isArray(data)) {
return data.filter(Boolean) as IntentEntry[]
}
if (data && typeof data === "object") {
const root = data as Record<string, unknown>
if (Array.isArray(root.active_intents)) {
return root.active_intents.filter(Boolean) as IntentEntry[]
}
if (Array.isArray(root.intents)) {
return root.intents.filter(Boolean) as IntentEntry[]
}
return Object.entries(root).map(([intent_id, entry]) => ({
intent_id,
...(entry && typeof entry === "object" ? (entry as Record<string, unknown>) : {}),
}))
}
return []
}
function toIntentId(entry: IntentEntry): string {
return String(entry.intent_id ?? entry.id ?? "").trim()
}
function parseTraceLines(traceContent: string, intentId: string): string[] {
const normalizedIntentId = intentId.trim()
if (!normalizedIntentId) {
return []
}
const candidates = traceContent
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
return candidates.filter((line) => {
const lower = line.toLowerCase()
return (
line.includes(normalizedIntentId) ||
lower.includes(`intent_id=${normalizedIntentId.toLowerCase()}`) ||
lower.includes(`[intent:${normalizedIntentId.toLowerCase()}]`)
)
})
}
async function readYamlFile(workspaceRoot: string): Promise<unknown> {
const yamlPath = path.join(workspaceRoot, ACTIVE_INTENTS_RELATIVE_PATH)
const content = await fs.readFile(yamlPath, "utf-8")
return yaml.parse(content)
}
async function readTraceFile(workspaceRoot: string): Promise<string> {
const tracePath = path.join(workspaceRoot, TRACE_LOG_RELATIVE_PATH)
try {
return await fs.readFile(tracePath, "utf-8")
} catch {
return ""
}
}
export async function loadIntentContext(workspaceRoot: string, intentId: string): Promise<IntentContext | null> {
const data = await readYamlFile(workspaceRoot)
const intents = normalizeIntentEntries(data)
const match = intents.find((entry) => toIntentId(entry) === intentId.trim())
if (!match) {
return null
}
const traceContent = await readTraceFile(workspaceRoot)
const relatedTraceEntries = parseTraceLines(traceContent, intentId)
return {
intent_id: toIntentId(match),
constraints: match.constraints ?? {},
scope: match.scope ?? {},
relatedTraceEntries,
}
}
export function renderIntentContextXml(context: Pick<IntentContext, "constraints" | "scope">): string {
const constraintsJson = xmlEscape(JSON.stringify(context.constraints ?? {}, null, 2))
const scopeJson = xmlEscape(JSON.stringify(context.scope ?? {}, null, 2))
return `<intent_context>\n <constraints>${constraintsJson}</constraints>\n <scope>${scopeJson}</scope>\n</intent_context>`
}
export function renderIntentPreHookXml(context: IntentContext): string {
const traceXml =
context.relatedTraceEntries.length > 0
? `\n <related_trace>\n${context.relatedTraceEntries
.map((entry) => ` <entry>${xmlEscape(entry)}</entry>`)
.join("\n")}\n </related_trace>`
: ""
const constraintsJson = xmlEscape(JSON.stringify(context.constraints ?? {}, null, 2))
const scopeJson = xmlEscape(JSON.stringify(context.scope ?? {}, null, 2))
return `<intent_context_snapshot>\n <intent_id>${xmlEscape(context.intent_id)}</intent_id>\n <constraints>${constraintsJson}</constraints>\n <scope>${scopeJson}</scope>${traceXml}\n</intent_context_snapshot>`
}

View file

@ -0,0 +1,55 @@
import fs from "fs/promises"
import os from "os"
import path from "path"
import { loadIntentContext, renderIntentContextXml } from "../IntentContextLoader"
describe("IntentContextLoader", () => {
it("loads matching intent from active_intents.yaml and extracts constraints/scope", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "intent-loader-"))
const orchestrationDir = path.join(tempRoot, ".orchestration")
await fs.mkdir(orchestrationDir, { recursive: true })
await fs.writeFile(
path.join(orchestrationDir, "active_intents.yaml"),
`active_intents:
- intent_id: INTENT-TEST
constraints:
must_not:
- direct-db-write
scope:
files:
- src/**
`,
"utf8",
)
await fs.writeFile(
path.join(tempRoot, ".roo-tool-trace.log"),
`[2026-02-21T00:00:00.000Z] TOOL EXECUTED: select_active_intent [intent:INTENT-TEST] intent_id=INTENT-TEST\n`,
"utf8",
)
const result = await loadIntentContext(tempRoot, "INTENT-TEST")
expect(result).not.toBeNull()
expect(result?.intent_id).toBe("INTENT-TEST")
expect(result?.constraints).toEqual({ must_not: ["direct-db-write"] })
expect(result?.scope).toEqual({ files: ["src/**"] })
expect(result?.relatedTraceEntries.length).toBe(1)
await fs.rm(tempRoot, { recursive: true, force: true })
})
it("returns XML with constraints and scope only", () => {
const xml = renderIntentContextXml({
constraints: { must: ["x"] },
scope: { files: ["src/**"] },
})
expect(xml).toContain("<intent_context>")
expect(xml).toContain("<constraints>")
expect(xml).toContain("<scope>")
expect(xml).not.toContain("<intent_id>")
})
})

View file

@ -25,6 +25,9 @@ import {
getSkillsSection,
} from "./sections"
const INTENT_DRIVEN_ARCHITECT_RULE =
"You are an Intent-Driven Architect. You CANNOT write code immediately. Your first action MUST be to analyze the user request and call select_active_intent to load the necessary context."
// Helper function to get prompt component, filtering out empty objects
export function getPromptComponent(
customModePrompts: CustomModePrompts | undefined,
@ -82,7 +85,9 @@ async function generatePrompt(
// Tools catalog is not included in the system prompt.
const toolsCatalog = ""
const basePrompt = `${roleDefinition}
const basePrompt = `${INTENT_DRIVEN_ARCHITECT_RULE}
${roleDefinition}
${markdownFormattingSection()}

View file

@ -13,6 +13,7 @@ import newTask from "./new_task"
import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import selectActiveIntent from "./select_active_intent"
import skill from "./skill"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
readCommandOutput,
createReadFileTool(readFileOptions),
runSlashCommand,
selectActiveIntent,
skill,
searchReplace,
edit_file,

View file

@ -0,0 +1,25 @@
import type OpenAI from "openai"
const SELECT_ACTIVE_INTENT_DESCRIPTION = `Load the active intent context from .orchestration/active_intents.yaml by intent ID. Use this before planning or coding so constraints and scope are explicitly available in the next turn.`
const INTENT_ID_PARAMETER_DESCRIPTION = `Intent ID to load from .orchestration/active_intents.yaml (for example: intent-1)`
export default {
type: "function",
function: {
name: "select_active_intent",
description: SELECT_ACTIVE_INTENT_DESCRIPTION,
strict: true,
parameters: {
type: "object",
properties: {
intent_id: {
type: "string",
description: INTENT_ID_PARAMETER_DESCRIPTION,
},
},
required: ["intent_id"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool

View file

@ -132,6 +132,7 @@ import { AutoApprovalHandler, checkAutoApproval } from "../auto-approval"
import { MessageManager } from "../message-manager"
import { validateAndFixToolResultIds } from "./validateToolResultIds"
import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages"
import { INVALID_ACTIVE_INTENT_ERROR, loadIntentContext, renderIntentPreHookXml } from "../intent/IntentContextLoader"
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
@ -2643,9 +2644,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return true
})
// Add environment details as its own text block, separate from tool
// results.
let finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }]
const intentPreHookContext = await this.buildIntentPreHookContext()
// Add environment details and pre-hook intent context as dedicated text blocks.
let finalUserContent = [
...contentWithoutEnvDetails,
{ type: "text" as const, text: environmentDetails },
{ type: "text" as const, text: intentPreHookContext },
]
// Only add user message to conversation history if:
// 1. This is the first attempt (retryAttempt === 0), AND
// 2. The original userContent was not empty (empty signals delegation resume where
@ -3818,6 +3824,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
})()
}
private async buildIntentPreHookContext(): Promise<string> {
const state = await this.providerRef.deref()?.getState()
const activeIntentId = String((state as any)?.activeIntentId ?? "").trim()
if (!activeIntentId) {
return `<intent_gatekeeper_error>${INVALID_ACTIVE_INTENT_ERROR}</intent_gatekeeper_error>`
}
try {
const context = await loadIntentContext(this.cwd, activeIntentId)
if (!context) {
return `<intent_gatekeeper_error>${INVALID_ACTIVE_INTENT_ERROR}</intent_gatekeeper_error>`
}
return renderIntentPreHookXml(context)
} catch {
return `<intent_gatekeeper_error>${INVALID_ACTIVE_INTENT_ERROR}</intent_gatekeeper_error>`
}
}
private getCurrentProfileId(state: any): string {
return (
state?.listApiConfigMeta?.find((profile: any) => profile.name === state?.currentApiConfigName)?.id ??

View file

@ -25,6 +25,14 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
readonly name = "apply_diff" as const
async execute(params: ApplyDiffParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
console.log("%c🟡 TOOL EXECUTED: " + this.name, "color: orange; font-size: 16px; font-weight: bold")
console.log("%c🟡 File operation in progress", "color: orange; font-size: 16px")
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
const { askApproval, handleError, pushToolResult } = callbacks
let { path: relPath, diff: diffContent } = params

View file

@ -53,6 +53,14 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
}
async execute(params: ApplyPatchParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
console.log("%c🟡 TOOL EXECUTED: " + this.name, "color: orange; font-size: 16px; font-weight: bold")
console.log("%c🟡 File operation in progress", "color: orange; font-size: 16px")
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
const { patch } = params
const { askApproval, handleError, pushToolResult } = callbacks

View file

@ -137,6 +137,14 @@ export class EditFileTool extends BaseTool<"edit_file"> {
private partialToolAskRelPath: string | undefined
async execute(params: EditFileParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
console.log("%c🟡 TOOL EXECUTED: " + this.name, "color: orange; font-size: 16px; font-weight: bold")
console.log("%c🟡 File operation in progress", "color: orange; font-size: 16px")
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
// Coerce old_string/new_string to handle malformed native tool calls where they could be non-strings.
// In native mode, malformed calls can pass numbers/objects; normalize those to "" to avoid later crashes.
const file_path = params.file_path

View file

@ -26,6 +26,14 @@ export class EditTool extends BaseTool<"edit"> {
readonly name = "edit" as const
async execute(params: EditParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
console.log("%c🟡 TOOL EXECUTED: " + this.name, "color: orange; font-size: 16px; font-weight: bold")
console.log("%c🟡 File operation in progress", "color: orange; font-size: 16px")
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
const { file_path: relPath, old_string: oldString, new_string: newString, replace_all: replaceAll } = params
const { askApproval, handleError, pushToolResult } = callbacks

View file

@ -25,6 +25,14 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
readonly name = "search_replace" as const
async execute(params: SearchReplaceParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
console.log("%c🟡 TOOL EXECUTED: " + this.name, "color: orange; font-size: 16px; font-weight: bold")
console.log("%c🟡 File operation in progress", "color: orange; font-size: 16px")
console.log("%c🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡🟡", "color: orange; font-size: 16px")
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
const { file_path, old_string, new_string } = params
const { askApproval, handleError, pushToolResult } = callbacks

View file

@ -0,0 +1,43 @@
import fs from "fs/promises"
import path from "path"
import { ACTIVE_INTENTS_RELATIVE_PATH, loadIntentContext, renderIntentContextXml } from "../intent/IntentContextLoader"
export class SelectActiveIntentTool {
async handle(params: { intent_id: string }, workspaceRoot: string, provider?: any): Promise<string> {
const intentId = params.intent_id?.trim()
if (!intentId) {
return "ERROR: Missing required parameter 'intent_id'."
}
try {
const context = await loadIntentContext(workspaceRoot, intentId)
if (!context) {
return `ERROR: Intent '${intentId}' not found in .orchestration/active_intents.yaml.`
}
await provider?.updateGlobalState("activeIntentId", intentId)
const tracePath = path.join(workspaceRoot, ".roo-tool-trace.log")
await fs
.appendFile(
tracePath,
`[${new Date().toISOString()}] TOOL EXECUTED: select_active_intent [intent:${intentId}] intent_id=${intentId}\n`,
"utf8",
)
.catch(() => {})
return renderIntentContextXml(context)
} catch (error) {
if ((error as any)?.code === "ENOENT") {
if ((error as any)?.path?.toString()?.includes(ACTIVE_INTENTS_RELATIVE_PATH)) {
return "ERROR: Governance sidecar not found at .orchestration/active_intents.yaml. Please initialize Phase 0 first."
}
return "ERROR: Governance sidecar not found. Please ensure .orchestration/active_intents.yaml exists."
}
return `ERROR: Failed to read sidecar: ${error instanceof Error ? error.message : String(error)}`
}
}
}

View file

@ -15,6 +15,8 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
import type { ToolUse } from "../../shared/tools"
import { INVALID_ACTIVE_INTENT_ERROR } from "../intent/IntentContextLoader"
import { appendAgentTrace } from "../trace/AgentTraceSerializer"
import { BaseTool, ToolCallbacks } from "./BaseTool"
@ -25,10 +27,17 @@ interface WriteToFileParams {
export class WriteToFileTool extends BaseTool<"write_to_file"> {
readonly name = "write_to_file" as const
private initialFileHash: string | undefined
async execute(params: WriteToFileParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
try {
const tracePath = path.join(task.cwd, ".roo-tool-trace.log")
await fs.appendFile(tracePath, `[${new Date().toISOString()}] TOOL EXECUTED: ${this.name}\n`, "utf8")
} catch {}
const { pushToolResult, handleError, askApproval } = callbacks
const relPath = params.path
const accessAllowed = task.rooIgnoreController?.validateAccess(relPath || "")
let newContent = params.content
if (!relPath) {
@ -47,7 +56,20 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
return
}
const accessAllowed = task.rooIgnoreController?.validateAccess(relPath)
const hasSelectedActiveIntent = (task.toolUsage.select_active_intent?.attempts ?? 0) > 0
const provider = task.providerRef.deref()
const state = await provider?.getState()
const hasActiveIntentState = !!(state as any)?.activeIntentId
if (!hasSelectedActiveIntent && !hasActiveIntentState) {
const governanceError = `GOVERNANCE ERROR: ${INVALID_ACTIVE_INTENT_ERROR} before modifying files.`
task.consecutiveMistakeCount++
task.recordToolError("write_to_file", governanceError)
task.didToolFailInCurrentTurn = true
pushToolResult(formatResponse.toolError(governanceError))
await task.diffViewProvider.reset()
return
}
if (!accessAllowed) {
await task.say("rooignore_error", relPath)
@ -67,6 +89,16 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
task.diffViewProvider.editType = fileExists ? "modify" : "create"
}
this.initialFileHash = undefined
if (fileExists) {
try {
const { getCurrentHash } = await import("../concurrency/OptimisticLock")
this.initialFileHash = await getCurrentHash(absolutePath)
} catch {
this.initialFileHash = undefined
}
}
// Create parent directories early for new files to prevent ENOENT errors
// in subsequent operations (e.g., diffViewProvider.open, fs.readFile)
if (!fileExists) {
@ -101,6 +133,7 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
const provider = task.providerRef.deref()
const state = await provider?.getState()
const activeIntentId = String((state as any)?.activeIntentId ?? "").trim()
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
@ -108,6 +141,30 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
)
const validateOptimisticLockOrThrow = async () => {
if (!fileExists || !this.initialFileHash) {
return
}
try {
const { validateLock, getCurrentHash, StaleFileError } = await import(
"../concurrency/OptimisticLock"
)
const isLockValid = await validateLock(this.initialFileHash, absolutePath)
if (!isLockValid) {
const currentHash = await getCurrentHash(absolutePath).catch(() => "unavailable")
throw new StaleFileError(relPath, this.initialFileHash, currentHash)
}
} catch (error) {
const { StaleFileError } = await import("../concurrency/OptimisticLock").catch(() => ({
StaleFileError: undefined,
}))
if (StaleFileError && error instanceof StaleFileError) {
throw error
}
}
}
if (isPreventFocusDisruptionEnabled) {
task.diffViewProvider.editType = fileExists ? "modify" : "create"
if (fileExists) {
@ -133,6 +190,22 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
return
}
try {
await validateOptimisticLockOrThrow()
} catch (error) {
const staleErrorMessage =
error instanceof Error
? error.message
: "STALE_FILE_ERROR: File changed since it was last read. Re-read the file before applying changes."
task.consecutiveMistakeCount++
task.recordToolError("write_to_file", staleErrorMessage)
task.didToolFailInCurrentTurn = true
pushToolResult(formatResponse.toolError(staleErrorMessage))
await task.diffViewProvider.reset()
this.resetPartialState()
return
}
await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
} else {
if (!task.diffViewProvider.isEditing) {
@ -166,9 +239,28 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
return
}
try {
await validateOptimisticLockOrThrow()
} catch (error) {
const staleErrorMessage =
error instanceof Error
? error.message
: "STALE_FILE_ERROR: File changed since it was last read. Re-read the file before applying changes."
task.consecutiveMistakeCount++
task.recordToolError("write_to_file", staleErrorMessage)
task.didToolFailInCurrentTurn = true
pushToolResult(formatResponse.toolError(staleErrorMessage))
await task.diffViewProvider.revertChanges()
await task.diffViewProvider.reset()
this.resetPartialState()
return
}
await task.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
}
this.initialFileHash = undefined
if (relPath) {
await task.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
}
@ -179,6 +271,16 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
pushToolResult(message)
if (activeIntentId && relPath) {
await appendAgentTrace({
workspaceRoot: task.cwd,
activeIntentId,
filePath: relPath,
content: newContent,
toolName: this.name,
}).catch(() => {})
}
await task.diffViewProvider.reset()
this.resetPartialState()
@ -190,6 +292,8 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
await task.diffViewProvider.reset()
this.resetPartialState()
return
} finally {
this.initialFileHash = undefined
}
}

View file

@ -0,0 +1,64 @@
import { SelectActiveIntentTool } from "../SelectActiveIntentTool"
import * as loader from "../../intent/IntentContextLoader"
vi.mock("../../intent/IntentContextLoader", async () => {
const actual = await vi.importActual<typeof import("../../intent/IntentContextLoader")>(
"../../intent/IntentContextLoader",
)
return {
...actual,
loadIntentContext: vi.fn(),
}
})
describe("SelectActiveIntentTool", () => {
const mockedLoadIntentContext = vi.mocked(loader.loadIntentContext)
const tool = new SelectActiveIntentTool()
const workspaceRoot = "/workspace"
beforeEach(() => {
vi.clearAllMocks()
})
it("returns intent_context XML for matching intent in intents array", async () => {
mockedLoadIntentContext.mockResolvedValue({
intent_id: "intent-1",
constraints: {
must_not: ["direct_db_writes"],
},
scope: {
files: ["src/**"],
},
relatedTraceEntries: [],
})
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toContain("<intent_context>")
expect(result).toContain("<constraints>")
expect(result).toContain("direct_db_writes")
expect(result).toContain("<scope>")
expect(result).toContain("src/**")
expect(result).toContain("</intent_context>")
})
it("returns not found error when intent_id does not exist", async () => {
mockedLoadIntentContext.mockResolvedValue(null)
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toBe("ERROR: Intent 'intent-1' not found in .orchestration/active_intents.yaml.")
})
it("returns initialization error when sidecar file is missing", async () => {
const missingFileError = Object.assign(new Error("ENOENT"), { code: "ENOENT" })
mockedLoadIntentContext.mockRejectedValue(missingFileError)
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toBe(
"ERROR: Governance sidecar not found. Please ensure .orchestration/active_intents.yaml exists.",
)
})
})

View file

@ -123,6 +123,10 @@ describe("writeToFileTool", () => {
mockCline.cwd = "/"
mockCline.consecutiveMistakeCount = 0
mockCline.didEditFile = false
mockCline.didToolFailInCurrentTurn = false
mockCline.toolUsage = {
select_active_intent: { attempts: 1, failures: 0 },
}
mockCline.diffStrategy = undefined
mockCline.providerRef = {
deref: vi.fn().mockReturnValue({
@ -141,6 +145,7 @@ describe("writeToFileTool", () => {
originalContent: "",
open: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockResolvedValue(undefined),
saveDirectly: vi.fn().mockResolvedValue(undefined),
reset: vi.fn().mockResolvedValue(undefined),
revertChanges: vi.fn().mockResolvedValue(undefined),
saveChanges: vi.fn().mockResolvedValue({
@ -236,6 +241,22 @@ describe("writeToFileTool", () => {
}
describe("access control", () => {
it("blocks write when no active intent has been selected in this session", async () => {
mockCline.toolUsage = {}
const result = await executeWriteFileTool({})
expect(result).toBe(
"Error: GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
)
expect(mockCline.recordToolError).toHaveBeenCalledWith(
"write_to_file",
"GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
)
expect(mockCline.diffViewProvider.saveChanges).not.toHaveBeenCalled()
expect(mockCline.diffViewProvider.saveDirectly).not.toHaveBeenCalled()
})
it("validates and allows access when rooIgnoreController permits", async () => {
await executeWriteFileTool({}, { accessAllowed: true })

View file

@ -0,0 +1,96 @@
import fs from "fs/promises"
import path from "path"
import * as yaml from "yaml"
import { hashContent } from "./ContentHasher"
import { classifySemanticChange, type SemanticChangeType } from "./SemanticClassifier"
const ACTIVE_INTENTS_PATH = path.join(".orchestration", "active_intents.yaml")
const AGENT_TRACE_PATH = path.join(".orchestration", "agent_trace.jsonl")
interface IntentRecord {
intent_id?: unknown
id?: unknown
title?: unknown
scope?: {
operations?: unknown
}
}
function normalizeIntentRecords(data: unknown): IntentRecord[] {
if (Array.isArray(data)) {
return data as IntentRecord[]
}
if (data && typeof data === "object") {
const root = data as Record<string, unknown>
if (Array.isArray(root.active_intents)) {
return root.active_intents as IntentRecord[]
}
if (Array.isArray(root.intents)) {
return root.intents as IntentRecord[]
}
}
return []
}
function normalizeIntentId(record: IntentRecord): string {
return String(record.intent_id ?? record.id ?? "").trim()
}
async function loadIntentMetadata(workspaceRoot: string, activeIntentId: string): Promise<IntentRecord | undefined> {
if (!activeIntentId) {
return undefined
}
try {
const content = await fs.readFile(path.join(workspaceRoot, ACTIVE_INTENTS_PATH), "utf8")
const parsed = yaml.parse(content)
const intents = normalizeIntentRecords(parsed)
return intents.find((record) => normalizeIntentId(record) === activeIntentId)
} catch {
return undefined
}
}
export interface AgentTraceAppendInput {
workspaceRoot: string
activeIntentId: string
filePath: string
content: string
toolName: string
}
interface AgentTraceEntry {
timestamp: string
intent_id: string
file_path: string
content_sha256: string
semantic_change: SemanticChangeType
tool: string
}
export async function appendAgentTrace(input: AgentTraceAppendInput): Promise<void> {
const intent = await loadIntentMetadata(input.workspaceRoot, input.activeIntentId)
const semanticChange = classifySemanticChange({
intentTitle: intent?.title,
intentOperations: intent?.scope?.operations,
filePath: input.filePath,
})
const entry: AgentTraceEntry = {
timestamp: new Date().toISOString(),
intent_id: input.activeIntentId,
file_path: input.filePath,
content_sha256: hashContent(input.content),
semantic_change: semanticChange,
tool: input.toolName,
}
const tracePath = path.join(input.workspaceRoot, AGENT_TRACE_PATH)
await fs.mkdir(path.dirname(tracePath), { recursive: true })
await fs.appendFile(tracePath, `${JSON.stringify(entry)}\n`, "utf8")
}

View file

@ -0,0 +1,5 @@
import { createHash } from "crypto"
export function hashContent(content: string): string {
return createHash("sha256").update(content, "utf8").digest("hex")
}

View file

@ -0,0 +1,30 @@
export type SemanticChangeType = "REFACTOR" | "EVOLUTION"
const REFACTOR_PATTERN = /\b(refactor|rewrite|rename|restructure|cleanup|simplif(y|ication))\b/i
function toSearchText(value: unknown): string {
if (typeof value === "string") {
return value
}
if (Array.isArray(value)) {
return value.map((item) => toSearchText(item)).join(" ")
}
if (value && typeof value === "object") {
return Object.values(value as Record<string, unknown>)
.map((item) => toSearchText(item))
.join(" ")
}
return ""
}
export function classifySemanticChange(input: {
intentTitle?: unknown
intentOperations?: unknown
filePath?: string
}): SemanticChangeType {
const corpus = [input.intentTitle, input.intentOperations, input.filePath].map((v) => toSearchText(v)).join(" ")
return REFACTOR_PATTERN.test(corpus) ? "REFACTOR" : "EVOLUTION"
}

View file

@ -1196,16 +1196,29 @@ export class ClineProvider
let localPort = "5173"
try {
const fs = require("fs")
const path = require("path")
const portFilePath = path.resolve(__dirname, "../../.vite-port")
const portFileCandidates = [
path.resolve(__dirname, "../../.vite-port"),
path.resolve(__dirname, "../../../.vite-port"),
path.resolve(__dirname, "../../../../.vite-port"),
]
if (fs.existsSync(portFilePath)) {
localPort = fs.readFileSync(portFilePath, "utf8").trim()
console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`)
let resolvedPortFilePath: string | undefined
for (const candidatePath of portFileCandidates) {
try {
await fs.access(candidatePath)
resolvedPortFilePath = candidatePath
break
} catch {
// Try next location.
}
}
if (resolvedPortFilePath) {
localPort = (await fs.readFile(resolvedPortFilePath, "utf8")).trim()
console.log(`[ClineProvider:Vite] Using Vite server port from ${resolvedPortFilePath}: ${localPort}`)
} else {
console.log(
`[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`,
`[ClineProvider:Vite] Port file not found in known locations, using default port: ${localPort}`,
)
}
} catch (err) {

View file

@ -194,6 +194,28 @@ export async function activate(context: vscode.ExtensionContext) {
// Initialize the provider *before* the Roo Code Cloud service.
const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService)
// Clear stale intent state on startup so governance checks don't pass due to
// an old activeIntentId from a previous session/workspace state.
try {
const state = await provider.getState()
const activeIntentId = String((state as any)?.activeIntentId ?? "").trim()
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
if (activeIntentId && workspaceRoot) {
const { loadIntentContext } = await import("./core/intent/IntentContextLoader")
const context = await loadIntentContext(workspaceRoot, activeIntentId).catch(() => null)
if (!context) {
await provider.contextProxy.updateGlobalState("activeIntentId" as any, undefined as any)
outputChannel.appendLine(`[Governance] Cleared stale activeIntentId: ${activeIntentId}`)
}
}
} catch (error) {
outputChannel.appendLine(
`[Governance] Failed stale activeIntentId check: ${error instanceof Error ? error.message : String(error)}`,
)
}
// Initialize Roo Code Cloud service.
const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebviewWithoutClineMessages()

38
src/hooks/README.md Normal file
View file

@ -0,0 +1,38 @@
# Hooks Directory - Clean Hook Interfaces
This directory contains the clean, composable hook implementations for the governed AI-native IDE.
## Pre-Hooks (Execution Phase)
| Hook | Phase | Purpose |
| ------------------- | ------- | --------------------------------------------------- |
| `validateIntent` | Phase 1 | Ensures active intent exists before execution |
| `classifyCommand` | Phase 2 | Categorizes SAFE vs DESTRUCTIVE commands |
| `enforceScope` | Phase 2 | Validates file against intent's owned_scope |
| `authorizeHITL` | Phase 2 | Shows user approval dialog for destructive commands |
| `loadIntentContext` | Phase 1 | Loads intent context from active_intents.yaml |
## Post-Hooks (Completion Phase)
| Hook | Phase | Purpose |
| -------------------- | ------- | ----------------------------------------------- |
| `hashContent` | Phase 3 | Generates SHA-256 content hash for traceability |
| `classifyMutation` | Phase 3 | Determines REFACTOR vs EVOLUTION |
| `recordTrace` | Phase 3 | Appends trace entry to agent_trace.jsonl |
| `validateLock` | Phase 4 | Checks for stale files before write |
| `captureInitialHash` | Phase 4 | Captures pre-write hash for optimistic locking |
## Integration Points
These hooks are wired into the following locations:
- **Pre-execution:** `src/core/assistant-message/presentAssistantMessage.ts`
- **Post-execution:** `src/core/tools/WriteToFileTool.ts`
- **Pre-prompt:** `src/core/task/Task.ts`
## Architecture Philosophy
- **Isolated:** Each hook is independent and focused
- **Composable:** Hooks can be combined in different orders
- **Fail-safe:** All hooks are wrapped in try/catch blocks
- **Non-intrusive:** Added without modifying core logic

3
src/hooks/index.ts Normal file
View file

@ -0,0 +1,3 @@
// Main hooks export
export * from "./pre-hooks"
export * from "./post-hooks"

View file

@ -0,0 +1,30 @@
import { INVALID_ACTIVE_INTENT_ERROR, loadIntentContext } from "../core/intent/IntentContextLoader"
import type { IntentHookContext, HookResult } from "./types"
export async function runIntentPreflightHook(context: IntentHookContext): Promise<HookResult> {
const activeIntentId = String(context.activeIntentId ?? "").trim()
if (!activeIntentId) {
return {
ok: false,
error: INVALID_ACTIVE_INTENT_ERROR,
}
}
try {
const intentContext = await loadIntentContext(context.workspaceRoot, activeIntentId)
if (!intentContext) {
return {
ok: false,
error: INVALID_ACTIVE_INTENT_ERROR,
}
}
return { ok: true }
} catch {
return {
ok: false,
error: INVALID_ACTIVE_INTENT_ERROR,
}
}
}

View file

@ -0,0 +1,27 @@
import { getCurrentHash, StaleFileError, validateLock } from "../core/concurrency/OptimisticLock"
import type { HookResult, OptimisticLockContext } from "./types"
export interface OptimisticLockResult extends HookResult {
currentHash?: string
}
export async function runOptimisticLockPreWriteHook(context: OptimisticLockContext): Promise<OptimisticLockResult> {
const expectedHash = String(context.expectedHash ?? "").trim()
if (!expectedHash) {
return { ok: true }
}
const isValid = await validateLock(expectedHash, context.filePath)
if (isValid) {
return { ok: true }
}
const currentHash = await getCurrentHash(context.filePath).catch(() => "unavailable")
const error = new StaleFileError(context.filePath, expectedHash, currentHash)
return {
ok: false,
error: error.message,
currentHash,
}
}

29
src/hooks/post-hooks.ts Normal file
View file

@ -0,0 +1,29 @@
export * from "./types"
export * from "./tracePostWriteHook"
import { hashContent as hashContentInternal } from "../core/trace/ContentHasher"
import { classifySemanticChange } from "../core/trace/SemanticClassifier"
import { appendAgentTrace, type AgentTraceAppendInput } from "../core/trace/AgentTraceSerializer"
import { getCurrentHash, validateLock } from "../core/concurrency/OptimisticLock"
// Post-execution hooks that run after tools
export const postHooks = {
// Phase 3: Content hashing
hashContent: (content: string) => hashContentInternal(content),
// Phase 3: Semantic classification
classifyMutation: (intentId: string, filePath: string) =>
classifySemanticChange({
intentTitle: intentId,
filePath,
}),
// Phase 3: Trace recording
recordTrace: async (entry: AgentTraceAppendInput) => appendAgentTrace(entry),
// Phase 4: Optimistic locking
validateLock: async (expectedHash: string, filePath: string) => validateLock(expectedHash, filePath),
// Phase 4: Capture initial hash
captureInitialHash: async (filePath: string) => getCurrentHash(filePath),
}

42
src/hooks/pre-hooks.ts Normal file
View file

@ -0,0 +1,42 @@
export * from "./types"
export * from "./intentPreflightHook"
export * from "./optimisticLockPreWriteHook"
import type { ToolName } from "@roo-code/types"
import { classifyCommand } from "../core/governance/CommandClassifier"
import { enforceIntentScope } from "../core/governance/ScopeEnforcer"
import { showHITLApproval } from "../core/governance/HITLAuthorizer"
import { loadIntentContext as loadIntentContextInternal } from "../core/intent/IntentContextLoader"
interface TaskLike {
providerRef?: {
deref?: () => { getState?: () => Promise<unknown> } | undefined
}
}
// Pre-execution hooks that run before tools
export const preHooks = {
// Phase 1: Intent validation
validateIntent: async (task: TaskLike) => {
const state = await task.providerRef?.deref?.()?.getState?.()
return !!(state as any)?.activeIntentId
},
// Phase 2: Command classification
classifyCommand: (toolName: string) => classifyCommand(toolName as ToolName),
// Phase 2: Scope enforcement
enforceScope: async (
filePath: string,
intentId: string,
cwd: string,
intentScope?: { files?: string[] } | unknown,
) => enforceIntentScope(intentId, cwd, intentScope ?? {}, [filePath]),
// Phase 2: HITL authorization
authorizeHITL: async (toolName: string, params: unknown) => showHITLApproval(toolName, params),
// Phase 1: Context loading
loadIntentContext: async (intentId: string, cwd: string) => loadIntentContextInternal(cwd, intentId),
}

View file

@ -0,0 +1,21 @@
import { appendAgentTrace } from "../core/trace/AgentTraceSerializer"
import type { HookResult, TraceHookContext } from "./types"
export async function runTracePostWriteHook(context: TraceHookContext): Promise<HookResult> {
try {
await appendAgentTrace({
workspaceRoot: context.workspaceRoot,
activeIntentId: context.activeIntentId,
filePath: context.filePath,
content: context.content,
toolName: context.toolName,
})
return { ok: true }
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
}
}
}

24
src/hooks/types.ts Normal file
View file

@ -0,0 +1,24 @@
export interface HookContext {
workspaceRoot: string
}
export interface HookResult {
ok: boolean
error?: string
}
export interface IntentHookContext extends HookContext {
activeIntentId?: string
}
export interface TraceHookContext extends HookContext {
activeIntentId: string
filePath: string
content: string
toolName: string
}
export interface OptimisticLockContext extends HookContext {
expectedHash?: string
filePath: string
}

View file

@ -58,6 +58,7 @@ export const toolParamNames = [
"todos",
"prompt",
"image",
"intent_id",
// read_file parameters (native protocol)
"operations", // search_and_replace parameter for multiple operations
"patch", // apply_patch parameter
@ -113,6 +114,7 @@ export type NativeToolArgs = {
skill: { skill: string; args?: string }
search_files: { path: string; regex: string; file_pattern?: string | null }
switch_mode: { mode_slug: string; reason: string }
select_active_intent: { intent_id: string }
update_todo_list: { todos: string }
use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> }
write_to_file: { path: string; content: string }
@ -283,6 +285,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
ask_followup_question: "ask questions",
attempt_completion: "complete tasks",
switch_mode: "switch modes",
select_active_intent: "load intent context",
new_task: "create new task",
codebase_search: "codebase search",
update_todo_list: "update todo list",
@ -308,7 +311,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["use_mcp_tool", "access_mcp_resource"],
},
modes: {
tools: ["switch_mode", "new_task"],
tools: ["switch_mode", "select_active_intent", "new_task"],
alwaysAvailable: true,
},
}
@ -318,6 +321,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"select_active_intent",
"new_task",
"update_todo_list",
"run_slash_command",

1
test.txt Normal file
View file

@ -0,0 +1 @@
Hello World

0
testinig.txt Normal file
View file

0
testtt.txt Normal file
View file

1
unauthorized_test.txt Normal file
View file

@ -0,0 +1 @@
This should not work