hook system

This commit is contained in:
Melaku 2026-02-18 09:11:21 +03:00
parent 7be2f94a27
commit 40fb5593f5
17 changed files with 1437 additions and 8 deletions

44
.orchestration/CLAUDE.md Normal file
View file

@ -0,0 +1,44 @@
# Shared Brain - Agent Collaboration
## Purpose
A persistent knowledge base shared across parallel sessions (Architect/Builder/Tester).
Contains "Lessons Learned" and project-specific stylistic rules.
## Project Rules
### Code Style
- Use TypeScript for all new code
- Follow existing code patterns in the codebase
- Add unit tests for new functionality
### Intent-Driven Development
1. Always select an intent before making changes: `select_active_intent`
2. Check the scope before editing files
3. Log all changes to the trace
### Parallel Workflow
- Architect agent defines the plan in intent_map.md
- Builder agent implements code
- Tester agent verifies against acceptance criteria
## Lessons Learned
### 2026-02-17
- Initial setup of Intent-Code Traceability system
- System enforces that agents must "checkout" an intent before writing code
- Scope validation prevents unauthorized file modifications
## Active Sessions
- Agent A (Architect): Monitoring intent_map.md
- Agent B (Builder): Working on INT-001
## Notes
- If you encounter a "Stale File" error, re-read the file before overwriting
- Use `select_active_intent` to load context before starting work

27
.orchestration/README.md Normal file
View file

@ -0,0 +1,27 @@
# Intent-Code Traceability Orchestration Directory
#
# This directory contains the state files for the Intent-Code Traceability system.
# It is managed by the Roo Code hook system and should not be manually edited
# unless you are initializing a new workspace.
# Files in this directory:
# - active_intents.yaml: Business intent specifications
# - agent_trace.jsonl: Append-only ledger of all code changes
# - intent_map.md: Spatial map of intents to files
# - CLAUDE.md: Shared brain for parallel agent sessions
# To initialize a new workspace:
# 1. Create this .orchestration directory
# 2. Create active_intents.yaml with your intent specifications
# 3. The system will automatically create other files as needed

View file

@ -0,0 +1,41 @@
# Active Intents Specification
#
# This file tracks the lifecycle of business requirements.
# Not all code changes are equal; this file tracks WHY we are working.
active_intents:
- id: "INT-001"
name: "Weather API Implementation"
status: "PENDING"
# Formal Scope Definition (Crucial for Parallelism)
owned_scope:
- "src/weather/**"
- "src/api/weather.ts"
constraints:
- "Must use OpenWeatherMap API"
- "Must support both current weather and forecast"
- "Must include error handling for API failures"
# The "Definition of Done"
acceptance_criteria:
- "Unit tests in tests/weather/ pass"
- "API endpoint returns valid JSON"
- "Rate limiting implemented"
created_at: "2026-02-17T00:00:00Z"
updated_at: "2026-02-17T00:00:00Z"
- id: "INT-002"
name: "User Authentication Refactor"
status: "PENDING"
owned_scope:
- "src/auth/**"
- "src/middleware/auth.ts"
constraints:
- "Must maintain backward compatibility with existing API"
- "Must support JWT and OAuth2"
- "Passwords must be hashed with bcrypt"
acceptance_criteria:
- "All auth tests pass"
- "No breaking changes to existing API contracts"
- "Security audit passes"
created_at: "2026-02-17T00:00:00Z"
updated_at: "2026-02-17T00:00:00Z"

View file

@ -0,0 +1,33 @@
# Intent Map
## Purpose
This file maps high-level business intents to physical files and AST nodes.
When a manager asks, "Where is the billing logic?", this file provides the answer.
## Intent to File Mapping
### INT-001: Weather API Implementation
- **Scope**: `src/weather/**`, `src/api/weather.ts`
- **Primary Files**:
- `src/api/weather.ts` - Main API endpoint
- `src/weather/client.ts` - OpenWeatherMap client
- `src/weather/types.ts` - Type definitions
- `src/weather/forecast.ts` - Forecast logic
### INT-002: User Authentication Refactor
- **Scope**: `src/auth/**`, `src/middleware/auth.ts`
- **Primary Files**:
- `src/auth/middleware.ts` - Auth middleware
- `src/auth/jwt.ts` - JWT handling
- `src/auth/oauth.ts` - OAuth2 implementation
- `src/auth/password.ts` - Password hashing
## Intent Evolution History
(Automatically updated when INTENT_EVOLUTION occurs)
- 2026-02-17: INT-001 created
- 2026-02-17: INT-002 created

View file

@ -46,6 +46,7 @@ export const toolNames = [
"skill",
"generate_image",
"custom_tool",
"select_active_intent",
] as const
export const toolNamesSchema = z.enum(toolNames)

View file

@ -1,12 +1,166 @@
# Architecture Notes Day 0
# Architecture Notes TRP1 Challenge Implementation
## What I achieved
## Phase 0: Archaeological Dig - Roo Code Architecture
- Successfully ran Roo Code in Extension Development Host
- Located main activation file
- Reloaded extension after a code change
### Overview
## Open Questions
Roo Code is an AI-native IDE extension for VS Code built on top of the Anthropic Messages API. It orchestrates AI agents to execute development tasks through a sophisticated tool execution system.
- Where tool execution happens
- Where system prompt is built
### Key Components
#### 1. Entry Point
- **`src/extension.ts`**: Main extension activation file
- Initializes VS Code extension context
- Registers commands and providers
- Loads environment variables from `.env`
#### 2. Task Orchestration (The Core Loop)
- **`src/core/task/Task.ts`**: Main task orchestration class (~17,700+ lines)
- Manages the conversation loop with the LLM
- Handles message streaming and tool execution scheduling
- Maintains conversation history
- Controls task state (paused, aborted, etc.)
#### 3. Tool Execution Pipeline
- **`src/core/assistant-message/presentAssistantMessage.ts`**: Tool execution entry point
- Uses a switch statement on `block.name` to route to specific tools
- Key execution points:
- `write_to_file``WriteToFileTool.handle()`
- `execute_command``ExecuteCommandTool.handle()`
- `read_file``ReadFileTool.handle()`
- etc.
#### 4. Tool Definitions
- **`src/core/tools/BaseTool.ts`**: Abstract base class for all tools
- Defines `execute()` method that tools implement
- Handles parameter parsing from `nativeArgs`
- Provides `handlePartial()` for streaming responses
- **Individual tools** in `src/core/tools/`:
- `WriteToFileTool.ts` - File writing with diff view
- `ExecuteCommandTool.ts` - Shell command execution
- `ReadFileTool.ts` - File reading
- `EditTool.ts`, `SearchReplaceTool.ts` - File editing
- `ApplyPatchTool.ts`, `ApplyDiffTool.ts` - Patch application
#### 5. System Prompt Generation
- **`src/core/prompts/system.ts`**: Builds the SYSTEM_PROMPT
- Called with `vscode.ExtensionContext` and other parameters
- Includes mode-specific instructions
- Provides tool definitions and capabilities
### Hook System Architecture
#### Injection Points
1. **Pre-Hook (Before tool execution)**:
- In `presentAssistantMessage.ts`, before each `tool.handle()` call
- Can intercept, validate, and modify tool parameters
- Can block execution and return error results
2. **Post-Hook (After tool execution)**:
- After successful tool execution
- For logging traces and updating documentation
#### Intent-Driven Workflow
1. **State 1 - Request**: User prompts the agent
2. **State 2 - Reasoning Intercept**: Agent must call `select_active_intent` tool
- Pre-Hook intercepts this call
- Loads intent context from `.orchestration/active_intents.yaml`
- Injects context into the prompt
3. **State 3 - Contextualized Action**: Agent executes tools with full context
- Pre-Hook validates scope before write operations
- Post-Hook logs traces to `.orchestration/agent_trace.jsonl`
### Data Model Files (`.orchestration/`)
#### 1. `active_intents.yaml`
Tracks business requirements and their lifecycle:
```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"
- "Must maintain backward compatibility"
acceptance_criteria:
- "Unit tests pass"
```
#### 2. `agent_trace.jsonl`
Append-only ledger linking Intent → Code Hash:
```json
{
"id": "uuid-v4",
"timestamp": "2026-02-16T12:00:00Z",
"vcs": { "revision_id": "git_sha_hash" },
"files": [
{
"relative_path": "src/auth/middleware.ts",
"conversations": [
{
"url": "session_log_id",
"contributor": {
"entity_type": "AI",
"model_identifier": "claude-3-5-sonnet"
},
"ranges": [
{
"start_line": 15,
"end_line": 45,
"content_hash": "sha256:a8f5f167f44f4964e6c998dee827110c"
}
],
"related": [{ "type": "specification", "value": "REQ-001" }]
}
]
}
]
}
```
#### 3. `intent_map.md`
Spatial map of business intents to files/AST nodes.
#### 4. `CLAUDE.md` (or `AGENT.md`)
Shared brain for parallel agent sessions.
### Hook Implementation Strategy
1. **Hook Engine**: `src/hooks/HookEngine.ts`
- Central middleware that wraps tool execution
- Provides Pre-Hook and Post-Hook capabilities
- Manages intent state across the session
2. **Intent Validator**: `src/hooks/IntentValidator.ts`
- Validates intent IDs and scope
- Blocks unauthorized operations
3. **Trace Logger**: `src/hooks/TraceLogger.ts`
- Computes content hashes
- Appends to `agent_trace.jsonl`
4. **New Tool**: `select_active_intent`
- Forces the reasoning loop
- Returns intent context to the agent

View file

@ -37,6 +37,7 @@ 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"
@ -849,6 +850,13 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "select_active_intent":
await selectActiveIntentTool.handle(cline, block as ToolUse<"select_active_intent">, {
askApproval,
handleError,
pushToolResult,
})
break
default: {
// Handle unknown/invalid tool names OR custom tools
// This is critical for native tool calling where every tool_use MUST have a tool_result

View file

@ -20,6 +20,7 @@ import searchFiles from "./search_files"
import switchMode from "./switch_mode"
import updateTodoList from "./update_todo_list"
import writeToFile from "./write_to_file"
import selectActiveIntent from "./select_active_intent"
export { getMcpServerTools } from "./mcp_server"
export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters"
@ -68,6 +69,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
switchMode,
updateTodoList,
writeToFile,
selectActiveIntent,
] satisfies OpenAI.Chat.ChatCompletionTool[]
}

View file

@ -0,0 +1,49 @@
import type OpenAI from "openai"
const SELECT_ACTIVE_INTENT_DESCRIPTION = `Select an active intent before performing any destructive operations like writing files, editing code, or executing commands. This is a MANDATORY step that must be completed before any code modifications.
The Intent-Code Traceability system requires you to "checkout" an intent just like you would check out a branch in version control. This ensures:
- You have the correct context for the task
- Your changes are properly tracked and attributed
- Scope violations are prevented
How to Use:
1. First, analyze the user's request to understand what they're asking for
2. Identify the appropriate intent ID from .orchestration/active_intents.yaml
3. Call this tool with the intent_id to load the context
4. Only AFTER selecting an intent, proceed with your task
When to Use:
- Before ANY write_to_file, edit, search_and_replace, or similar file-modifying operations
- Before execute_command if it will modify the codebase
- When starting any new task or subtask
When NOT to Use:
- For read-only operations (read_file, list_files, search_files are safe)
- After you have already selected an intent and are continuing within the same intent
Example: { "intent_id": "INT-001" }
Note: If no active_intents.yaml exists, you must first create the .orchestration directory and define your intents.`
const INTENT_ID_PARAMETER_DESCRIPTION = `The intent ID from .orchestration/active_intents.yaml (e.g., "INT-001", "INT-002")`
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

@ -479,6 +479,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.instanceId = crypto.randomUUID().slice(0, 8)
this.taskNumber = -1
// Initialize the Hook Engine for Intent-Code Traceability
try {
const { initializeHookEngine } = require("../../hooks/index")
initializeHookEngine(this.cwd, this.taskId, this.instanceId)
} catch (error) {
console.warn("[Task] Failed to initialize Hook Engine:", error)
}
this.rooIgnoreController = new RooIgnoreController(this.cwd)
this.rooProtectedController = new RooProtectedController(this.cwd)
this.fileContextTracker = new FileContextTracker(provider, this.taskId)

View file

@ -0,0 +1,71 @@
/**
* Select Active Intent Tool
*
* This tool allows the agent to "checkout" an intent before performing any
* destructive operations. This enforces the Reasoning Loop pattern.
*/
import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
import { getHookEngine } from "../../hooks/HookEngine"
import { validateIntentId, formatIntentForDisplay } from "../../hooks/IntentValidator"
import { BaseTool, ToolCallbacks } from "./BaseTool"
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<void> {
const { pushToolResult } = callbacks
const intentId = params.intent_id
if (!intentId) {
task.consecutiveMistakeCount++
task.recordToolError("select_active_intent")
pushToolResult(await task.sayAndCreateMissingParamError("select_active_intent", "intent_id"))
return
}
// Validate intent ID
const validation = await validateIntentId(task.cwd, intentId)
if (!validation.valid) {
task.consecutiveMistakeCount++
task.recordToolError("select_active_intent")
pushToolResult(formatResponse.toolError(validation.error || "Invalid intent"))
return
}
// Set the active intent in the hook engine
const hookEngine = getHookEngine()
const result = await hookEngine.setActiveIntent(intentId)
if (!result.allowed) {
task.consecutiveMistakeCount++
task.recordToolError("select_active_intent")
pushToolResult(formatResponse.toolError(result.errorMessage || "Failed to set active intent"))
return
}
task.consecutiveMistakeCount = 0
// Format the intent context for display
const intentDisplay = formatIntentForDisplay(validation.intent!)
const successMessage = `
## Intent Selected Successfully
${intentDisplay}
You now have context to work within this intent's scope. You may proceed with your task.
When making file modifications, ensure they stay within the owned scope listed above.
`.trim()
pushToolResult(successMessage)
}
}
export const selectActiveIntentTool = new SelectActiveIntentTool()

361
src/hooks/HookEngine.ts Normal file
View file

@ -0,0 +1,361 @@
/**
* Hook Engine - Middleware for Intent-Code Traceability
*
* This is the central middleware that intercepts all tool executions to:
* 1. Enforce intent context injection (Pre-Hook)
* 2. Validate scope and authorization
* 3. Log traces and update documentation (Post-Hook)
*/
import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs"
import { v4 as uuidv4 } from "uuid"
import {
type HookContext,
type PreHookResult,
type PostHookResult,
type ActiveIntent,
type AgentTraceEntry,
type MutationClass,
classifyTool,
computeContentHash,
getOrchestrationDir,
ensureOrchestrationDir,
loadActiveIntents,
saveActiveIntents,
getIntentById,
isFileInScope,
getGitRevision,
} from "./types"
/**
* Session state for tracking active intent across the conversation
*/
interface SessionState {
activeIntentId: string | null
taskId: string
instanceId: string
startedAt: string
}
/**
* The Hook Engine - main middleware for intercepting tool executions
*/
export class HookEngine {
private static instance: HookEngine | null = null
private sessionState: SessionState | null = null
private workspacePath: string = ""
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): HookEngine {
if (!HookEngine.instance) {
HookEngine.instance = new HookEngine()
}
return HookEngine.instance
}
/**
* Initialize the hook engine with workspace context
*/
initialize(workspacePath: string, taskId: string, instanceId: string): void {
this.workspacePath = workspacePath
this.sessionState = {
activeIntentId: null,
taskId,
instanceId,
startedAt: new Date().toISOString(),
}
console.log(`[HookEngine] Initialized for workspace: ${workspacePath}`)
}
/**
* Get the current active intent ID
*/
getActiveIntentId(): string | null {
return this.sessionState?.activeIntentId || null
}
/**
* Set the active intent (called when agent selects an intent)
*/
async setActiveIntent(intentId: string): Promise<PreHookResult> {
if (!this.sessionState) {
return {
allowed: false,
errorMessage: "HookEngine not initialized",
}
}
const intentsData = await loadActiveIntents(this.workspacePath)
if (!intentsData) {
return {
allowed: false,
errorMessage: "No active_intents.yaml found. Please initialize the orchestration directory.",
}
}
const intent = getIntentById(intentsData, intentId)
if (!intent) {
return {
allowed: false,
errorMessage: `Intent ID '${intentId}' not found in active_intents.yaml`,
}
}
// Update session state
this.sessionState.activeIntentId = intentId
// Update intent status to IN_PROGRESS
intent.status = "IN_PROGRESS"
intent.updated_at = new Date().toISOString()
await saveActiveIntents(this.workspacePath, intentsData)
// Generate context for injection
const injectedContext = this.generateIntentContext(intent)
console.log(`[HookEngine] Active intent set to: ${intentId}`)
return {
allowed: true,
injectedContext,
}
}
/**
* Generate the intent context XML block for injection
*/
private generateIntentContext(intent: ActiveIntent): string {
const constraints = intent.constraints.map((c) => ` - ${c}`).join("\n")
const scope = intent.owned_scope.map((s) => ` - ${s}`).join("\n")
const acceptance = intent.acceptance_criteria.map((a) => ` - ${a}`).join("\n")
return `
<intent_context id="${intent.id}" name="${intent.name}" status="${intent.status}">
<owned_scope>
${scope}
</owned_scope>
<constraints>
${constraints}
</constraints>
<acceptance_criteria>
${acceptance}
</acceptance_criteria>
</intent_context>
`.trim()
}
/**
* Pre-Hook: Called before tool execution
*/
async preHook(context: HookContext): Promise<PreHookResult> {
const { toolName, toolParams, cwd } = context
// If no active intent is set, block destructive tools
if (!this.sessionState?.activeIntentId) {
const classification = classifyTool(toolName)
if (classification === "DESTRUCTIVE") {
return {
allowed: false,
errorMessage: `Scope Violation: No active intent selected. You must call 'select_active_intent' before performing destructive operations like '${toolName}'.`,
}
}
// Allow safe tools without intent
return { allowed: true }
}
// Check scope for write operations
if (
toolName === "write_to_file" ||
toolName === "edit" ||
toolName === "search_and_replace" ||
toolName === "edit_file"
) {
const filePath = (toolParams.path as string) || (toolParams.file_path as string)
if (filePath) {
const intentsData = await loadActiveIntents(cwd)
if (intentsData) {
const intent = getIntentById(intentsData, this.sessionState.activeIntentId)
if (intent) {
const isInScope = isFileInScope(filePath, intent.owned_scope)
if (!isInScope) {
return {
allowed: false,
errorMessage: `Scope Violation: Intent ${this.sessionState.activeIntentId} is not authorized to edit '${filePath}'. Authorized scope: ${intent.owned_scope.join(", ")}`,
}
}
}
}
}
}
return { allowed: true }
}
/**
* Post-Hook: Called after successful tool execution
*/
async postHook(
context: HookContext,
toolResult: string,
mutationClass: MutationClass = "UNKNOWN",
): Promise<PostHookResult> {
const { toolName, toolParams, cwd } = context
const classification = classifyTool(toolName)
// Only trace destructive/modifying operations
if (classification !== "DESTRUCTIVE") {
return { success: true }
}
// Get the file path from tool params
const filePath = (toolParams.path as string) || (toolParams.file_path as string) || (toolParams.file as string)
if (!filePath || !this.sessionState?.activeIntentId) {
return { success: true }
}
try {
// Ensure orchestration directory exists
await ensureOrchestrationDir(cwd)
// Read the current file content to compute hash
const fullPath = path.resolve(cwd, filePath)
let content = ""
let startLine = 1
let endLine = 1
if (fs.existsSync(fullPath)) {
content = fs.readFileSync(fullPath, "utf-8")
const lines = content.split("\n")
endLine = lines.length
// For new files, startLine would be 1
// For edits, we'd need the actual range - for now use entire file
}
// Compute content hash
const contentHash = computeContentHash(content)
// Get git revision
const gitRevision = getGitRevision(cwd)
// Create trace entry
const traceEntry: AgentTraceEntry = {
id: uuidv4(),
timestamp: new Date().toISOString(),
vcs: {
revision_id: gitRevision,
},
files: [
{
relative_path: filePath,
conversations: [
{
url: this.sessionState.taskId,
contributor: {
entity_type: "AI",
model_identifier: "claude-3-5-sonnet", // Would get from actual model
},
ranges: [
{
start_line: startLine,
end_line: endLine,
content_hash: contentHash,
},
],
related: [
{
type: "intent",
value: this.sessionState.activeIntentId,
},
],
},
],
},
],
}
// Append to trace file
const tracePath = path.join(getOrchestrationDir(cwd), "agent_trace.jsonl")
const traceLine = JSON.stringify(traceEntry) + "\n"
fs.appendFileSync(tracePath, traceLine, "utf-8")
console.log(
`[HookEngine] Traced ${toolName} on ${filePath} with intent ${this.sessionState.activeIntentId}`,
)
return {
success: true,
traceEntry,
}
} catch (error) {
console.error("[HookEngine] Post-hook error:", error)
return {
success: false,
errorMessage: error instanceof Error ? error.message : String(error),
}
}
}
/**
* Check if a file has been modified since the agent started
* Used for optimistic locking in parallel orchestration
*/
async checkFileConcurrency(
filePath: string,
originalHash: string,
): Promise<{ stale: boolean; currentHash: string }> {
const fullPath = path.resolve(this.workspacePath, filePath)
if (!fs.existsSync(fullPath)) {
return { stale: false, currentHash: "" }
}
const content = fs.readFileSync(fullPath, "utf-8")
const currentHash = computeContentHash(content)
return {
stale: currentHash !== originalHash,
currentHash,
}
}
/**
* Update intent status (for completion or blocking)
*/
async updateIntentStatus(intentId: string, status: "COMPLETED" | "BLOCKED"): Promise<void> {
const intentsData = await loadActiveIntents(this.workspacePath)
if (!intentsData) return
const intent = getIntentById(intentsData, intentId)
if (intent) {
intent.status = status
intent.updated_at = new Date().toISOString()
await saveActiveIntents(this.workspacePath, intentsData)
}
if (this.sessionState?.activeIntentId === intentId) {
this.sessionState.activeIntentId = null
}
}
/**
* Clear session state
*/
reset(): void {
this.sessionState = null
this.workspacePath = ""
}
}
/**
* Convenience function to get the HookEngine instance
*/
export function getHookEngine(): HookEngine {
return HookEngine.getInstance()
}

View file

@ -0,0 +1,103 @@
/**
* Intent Validator
*
* Validates intent IDs and scope before allowing tool execution.
* Part of the Pre-Hook security boundary.
*/
import { loadActiveIntents, getIntentById, isFileInScope, type ActiveIntent, type ActiveIntentsData } from "./types"
/**
* Validate an intent ID exists and is active
*/
export async function validateIntentId(
workspacePath: string,
intentId: string,
): Promise<{ valid: boolean; intent?: ActiveIntent; error?: string }> {
const intentsData = await loadActiveIntents(workspacePath)
if (!intentsData) {
return {
valid: false,
error: "No active_intents.yaml found. Please initialize the orchestration directory.",
}
}
const intent = getIntentById(intentsData, intentId)
if (!intent) {
const availableIds = intentsData.active_intents.map((i) => i.id).join(", ")
return {
valid: false,
error: `Intent ID '${intentId}' not found. Available intents: ${availableIds || "none"}`,
}
}
if (intent.status === "COMPLETED") {
return {
valid: false,
error: `Intent '${intentId}' has already been completed. Please select a different intent.`,
}
}
if (intent.status === "BLOCKED") {
return {
valid: false,
error: `Intent '${intentId}' is blocked. Please resolve the blocking issue or select a different intent.`,
}
}
return { valid: true, intent }
}
/**
* Validate that a file is within the intent's owned scope
*/
export function validateFileScope(filePath: string, intent: ActiveIntent): { valid: boolean; error?: string } {
const isInScope = isFileInScope(filePath, intent.owned_scope)
if (!isInScope) {
return {
valid: false,
error: `Scope Violation: Intent ${intent.id} is not authorized to edit '${filePath}'. Authorized scope: ${intent.owned_scope.join(", ")}. Request scope expansion in the intent specification.`,
}
}
return { valid: true }
}
/**
* Get all available intents for display
*/
export async function getAvailableIntents(workspacePath: string): Promise<ActiveIntent[]> {
const intentsData = await loadActiveIntents(workspacePath)
if (!intentsData) return []
return intentsData.active_intents.filter((intent) => intent.status === "PENDING" || intent.status === "IN_PROGRESS")
}
/**
* Format intent for display to the AI agent
*/
export function formatIntentForDisplay(intent: ActiveIntent): string {
const scope =
intent.owned_scope.length > 0
? `\n Owned Scope:\n${intent.owned_scope.map((s) => ` - ${s}`).join("\n")}`
: ""
const constraints =
intent.constraints.length > 0
? `\n Constraints:\n${intent.constraints.map((c) => ` - ${c}`).join("\n")}`
: ""
const acceptance =
intent.acceptance_criteria.length > 0
? `\n Acceptance Criteria:\n${intent.acceptance_criteria.map((a) => ` - ${a}`).join("\n")}`
: ""
return `
## Intent: ${intent.name} (${intent.id})
Status: ${intent.status}
${scope}${constraints}${acceptance}
`.trim()
}

View file

@ -0,0 +1,204 @@
/**
* Trace Logger
*
* Handles logging of agent actions to the append-only ledger (agent_trace.jsonl).
* Part of the Post-Hook system for AI-Native Git Layer.
*/
import * as path from "path"
import * as fs from "fs"
import { v4 as uuidv4 } from "uuid"
import {
type AgentTraceEntry,
type TraceFileEntry,
type TraceConversation,
type MutationClass,
computeContentHash,
getOrchestrationDir,
getGitRevision,
} from "./types"
export interface LogTraceParams {
workspacePath: string
taskId: string
instanceId: string
intentId: string
filePath: string
content: string
startLine: number
endLine: number
modelIdentifier?: string
mutationClass: MutationClass
}
/**
* Log a trace entry to the append-only ledger
*/
export async function logTrace(params: LogTraceParams): Promise<AgentTraceEntry> {
const {
workspacePath,
taskId,
intentId,
filePath,
content,
startLine,
endLine,
modelIdentifier = "claude-3-5-sonnet",
} = params
// Ensure orchestration directory exists
const orchDir = getOrchestrationDir(workspacePath)
if (!fs.existsSync(orchDir)) {
fs.mkdirSync(orchDir, { recursive: true })
}
// Compute content hash
const contentHash = computeContentHash(content)
// Get git revision
const gitRevision = getGitRevision(workspacePath)
// Create trace entry
const traceEntry: AgentTraceEntry = {
id: uuidv4(),
timestamp: new Date().toISOString(),
vcs: {
revision_id: gitRevision,
},
files: [
{
relative_path: filePath,
conversations: [
{
url: taskId,
contributor: {
entity_type: "AI",
model_identifier: modelIdentifier,
},
ranges: [
{
start_line: startLine,
end_line: endLine,
content_hash: contentHash,
},
],
related: [
{
type: "intent",
value: intentId,
},
],
},
],
},
],
}
// Append to trace file (JSONL format)
const tracePath = path.join(orchDir, "agent_trace.jsonl")
const traceLine = JSON.stringify(traceEntry) + "\n"
fs.appendFileSync(tracePath, traceLine, "utf-8")
console.log(`[TraceLogger] Logged trace for ${filePath} with intent ${intentId}`)
return traceEntry
}
/**
* Classify the mutation based on context
* This is a simplified version - in production would use AST analysis
*/
export function classifyMutation(originalContent: string, newContent: string, intent?: string): MutationClass {
// If the content is very similar, it's likely a refactor
const similarity = calculateSimilarity(originalContent, newContent)
if (similarity > 0.8) {
return "AST_REFACTOR"
}
// If significantly different, it's likely a new feature/evolution
if (similarity < 0.5) {
return "INTENT_EVOLUTION"
}
return "UNKNOWN"
}
/**
* Calculate simple similarity between two strings
*/
function calculateSimilarity(str1: string, str2: string): number {
if (str1 === str2) return 1
if (!str1 || !str2) return 0
const longer = str1.length > str2.length ? str1 : str2
const shorter = str1.length > str2.length ? str2 : str1
if (longer.length === 0) return 1
const editDistance = levenshteinDistance(longer, shorter)
return (longer.length - editDistance) / longer.length
}
/**
* Calculate Levenshtein distance between two strings
*/
function levenshteinDistance(str1: string, str2: string): number {
const matrix: number[][] = []
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i]
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1]
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)
}
}
}
return matrix[str2.length][str1.length]
}
/**
* Read the trace history for a specific intent
*/
export async function getTraceHistoryForIntent(workspacePath: string, intentId: string): Promise<AgentTraceEntry[]> {
const tracePath = path.join(getOrchestrationDir(workspacePath), "agent_trace.jsonl")
const entries: AgentTraceEntry[] = []
if (!fs.existsSync(tracePath)) {
return entries
}
const content = fs.readFileSync(tracePath, "utf-8")
const lines = content.split("\n").filter((line) => line.trim())
for (const line of lines) {
try {
const entry = JSON.parse(line) as AgentTraceEntry
// Check if this entry is related to the intent
for (const file of entry.files) {
for (const conv of file.conversations) {
for (const related of conv.related) {
if (related.value === intentId) {
entries.push(entry)
break
}
}
}
}
} catch {
// Skip malformed lines
}
}
return entries
}

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

@ -0,0 +1,73 @@
/**
* Hook System Integration
*
* This module provides functions to integrate the hook system
* with the tool execution pipeline.
*/
import { getHookEngine } from "./HookEngine"
import { logTrace, classifyMutation } from "./TraceLogger"
import type { HookContext, MutationClass } from "./types"
/**
* Execute a tool with Pre-Hook and Post-Hook
*
* @param context - The hook context
* @param executeTool - The actual tool execution function
* @param toolResult - The result of the tool execution (for Post-Hook)
* @param mutationClass - Classification of the mutation
* @returns The result of the tool execution, or an error if blocked
*/
export async function executeWithHooks(
context: HookContext,
executeTool: () => Promise<void>,
toolResult: string,
mutationClass: MutationClass = "UNKNOWN",
): Promise<{ success: boolean; error?: string }> {
const hookEngine = getHookEngine()
// Pre-Hook: Validate and potentially modify the execution
const preResult = await hookEngine.preHook(context)
if (!preResult.allowed) {
return {
success: false,
error: preResult.errorMessage || "Execution blocked by Pre-Hook",
}
}
// Execute the tool
try {
await executeTool()
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
}
}
// Post-Hook: Log the trace
const postResult = await hookEngine.postHook(context, toolResult, mutationClass)
if (!postResult.success) {
console.warn("[HookSystem] Post-Hook warning:", postResult.errorMessage)
}
return { success: true }
}
/**
* Initialize the hook engine for a new task
*/
export function initializeHookEngine(workspacePath: string, taskId: string, instanceId: string): void {
const hookEngine = getHookEngine()
hookEngine.initialize(workspacePath, taskId, instanceId)
}
/**
* Reset the hook engine
*/
export function resetHookEngine(): void {
const hookEngine = getHookEngine()
hookEngine.reset()
}

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

@ -0,0 +1,247 @@
/**
* Intent-Code Traceability Hook System
*
* This module provides the core types and interfaces for the hook system
* that intercepts tool executions to enforce intent context and trace code changes.
*/
import * as vscode from "vscode"
import * as path from "path"
import * as crypto from "crypto"
import * as fs from "fs"
import * as yaml from "yaml"
/**
* Represents an active intent in the system
*/
export interface ActiveIntent {
id: string
name: string
status: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "BLOCKED"
owned_scope: string[]
constraints: string[]
acceptance_criteria: string[]
created_at: string
updated_at: string
}
/**
* The active intents data model
*/
export interface ActiveIntentsData {
active_intents: ActiveIntent[]
}
/**
* Represents a single file modification in the trace
*/
export interface TraceFileEntry {
relative_path: string
conversations: TraceConversation[]
}
/**
* Represents a conversation/contribution to a file
*/
export interface TraceConversation {
url: string // session_log_id
contributor: {
entity_type: "AI" | "HUMAN"
model_identifier?: string
}
ranges: TraceRange[]
related: TraceRelated[]
}
/**
* A range of lines with content hash for spatial independence
*/
export interface TraceRange {
start_line: number
end_line: number
content_hash: string
}
/**
* Related specifications/intents
*/
export interface TraceRelated {
type: "specification" | "intent" | "constraint"
value: string
}
/**
* A single trace entry in the ledger
*/
export interface AgentTraceEntry {
id: string
timestamp: string
vcs: {
revision_id: string
}
files: TraceFileEntry[]
}
/**
* Mutation classification for distinguishing refactors from features
*/
export type MutationClass = "AST_REFACTOR" | "INTENT_EVOLUTION" | "DOCUMENTATION" | "UNKNOWN"
/**
* Hook execution context
*/
export interface HookContext {
taskId: string
instanceId: string
cwd: string
activeIntentId: string | null
toolName: string
toolParams: Record<string, unknown>
}
/**
* Result of a Pre-Hook check
*/
export interface PreHookResult {
allowed: boolean
errorMessage?: string
modifiedParams?: Record<string, unknown>
injectedContext?: string
}
/**
* Result of a Post-Hook operation
*/
export interface PostHookResult {
success: boolean
traceEntry?: AgentTraceEntry
errorMessage?: string
}
/**
* Tool classification
*/
export type ToolClassification = "SAFE" | "DESTRUCTIVE" | "UNKNOWN"
/**
* Classification of tools based on their potential impact
*/
export function classifyTool(toolName: string): ToolClassification {
const safeTools = ["read_file", "list_files", "search_files", "codebase_search", "read_command_output"]
const destructiveTools = [
"write_to_file",
"edit",
"search_and_replace",
"search_replace",
"edit_file",
"apply_patch",
"apply_diff",
"execute_command",
"delete_file",
]
if (safeTools.includes(toolName)) return "SAFE"
if (destructiveTools.includes(toolName)) return "DESTRUCTIVE"
return "UNKNOWN"
}
/**
* Compute SHA-256 hash of content for spatial independence
*/
export function computeContentHash(content: string): string {
const hash = crypto.createHash("sha256")
hash.update(content)
return `sha256:${hash.digest("hex")}`
}
/**
* Get the workspace orchestration directory path
*/
export function getOrchestrationDir(workspacePath: string): string {
return path.join(workspacePath, ".orchestration")
}
/**
* Ensure the orchestration directory exists
*/
export async function ensureOrchestrationDir(workspacePath: string): Promise<string> {
const dir = getOrchestrationDir(workspacePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
return dir
}
/**
* Load active intents from YAML file
*/
export async function loadActiveIntents(workspacePath: string): Promise<ActiveIntentsData | null> {
const filePath = path.join(getOrchestrationDir(workspacePath), "active_intents.yaml")
try {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, "utf-8")
return yaml.parse(content) as ActiveIntentsData
}
} catch (error) {
console.error("[HookSystem] Failed to load active_intents.yaml:", error)
}
return null
}
/**
* Save active intents to YAML file
*/
export async function saveActiveIntents(workspacePath: string, data: ActiveIntentsData): Promise<void> {
const dir = await ensureOrchestrationDir(workspacePath)
const filePath = path.join(dir, "active_intents.yaml")
const content = yaml.stringify(data, { indent: 2 })
fs.writeFileSync(filePath, content, "utf-8")
}
/**
* Get a specific intent by ID
*/
export function getIntentById(data: ActiveIntentsData, intentId: string): ActiveIntent | null {
return data.active_intents.find((intent) => intent.id === intentId) || null
}
/**
* Check if a file path matches the intent's owned scope
*/
export function isFileInScope(filePath: string, scopePatterns: string[]): boolean {
// Simple glob matching - can be enhanced with proper glob library
for (const pattern of scopePatterns) {
// Convert glob pattern to regex
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".")
const regex = new RegExp(`^${regexPattern}$`)
if (regex.test(filePath)) {
return true
}
}
return false
}
/**
* Get current git revision ID
*/
export function getGitRevision(workspacePath: string): string {
try {
// This is a simplified version - in production would use simple-git
const headPath = path.join(workspacePath, ".git", "HEAD")
if (fs.existsSync(headPath)) {
const headContent = fs.readFileSync(headPath, "utf-8").trim()
if (headContent.startsWith("ref: ")) {
const refPath = path.join(workspacePath, ".git", headContent.slice(5))
if (fs.existsSync(refPath)) {
return fs.readFileSync(refPath, "utf-8").trim().slice(0, 7)
}
}
return headContent.slice(0, 7)
}
} catch (error) {
console.error("[HookSystem] Failed to get git revision:", error)
}
return "unknown"
}

View file

@ -115,6 +115,7 @@ export type NativeToolArgs = {
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 }
select_active_intent: { intent_id: string }
// Add more tools as they are migrated to native protocol
}
@ -289,6 +290,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
skill: "load skill",
generate_image: "generate images",
custom_tool: "use custom tools",
select_active_intent: "select active intent",
} as const
// Define available tool groups.
@ -321,6 +323,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"update_todo_list",
"run_slash_command",
"skill",
"select_active_intent",
] as const
/**