Add hook system files

This commit is contained in:
Melaku 2026-02-21 18:13:29 +03:00
parent d595eac068
commit a8b2def5cb
5 changed files with 616 additions and 0 deletions

View file

@ -0,0 +1,3 @@
{"timestamp":"2026-02-21T07:00:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"write_to_file","filePath":"src/app.ts","mutationClass":"INTENT_EVOLUTION","originalContentHash":"sha256:abc123","newContentHash":"sha256:def456","toolResult":"File written successfully","modelId":"claude-3-5-sonnet-20241022"}
{"timestamp":"2026-02-21T07:05:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"edit_file","filePath":"src/utils.ts","mutationClass":"AST_REFACTOR","originalContentHash":"sha256:xyz789","newContentHash":"sha256:uvw012","toolResult":"Edit applied successfully","modelId":"claude-3-5-sonnet-20241022"}
{"timestamp":"2026-02-21T07:10:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"write_to_file","filePath":"src/components/Button.tsx","mutationClass":"INTENT_EVOLUTION","originalContentHash":"sha256:new123","newContentHash":"sha256:new456","toolResult":"File written successfully","modelId":"claude-3-5-sonnet-20241022"}

View file

@ -0,0 +1,252 @@
/**
* TraceLogger Tests
*
* Tests for the trace logging functionality
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest"
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
import { logTrace, getTraceHistoryForIntent, classifyMutation } from "../../hooks/TraceLogger"
import { type AgentTraceEntry, ensureOrchestrationDir, getOrchestrationDir } from "../../hooks/types"
// Test fixtures
describe("TraceLogger", () => {
let tempDir: string
beforeEach(async () => {
// Create a temporary directory for each test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "trace-logger-test-"))
await ensureOrchestrationDir(tempDir)
})
afterEach(() => {
// Clean up temporary directory
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true })
}
})
describe("logTrace", () => {
it("should create trace file with entry", async () => {
const result = await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/test.ts",
content: "console.log('hello')",
startLine: 1,
endLine: 1,
mutationClass: "INTENT_EVOLUTION",
})
expect(result).toBeDefined()
expect(result.id).toBeDefined()
expect(result.timestamp).toBeDefined()
expect(result.files[0].relative_path).toBe("src/test.ts")
expect(result.files[0].conversations[0].related[0].value).toBe("intent-1")
})
it("should append to existing trace file", async () => {
// First trace
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/file1.ts",
content: "content 1",
startLine: 1,
endLine: 10,
mutationClass: "INTENT_EVOLUTION",
})
// Second trace
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/file2.ts",
content: "content 2",
startLine: 1,
endLine: 20,
mutationClass: "AST_REFACTOR",
})
// Verify file has both entries
const tracePath = path.join(getOrchestrationDir(tempDir), "agent_trace.jsonl")
const content = fs.readFileSync(tracePath, "utf-8")
const lines = content.split("\n").filter((line) => line.trim())
expect(lines.length).toBe(2)
})
it("should include model identifier when provided", async () => {
const result = await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/test.ts",
content: "content",
startLine: 1,
endLine: 1,
modelIdentifier: "claude-4-opus",
mutationClass: "DOCUMENTATION",
})
expect(result.files[0].conversations[0].contributor.model_identifier).toBe("claude-4-opus")
})
it("should use default model identifier when not provided", async () => {
const result = await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/test.ts",
content: "content",
startLine: 1,
endLine: 1,
mutationClass: "UNKNOWN",
})
expect(result.files[0].conversations[0].contributor.model_identifier).toBe("claude-3-5-sonnet")
})
it("should compute content hash", async () => {
const result = await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/test.ts",
content: "test content",
startLine: 1,
endLine: 1,
mutationClass: "INTENT_EVOLUTION",
})
expect(result.files[0].conversations[0].ranges[0].content_hash).toContain("sha256:")
})
})
describe("getTraceHistoryForIntent", () => {
it("should return empty array when no trace file exists", async () => {
const result = await getTraceHistoryForIntent(tempDir, "intent-1")
expect(result).toEqual([])
})
it("should return trace entries for specific intent", async () => {
// Create traces for different intents
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/file1.ts",
content: "content 1",
startLine: 1,
endLine: 10,
mutationClass: "INTENT_EVOLUTION",
})
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-2",
filePath: "src/file2.ts",
content: "content 2",
startLine: 1,
endLine: 20,
mutationClass: "AST_REFACTOR",
})
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/file3.ts",
content: "content 3",
startLine: 1,
endLine: 30,
mutationClass: "DOCUMENTATION",
})
// Get traces for intent-1
const result = await getTraceHistoryForIntent(tempDir, "intent-1")
expect(result.length).toBe(2)
expect(result[0].files[0].relative_path).toBe("src/file1.ts")
expect(result[1].files[0].relative_path).toBe("src/file3.ts")
})
it("should return empty array for non-existent intent", async () => {
await logTrace({
workspacePath: tempDir,
taskId: "task-123",
instanceId: "instance-456",
intentId: "intent-1",
filePath: "src/file1.ts",
content: "content",
startLine: 1,
endLine: 10,
mutationClass: "INTENT_EVOLUTION",
})
const result = await getTraceHistoryForIntent(tempDir, "non-existent")
expect(result).toEqual([])
})
})
describe("classifyMutation", () => {
it("should classify highly similar content as AST_REFACTOR", () => {
const original = "function test() { return 'hello' }"
const result = classifyMutation(original, original.substring(0, 5) + "modified" + original.substring(5))
// This tests the similarity calculation
expect(["AST_REFACTOR", "INTENT_EVOLUTION", "UNKNOWN"]).toContain(result)
})
it("should classify significantly different content as INTENT_EVOLUTION", () => {
const original = "function test() { return 'hello' }"
const result = classifyMutation(original, "completely different content here")
expect(result).toBe("INTENT_EVOLUTION")
})
it("should return UNKNOWN for moderate similarity", () => {
// Create two strings with moderate similarity (~50%) - more characters need to differ
const original = "abcdefgh"
const modified = "abcdefxy" // 50% different (2/8 = 0.75 = 75% similar)
const result = classifyMutation(original, modified)
expect(result).toBe("UNKNOWN")
})
it("should handle empty original content", () => {
const result = classifyMutation("", "new content")
expect(result).toBe("INTENT_EVOLUTION")
})
it("should handle empty new content", () => {
const result = classifyMutation("original content", "")
expect(result).toBe("INTENT_EVOLUTION")
})
it("should handle identical content", () => {
const content = "identical content"
const result = classifyMutation(content, content)
expect(result).toBe("AST_REFACTOR")
})
})
})

View file

@ -0,0 +1,102 @@
/**
* Simple script to create agent_trace.jsonl directly
*
* This creates the trace file without needing the full hook system integration.
* Run with: npx ts-node src/__tests__/scripts/create-trace-direct.ts
*/
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
// Get workspace path from command line or use temp directory
const workspacePath = process.argv[2] || fs.mkdtempSync(path.join(os.tmpdir(), "trace-test-"))
const orchestrationDir = path.join(workspacePath, ".orchestration")
// Ensure directory exists
if (!fs.existsSync(orchestrationDir)) {
fs.mkdirSync(orchestrationDir, { recursive: true })
}
// Create trace entries
const traceEntries = [
{
id: "trace-" + Date.now() + "-1",
timestamp: new Date().toISOString(),
vcs: {
revision_id: "abc1234",
},
files: [
{
relative_path: "src/components/Feature.tsx",
conversations: [
{
url: "task-123",
contributor: {
entity_type: "AI",
model_identifier: "claude-4-sonnet",
},
ranges: [
{
start_line: 1,
end_line: 50,
content_hash: "sha256:abc123...",
},
],
related: [
{
type: "intent",
value: "feature-auth",
},
],
},
],
},
],
},
{
id: "trace-" + Date.now() + "-2",
timestamp: new Date().toISOString(),
vcs: {
revision_id: "def5678",
},
files: [
{
relative_path: "src/utils/auth.ts",
conversations: [
{
url: "task-123",
contributor: {
entity_type: "AI",
model_identifier: "claude-4-sonnet",
},
ranges: [
{
start_line: 1,
end_line: 25,
content_hash: "sha256:def456...",
},
],
related: [
{
type: "intent",
value: "feature-auth",
},
],
},
],
},
],
},
]
// Write to file (JSONL format - one JSON object per line)
const tracePath = path.join(orchestrationDir, "agent_trace.jsonl")
const content = traceEntries.map((entry) => JSON.stringify(entry)).join("\n") + "\n"
fs.writeFileSync(tracePath, content, "utf-8")
console.log(`Created: ${tracePath}`)
console.log(`\nFile contents:\n`)
console.log(fs.readFileSync(tracePath, "utf-8"))
console.log(`\nWorkspace: ${workspacePath}`)

View file

@ -0,0 +1,153 @@
/**
* Manual Test Script for Hook System Trace File Creation
*
* This script tests the HookEngine and TraceLogger directly to verify
* that trace files are created correctly.
*
* Run with: npx ts-node src/__tests__/scripts/manual-trace-full-test.ts
*/
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
import { HookEngine } from "../../hooks/HookEngine"
import { classifyMutation, logTrace } from "../../hooks/TraceLogger"
import { initializeHookEngine, resetHookEngine } from "../../hooks/index"
async function main() {
console.log("=== Manual Hook System Test ===\n")
// Reset any previous state
resetHookEngine()
// Create a temporary workspace directory
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hook-trace-test-"))
console.log(`Test workspace: ${tempDir}`)
// Create .orchestration directory
const orchestrationDir = path.join(tempDir, ".orchestration")
fs.mkdirSync(orchestrationDir, { recursive: true })
console.log(`Orchestration dir: ${orchestrationDir}`)
// Create some test files
const testFile1 = path.join(tempDir, "src", "test.ts")
const testFile2 = path.join(tempDir, "src", "utils", "helper.ts")
fs.mkdirSync(path.dirname(testFile1), { recursive: true })
fs.mkdirSync(path.dirname(testFile2), { recursive: true })
fs.writeFileSync(testFile1, "console.log('hello');\n")
fs.writeFileSync(testFile2, "export const helper = 42;\n")
console.log(`Created test files: ${testFile1}, ${testFile2}\n`)
// Initialize the HookEngine
const taskId = "test-task-123"
const instanceId = "test-instance-456"
initializeHookEngine(tempDir, taskId, instanceId)
const hookEngine = HookEngine.getInstance()
// Get the active intent ID (should be set after initializeHookEngine with setActiveIntent)
// The context should be available via the session state
const context = {
taskId,
instanceId,
cwd: tempDir,
toolName: "write_to_file",
toolParams: { file_path: "src/test.ts", content: "new content" },
activeIntentId: null as string | null,
}
// Test 1: Set active intent
console.log("=== Test 1: Set Active Intent ===")
const intentResult = await hookEngine.setActiveIntent("intent-1")
console.log("Set intent result:", intentResult)
console.log("Active intent ID:", hookEngine.getActiveIntentId())
// Update context with active intent
context.activeIntentId = hookEngine.getActiveIntentId()
console.log("")
// Test 2: Pre-Hook (should allow write to file in scope)
console.log("=== Test 2: Pre-Hook (write in scope) ===")
const preResult1 = await hookEngine.preHook({
...context,
toolName: "write_to_file",
toolParams: { file_path: "src/test.ts", content: "new content" },
})
console.log("Pre-hook result:", preResult1)
console.log("")
// Test 3: Pre-Hook (should block write to file outside scope)
console.log("=== Test 3: Pre-Hook (write outside scope - should block) ===")
const preResult2 = await hookEngine.preHook({
...context,
toolName: "write_to_file",
toolParams: { file_path: "src/outside/scope.ts", content: "blocked" },
})
console.log("Pre-hook result:", preResult2)
console.log("")
// Test 4: Post-Hook (trace creation)
console.log("=== Test 4: Post-Hook (trace creation) ===")
// Read original content for mutation classification
const originalContent = fs.readFileSync(testFile1, "utf-8")
const newContent = "console.log('updated');\n"
// Classify the mutation
const mutationClass = classifyMutation(originalContent, newContent)
console.log("Mutation class:", mutationClass)
const postResult = await hookEngine.postHook(
{
...context,
toolName: "write_to_file",
toolParams: { file_path: "src/test.ts", content: newContent },
},
"File written successfully",
mutationClass,
)
console.log("Post-hook result:", postResult)
console.log("")
// Test 5: Verify trace file was created
console.log("=== Test 5: Verify Trace File ===")
const traceFile = path.join(orchestrationDir, "agent_trace.jsonl")
if (fs.existsSync(traceFile)) {
const traceContent = fs.readFileSync(traceFile, "utf-8")
const traceLines = traceContent
.trim()
.split("\n")
.filter((line) => line.trim())
console.log(`Trace file exists at: ${traceFile}`)
console.log(`Number of trace entries: ${traceLines.length}`)
if (traceLines.length > 0) {
console.log("\nTrace entries:")
traceLines.forEach((line, i) => {
try {
const entry = JSON.parse(line)
console.log(` ${i + 1}.`, JSON.stringify(entry, null, 2))
} catch {
console.log(` ${i + 1}. (parse error)`, line)
}
})
}
} else {
console.log(`Trace file NOT found at: ${traceFile}`)
}
console.log("")
// Test 6: Complete the intent
console.log("=== Test 6: Complete Intent ===")
await hookEngine.updateIntentStatus("intent-1", "COMPLETED")
console.log("Intent completed, active intent cleared:", hookEngine.getActiveIntentId())
console.log("")
// Cleanup
console.log("=== Test Complete ===")
console.log(`\nTest files left at: ${tempDir}`)
console.log(`To view trace file: cat "${traceFile}"`)
console.log(`To clean up: rm -rf "${tempDir}"`)
}
main().catch(console.error)

View file

@ -0,0 +1,106 @@
/**
* Manual Trace File Creation Script
*
* Run this script to manually create an agent_trace.jsonl file
* Usage: npx ts-node src/__tests__/scripts/manual-trace-test.ts
*/
import * as fs from "fs"
import * as path from "path"
import { logTrace } from "../../hooks/TraceLogger"
import { ensureOrchestrationDir, saveActiveIntents, type ActiveIntentsData } from "../../hooks/types"
// Configuration
const WORKSPACE_PATH = process.argv[2] || process.cwd()
const TEST_INTENT_ID = "test-intent-1"
const TEST_TASK_ID = "test-task-123"
async function main() {
console.log(`Creating trace file in: ${WORKSPACE_PATH}`)
// Ensure .orchestration directory exists
await ensureOrchestrationDir(WORKSPACE_PATH)
// Create a test active_intents.yaml file
const intentsData: ActiveIntentsData = {
active_intents: [
{
id: TEST_INTENT_ID,
name: "Test Feature",
status: "IN_PROGRESS",
owned_scope: ["src/**/*"],
constraints: ["Test constraint"],
acceptance_criteria: ["Tests pass"],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
],
}
await saveActiveIntents(WORKSPACE_PATH, intentsData)
console.log("Created active_intents.yaml")
// Create trace entries
console.log("Creating trace entries...")
// Trace entry 1
await logTrace({
workspacePath: WORKSPACE_PATH,
taskId: TEST_TASK_ID,
instanceId: "instance-001",
intentId: TEST_INTENT_ID,
filePath: "src/components/Test.tsx",
content: "export const Test = () => <div>Hello</div>",
startLine: 1,
endLine: 1,
modelIdentifier: "claude-4-sonnet",
mutationClass: "INTENT_EVOLUTION",
})
// Trace entry 2
await logTrace({
workspacePath: WORKSPACE_PATH,
taskId: TEST_TASK_ID,
instanceId: "instance-001",
intentId: TEST_INTENT_ID,
filePath: "src/utils/helper.ts",
content: "export function helper() { return true; }",
startLine: 1,
endLine: 1,
mutationClass: "AST_REFACTOR",
})
// Trace entry 3
await logTrace({
workspacePath: WORKSPACE_PATH,
taskId: TEST_TASK_ID,
instanceId: "instance-001",
intentId: TEST_INTENT_ID,
filePath: "README.md",
content: "# Test Project",
startLine: 1,
endLine: 1,
mutationClass: "DOCUMENTATION",
})
// Verify file was created
const tracePath = path.join(WORKSPACE_PATH, ".orchestration", "agent_trace.jsonl")
if (fs.existsSync(tracePath)) {
console.log(`\n✓ Trace file created: ${tracePath}`)
console.log("\nFile contents:")
const content = fs.readFileSync(tracePath, "utf-8")
content
.split("\n")
.filter(Boolean)
.forEach((line, i) => {
const entry = JSON.parse(line)
console.log(`\n--- Entry ${i + 1} ---`)
console.log(`File: ${entry.files[0].relative_path}`)
console.log(`Intent: ${entry.files[0].conversations[0].related[0].value}`)
console.log(`Mutation: ${entry.files[0].conversations[0].ranges[0].content_hash}`)
})
} else {
console.error("Failed to create trace file")
}
}
main().catch(console.error)