Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .orchestration/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CLAUDE.md

# Shared Brain for Roo Code Agent Sessions

## Project: AI-Native IDE – Week 1

### Lessons Learned

- PreHook enforces scope and intent validation.
- PostHook logs AST diffs, content hashes, and mutation class.
- Session IDs and contributor model must be tied dynamically to each agent instance.
- Active intents are required for agent execution; context injection is mandatory.
- Parallel agents must respect content hashes to prevent stale file overwrites.

### Architectural Notes

- Hooks isolated and composable.
- agent_trace.jsonl stores intent → file → hash → mutation_class.
- Future: integrate real Git SHA for vcs.revision_id.
- Future: append lessons automatically when linter/test fails.
34 changes: 34 additions & 0 deletions .orchestration/active_intents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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"
- "Must maintain backward compatibility with Basic Auth"
acceptance_criteria:
- "Unit tests in tests/auth/ pass"

- id: "INT-002"
name: "Refactor Billing Service"
status: "IN_PROGRESS"
owned_scope:
- "src/billing/**"
constraints:
- "Preserve API endpoints"
- "Maintain decimal precision"
acceptance_criteria:
- "Integration tests in tests/billing/ pass"

- id: "INT-003"
name: "Add Logging Middleware"
status: "IN_PROGRESS"
owned_scope:
- "src/middleware/logging.ts"
constraints:
- "Log only request metadata"
- "Must be async non-blocking"
acceptance_criteria:
- "Logs appear in console without delaying response"
3 changes: 3 additions & 0 deletions .orchestration/agent_trace.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"id":"43b95d93-fcc5-4480-b80e-c033f9e9b11b","timestamp":"2026-02-21T00:00:00.000Z","vcs":{"revision_id":"local-dev"},"files":[{"relative_path":"src/core/assistant-message/presentAssistantMessage.ts","conversations":[{"url":"session:local:manual-entry","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":1,"end_line":1069,"content_hash":"sha256:209ad21a8df7e2aa03bde9ea54de60bce13147ea352c700f89229d1f43c5407f"}],"related":[{"type":"specification","value":"INT-001"}],"mutation_class":"AST_REFACTOR"}]}]}
{"id":"a451fa4e-65a4-4f8f-ac20-0839a389bd11","timestamp":"2026-02-21T16:52:50Z","vcs":{"revision_id":"local-dev"},"files":[{"relative_path":"src/hooks/preHook.ts","conversations":[{"url":"session:local:manual-entry","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":1,"end_line":77,"content_hash":"sha256:15af55ba540fe35138544976f89a98469717fb72b50e1555a2f935955a578e8f"}],"related":[{"type":"specification","value":"INT-002"}],"mutation_class":"AST_REFACTOR"}]}]}
{"id":"ff8c2018-c345-4270-af94-80c8e4a3bcc1","timestamp":"2026-02-21T16:52:50Z","vcs":{"revision_id":"local-dev"},"files":[{"relative_path":"src/hooks/postHook.ts","conversations":[{"url":"session:local:manual-entry","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":1,"end_line":126,"content_hash":"sha256:ffd9f315c486988097be84aaca3057bece77fb65e8af95eb5102fa75770e6481"}],"related":[{"type":"specification","value":"INT-003"}],"mutation_class":"AST_REFACTOR"}]}]}
5 changes: 5 additions & 0 deletions .orchestration/intent_map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Intent Map - Spatial Layout

- INT-001: src/auth/jwt.ts, src/middleware/jwt.ts
- INT-002: src/billing/service.ts, src/billing/utils.ts
- INT-003: src/middleware/logging.ts
38 changes: 38 additions & 0 deletions ARCHITECTURE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Roo Code Hook Architecture Notes

## Entry Points

- **execute_command**: Handles all tool execution requests from the agent.
- **write_file**: Performs all codebase mutations initiated by the agent.
- These are the primary points where the hook engine intercepts and enforces governance.

## Hook Engine

- **preHook(intentId)**:
- Validates the selected intent against `active_intents.yaml`.
- Injects intent-specific constraints and owned scope into the agent context.
- Blocks execution if no valid intent is selected.
- **postHook(intentId, filePath, content)**:
- Computes SHA-256 hash of the modified code to ensure **spatial independence**.
- Appends a structured entry to `.orchestration/agent_trace.jsonl`, linking the change back to the business intent.
- Supports multiple intents and can track simultaneous agent actions.

## Orchestration Folder

- **active_intents.yaml**: Stores all high-level business intents, their status, constraints, and acceptance criteria.
- **agent_trace.jsonl**: Append-only ledger capturing all mutating actions, with intent mapping and content hashes.
- **intent_map.md**: Maps business intents to physical files and AST nodes for traceability and future planning.

## Demo Flow

1. User selects **INT-001** (e.g., JWT Authentication Migration).
2. **preHook** injects constraints and scope for the selected intent.
3. Agent executes `write_file` to modify code (e.g., `src/auth/jwt.ts`).
4. **postHook** records the file content hash and updates `agent_trace.jsonl`.
5. Repeat for other intents (e.g., INT-002 Billing Refactor, INT-003 Logging Middleware) to demonstrate parallel intent handling.

**Key Features Demonstrated:**

- **Traceability:** Every code change is linked to a specific intent.
- **Governance:** Hooks enforce scope and constraints before code is written.
- **Scalability:** Multiple intents can be managed concurrently with consistent context injection and trace logging.
172 changes: 123 additions & 49 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../sha

import { AskIgnoredError } from "../task/AskIgnoredError"
import { Task } from "../task/Task"

import { preWriteHook } from "../../hooks/preHook"
import { postWriteHook } from "../../hooks/postHook"
import { listFilesTool } from "../tools/ListFilesTool"
import { readFileTool } from "../tools/ReadFileTool"
import { readCommandOutputTool } from "../tools/ReadCommandOutputTool"
Expand Down Expand Up @@ -58,6 +59,66 @@ import { sanitizeToolUseId } from "../../utils/tool-id"
* as it becomes available.
*/

async function runWithHooks<T extends ToolName>(
cline: Task,
block: ToolUse<T>,
handler: ToolHandler<T>,
options?: { checkpoint?: boolean },
) {
if (options?.checkpoint) await checkpointSaveAndMark(cline)

// ===== BRIDGE START =====
// convert Roo Code block/agent info to what your hooks expect
const filePath = block.filePath || block.target // actual file being edited
const intentId = block.intentId || cline.activeIntentId // pick from your agent context
const sessionId = cline.sessionId || crypto.randomUUID() // per-session ID
const contributorModel = cline.modelIdentifier || "claude-3-5-sonnet" // agent model name
// ===== BRIDGE END =====

// pre-hook can access cline and block
await preWriteHook(filePath, intentId)

// Keep helpers in scope so handlers still compile
const askApproval = async (
type: ClineAsk,
partialMessage?: string,
progressStatus?: ToolProgressStatus,
isProtected?: boolean,
) => {
const { response, text, images } = await cline.ask(
type,
partialMessage,
false,
progressStatus,
isProtected || false,
)
if (response !== "yesButtonClicked") {
if (text) {
await cline.say("user_feedback", text, images)
pushToolResult(formatResponse.toolDeniedWithFeedback(text), images)
} else {
pushToolResult(formatResponse.toolDenied())
}
return false
}
return true
}

const pushToolResult = (content: ToolResponse, images?: string[]) => {
cline.pushToolResultToUserContent({ type: "tool_result", tool_use_id: block.id, content })
if (images) cline.userMessageContent.push(...images)
}

try {
await handler.handle(cline, block, { askApproval, handleError, pushToolResult })

postWriteHook(filePath, intentId, sessionId, contributorModel, "SUCCESS")
} catch (err) {
postWriteHook(filePath, intentId, sessionId, contributorModel, "FAILED", err)
throw err
}
}

export async function presentAssistantMessage(cline: Task) {
if (cline.abort) {
throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
Expand Down Expand Up @@ -326,63 +387,81 @@ export async function presentAssistantMessage(cline: Task) {

const toolDescription = (): string => {
switch (block.name) {
case "execute_command":
return `[${block.name} for '${block.params.command}']`
case "read_file":
// Prefer native typed args when available; fall back to legacy params
// Check if nativeArgs exists (native protocol)
if (block.nativeArgs) {
return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
}
return readFileTool.getReadFileToolDescription(block.name, block.params)
case "write_to_file":
return `[${block.name} for '${block.params.path}']`
await runWithHooks(cline, block as ToolUse<"write_to_file">, writeToFileTool, {
checkpoint: true,
})
break
case "update_todo_list":
await runWithHooks(cline, block as ToolUse<"update_todo_list">, updateTodoListTool)
break
case "apply_diff":
// Native-only: tool args are structured (no XML payloads).
return block.params?.path ? `[${block.name} for '${block.params.path}']` : `[${block.name}]`
case "search_files":
return `[${block.name} for '${block.params.regex}'${
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
}]`
await runWithHooks(cline, block as ToolUse<"apply_diff">, applyDiffToolClass, {
checkpoint: true,
})
break
case "edit":
case "search_and_replace":
return `[${block.name} for '${block.params.file_path}']`
await runWithHooks(cline, block as ToolUse<"edit">, editTool, { checkpoint: true })
break
case "search_replace":
return `[${block.name} for '${block.params.file_path}']`
await runWithHooks(cline, block as ToolUse<"search_replace">, searchReplaceTool, {
checkpoint: true,
})
break
case "edit_file":
return `[${block.name} for '${block.params.file_path}']`
await runWithHooks(cline, block as ToolUse<"edit_file">, editFileTool, { checkpoint: true })
break
case "apply_patch":
return `[${block.name}]`
await runWithHooks(cline, block as ToolUse<"apply_patch">, applyPatchTool, { checkpoint: true })
break
case "read_file":
await runWithHooks(cline, block as ToolUse<"read_file">, readFileTool)
break
case "list_files":
return `[${block.name} for '${block.params.path}']`
await runWithHooks(cline, block as ToolUse<"list_files">, listFilesTool)
break
case "codebase_search":
await runWithHooks(cline, block as ToolUse<"codebase_search">, codebaseSearchTool)
break
case "search_files":
await runWithHooks(cline, block as ToolUse<"search_files">, searchFilesTool)
break
case "execute_command":
await runWithHooks(cline, block as ToolUse<"execute_command">, executeCommandTool)
break
case "read_command_output":
await runWithHooks(cline, block as ToolUse<"read_command_output">, readCommandOutputTool)
break
case "use_mcp_tool":
return `[${block.name} for '${block.params.server_name}']`
await runWithHooks(cline, block as ToolUse<"use_mcp_tool">, useMcpToolTool)
break
case "access_mcp_resource":
return `[${block.name} for '${block.params.server_name}']`
await runWithHooks(cline, block as ToolUse<"access_mcp_resource">, accessMcpResourceTool)
break
case "ask_followup_question":
return `[${block.name} for '${block.params.question}']`
case "attempt_completion":
return `[${block.name}]`
await runWithHooks(cline, block as ToolUse<"ask_followup_question">, askFollowupQuestionTool)
break
case "switch_mode":
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
case "codebase_search":
return `[${block.name} for '${block.params.query}']`
case "read_command_output":
return `[${block.name} for '${block.params.artifact_id}']`
case "update_todo_list":
return `[${block.name}]`
case "new_task": {
const mode = block.params.mode ?? defaultModeSlug
const message = block.params.message ?? "(no message)"
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
return `[${block.name} in ${modeName} mode: '${message}']`
}
await runWithHooks(cline, block as ToolUse<"switch_mode">, switchModeTool)
break
case "new_task":
await runWithHooks(cline, block as ToolUse<"new_task">, newTaskTool, { checkpoint: true })
break
case "attempt_completion":
await runWithHooks(cline, block as ToolUse<"attempt_completion">, attemptCompletionTool)
break
case "run_slash_command":
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
await runWithHooks(cline, block as ToolUse<"run_slash_command">, runSlashCommandTool)
break
case "skill":
return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
await runWithHooks(cline, block as ToolUse<"skill">, skillTool)
break
case "generate_image":
return `[${block.name} for '${block.params.path}']`
await runWithHooks(cline, block as ToolUse<"generate_image">, generateImageTool, {
checkpoint: true,
})
break
default:
return `[${block.name}]`
}
Expand Down Expand Up @@ -677,12 +756,7 @@ export async function presentAssistantMessage(cline: Task) {

switch (block.name) {
case "write_to_file":
await checkpointSaveAndMark(cline)
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
askApproval,
handleError,
pushToolResult,
})
await runWithHooks(cline, block as ToolUse<"write_to_file">, writeToFileTool, { checkpoint: true })
break
case "update_todo_list":
await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
Expand Down
4 changes: 2 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as path from "path"
import * as vscode from "vscode"
import os from "os"
import crypto from "crypto"
import { v7 as uuidv7 } from "uuid"
import EventEmitter from "events"

import * as crypto from "crypto"
import * as fs from "fs/promises"
import { AskIgnoredError } from "./AskIgnoredError"

import { Anthropic } from "@anthropic-ai/sdk"
Expand Down
Loading
Loading