mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-13 23:11:10 +00:00
hook system
This commit is contained in:
parent
7be2f94a27
commit
40fb5593f5
17 changed files with 1437 additions and 8 deletions
44
.orchestration/CLAUDE.md
Normal file
44
.orchestration/CLAUDE.md
Normal 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
27
.orchestration/README.md
Normal 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
|
||||
41
.orchestration/active_intents.yaml
Normal file
41
.orchestration/active_intents.yaml
Normal 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"
|
||||
33
.orchestration/intent_map.md
Normal file
33
.orchestration/intent_map.md
Normal 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
|
||||
|
|
@ -46,6 +46,7 @@ export const toolNames = [
|
|||
"skill",
|
||||
"generate_image",
|
||||
"custom_tool",
|
||||
"select_active_intent",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
||||
|
|
|
|||
49
src/core/prompts/tools/native-tools/select_active_intent.ts
Normal file
49
src/core/prompts/tools/native-tools/select_active_intent.ts
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
71
src/core/tools/SelectActiveIntentTool.ts
Normal file
71
src/core/tools/SelectActiveIntentTool.ts
Normal 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
361
src/hooks/HookEngine.ts
Normal 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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
73
src/hooks/index.ts
Normal 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
247
src/hooks/types.ts
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue