mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-13 23:11:10 +00:00
chore: commit src/hooks/OrchestrationDataModel.ts
This commit is contained in:
parent
5ce9e584ae
commit
b8f44b1c44
1 changed files with 241 additions and 5 deletions
|
|
@ -30,6 +30,20 @@ export interface AgentTraceRange {
|
|||
content_hash: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation classification types (Phase 3 requirement)
|
||||
*/
|
||||
export type MutationClass = "AST_REFACTOR" | "INTENT_EVOLUTION"
|
||||
|
||||
/**
|
||||
* Mutation classification result
|
||||
*/
|
||||
export interface MutationClassification {
|
||||
mutation_class: MutationClass
|
||||
confidence: "high" | "medium" | "low"
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface AgentTraceConversation {
|
||||
url: string
|
||||
contributor: {
|
||||
|
|
@ -51,6 +65,8 @@ export interface AgentTraceFile {
|
|||
export interface AgentTraceEntry {
|
||||
id: string
|
||||
timestamp: string
|
||||
tool_name?: string // Phase 3: Track which tool made the change
|
||||
mutation_class?: MutationClass // Phase 3: AST_REFACTOR or INTENT_EVOLUTION
|
||||
vcs: {
|
||||
revision_id: string
|
||||
}
|
||||
|
|
@ -125,6 +141,25 @@ This file contains persistent knowledge shared across parallel sessions (Archite
|
|||
`
|
||||
await fs.writeFile(agentPath, header, "utf-8")
|
||||
}
|
||||
|
||||
// Initialize .intentignore if it doesn't exist (Phase 2 requirement)
|
||||
const intentIgnorePath = path.join(this.orchestrationDir, ".intentignore")
|
||||
try {
|
||||
await fs.access(intentIgnorePath)
|
||||
} catch {
|
||||
// File doesn't exist, create it with header
|
||||
const header = `# Intent Ignore File
|
||||
# List intent IDs that should be protected from modifications
|
||||
# One intent ID per line
|
||||
# Lines starting with # are comments
|
||||
#
|
||||
# Example:
|
||||
# INT-005 # Legacy system - deprecated
|
||||
# INT-010 # Production critical - manual changes only
|
||||
|
||||
`
|
||||
await fs.writeFile(intentIgnorePath, header, "utf-8")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize orchestration directory:", error)
|
||||
throw error
|
||||
|
|
@ -201,7 +236,10 @@ This file contains persistent knowledge shared across parallel sessions (Archite
|
|||
|
||||
try {
|
||||
const content = await fs.readFile(tracePath, "utf-8")
|
||||
const lines = content.trim().split("\n").filter((line) => line.trim() && !line.startsWith("#"))
|
||||
const lines = content
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() && !line.startsWith("#"))
|
||||
|
||||
const entries: AgentTraceEntry[] = []
|
||||
|
||||
|
|
@ -213,9 +251,7 @@ This file contains persistent knowledge shared across parallel sessions (Archite
|
|||
// Check if any file's conversation references this intent
|
||||
const referencesIntent = entry.files.some((file) =>
|
||||
file.conversations.some((conv) =>
|
||||
conv.related.some(
|
||||
(rel) => rel.type === "intent" && rel.value === intentId,
|
||||
),
|
||||
conv.related.some((rel) => rel.type === "intent" && rel.value === intentId),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -250,11 +286,211 @@ This file contains persistent knowledge shared across parallel sessions (Archite
|
|||
return crypto.createHash("sha256").update(content).digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify mutation type: AST_REFACTOR vs INTENT_EVOLUTION (Phase 3 requirement)
|
||||
*
|
||||
* Heuristics:
|
||||
* - AST_REFACTOR: Same semantic meaning, structural changes (renames, formatting, reorganization)
|
||||
* - INTENT_EVOLUTION: New functionality, feature additions, behavior changes
|
||||
*
|
||||
* This is a simplified heuristic - in production, this could use AST diffing or ML models
|
||||
*/
|
||||
classifyMutation(oldContent: string | null, newContent: string, filePath: string): MutationClassification {
|
||||
// If file didn't exist before, it's always INTENT_EVOLUTION
|
||||
if (!oldContent || oldContent === "") {
|
||||
return {
|
||||
mutation_class: "INTENT_EVOLUTION",
|
||||
confidence: "high",
|
||||
reason: "New file creation",
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize whitespace for comparison
|
||||
const oldNormalized = oldContent.replace(/\s+/g, " ").trim()
|
||||
const newNormalized = newContent.replace(/\s+/g, " ").trim()
|
||||
|
||||
// If content is identical (after normalization), it's not a mutation
|
||||
if (oldNormalized === newNormalized) {
|
||||
return {
|
||||
mutation_class: "AST_REFACTOR",
|
||||
confidence: "high",
|
||||
reason: "No semantic changes detected",
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate similarity ratio
|
||||
const similarity = this.calculateSimilarity(oldNormalized, newNormalized)
|
||||
|
||||
// Heuristic: If >80% similar, likely a refactor
|
||||
if (similarity > 0.8) {
|
||||
// Check if it's mostly structural changes
|
||||
const oldLines = oldContent.split("\n")
|
||||
const newLines = newContent.split("\n")
|
||||
const lineCountDiff = Math.abs(oldLines.length - newLines.length) / Math.max(oldLines.length, 1)
|
||||
|
||||
// If line count changed significantly, might be evolution
|
||||
if (lineCountDiff > 0.3) {
|
||||
return {
|
||||
mutation_class: "INTENT_EVOLUTION",
|
||||
confidence: "medium",
|
||||
reason: `Significant line count change (${Math.round(lineCountDiff * 100)}%) despite high similarity`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mutation_class: "AST_REFACTOR",
|
||||
confidence: similarity > 0.9 ? "high" : "medium",
|
||||
reason: `High similarity (${Math.round(similarity * 100)}%) suggests refactoring`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new function/class definitions (strong indicator of evolution)
|
||||
const newFunctionPattern = /(?:function|class|const|let|var)\s+\w+\s*[=:\(]/g
|
||||
const oldMatches = (oldContent.match(newFunctionPattern) || []).length
|
||||
const newMatches = (newContent.match(newFunctionPattern) || []).length
|
||||
|
||||
if (newMatches > oldMatches) {
|
||||
return {
|
||||
mutation_class: "INTENT_EVOLUTION",
|
||||
confidence: "high",
|
||||
reason: `New code definitions detected (${newMatches - oldMatches} new)`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for significant content addition
|
||||
const contentGrowth = (newContent.length - oldContent.length) / Math.max(oldContent.length, 1)
|
||||
if (contentGrowth > 0.5) {
|
||||
return {
|
||||
mutation_class: "INTENT_EVOLUTION",
|
||||
confidence: "medium",
|
||||
reason: `Significant content growth (${Math.round(contentGrowth * 100)}%)`,
|
||||
}
|
||||
}
|
||||
|
||||
// Default: if similarity is low, likely evolution
|
||||
return {
|
||||
mutation_class: similarity < 0.5 ? "INTENT_EVOLUTION" : "AST_REFACTOR",
|
||||
confidence: "low",
|
||||
reason: `Similarity: ${Math.round(similarity * 100)}%`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity ratio between two strings using Levenshtein distance
|
||||
*/
|
||||
private calculateSimilarity(str1: string, str2: string): number {
|
||||
const maxLen = Math.max(str1.length, str2.length)
|
||||
if (maxLen === 0) return 1.0
|
||||
|
||||
const distance = this.levenshteinDistance(str1, str2)
|
||||
return 1 - distance / maxLen
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Levenshtein distance between two strings
|
||||
*/
|
||||
private levenshteinDistance(str1: string, str2: string): number {
|
||||
const m = str1.length
|
||||
const n = str2.length
|
||||
const dp: number[][] = []
|
||||
|
||||
for (let i = 0; i <= m; i++) {
|
||||
dp[i] = [i]
|
||||
}
|
||||
for (let j = 0; j <= n; j++) {
|
||||
dp[0][j] = j
|
||||
}
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (str1[i - 1] === str2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
dp[i][j] = Math.min(
|
||||
dp[i - 1][j] + 1, // deletion
|
||||
dp[i][j - 1] + 1, // insertion
|
||||
dp[i - 1][j - 1] + 1, // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m][n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Append lesson learned to AGENT.md (Phase 4 requirement)
|
||||
*/
|
||||
async appendLesson(lesson: string, context?: { tool?: string; error?: string; file?: string }): Promise<void> {
|
||||
const agentPath = path.join(this.orchestrationDir, "AGENT.md")
|
||||
try {
|
||||
let content = await fs.readFile(agentPath, "utf-8")
|
||||
|
||||
// Ensure Lessons Learned section exists
|
||||
if (!content.includes("## Lessons Learned")) {
|
||||
content += "\n\n## Lessons Learned\n\n"
|
||||
}
|
||||
|
||||
// Append new lesson with timestamp
|
||||
const timestamp = new Date().toISOString()
|
||||
const contextInfo = context
|
||||
? `\n**Context:** ${context.tool ? `Tool: ${context.tool}` : ""}${context.file ? ` | File: ${context.file}` : ""}${context.error ? ` | Error: ${context.error}` : ""}\n`
|
||||
: ""
|
||||
const lessonEntry = `### ${timestamp}\n${contextInfo}${lesson}\n\n---\n\n`
|
||||
|
||||
// Find the Lessons Learned section and append
|
||||
const lessonsIndex = content.indexOf("## Lessons Learned")
|
||||
if (lessonsIndex !== -1) {
|
||||
const afterHeader = content.indexOf("\n", lessonsIndex) + 1
|
||||
content = content.slice(0, afterHeader) + lessonEntry + content.slice(afterHeader)
|
||||
} else {
|
||||
content += `\n## Lessons Learned\n\n${lessonEntry}`
|
||||
}
|
||||
|
||||
await fs.writeFile(agentPath, content, "utf-8")
|
||||
} catch (error) {
|
||||
console.error("Failed to append lesson to AGENT.md:", error)
|
||||
// Don't throw - lesson recording shouldn't break execution
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestration directory path
|
||||
*/
|
||||
getOrchestrationDir(): string {
|
||||
return this.orchestrationDir
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read .intentignore file and return list of ignored intent IDs (Phase 2 requirement)
|
||||
*/
|
||||
async readIntentIgnore(): Promise<string[]> {
|
||||
const intentIgnorePath = path.join(this.orchestrationDir, ".intentignore")
|
||||
try {
|
||||
const content = await fs.readFile(intentIgnorePath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
const ignoredIntents: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// Remove comments and trim
|
||||
const cleanLine = line.split("#")[0].trim()
|
||||
if (cleanLine && !cleanLine.startsWith("#")) {
|
||||
ignoredIntents.push(cleanLine)
|
||||
}
|
||||
}
|
||||
|
||||
return ignoredIntents
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read - return empty array
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an intent ID is in the ignore list
|
||||
*/
|
||||
async isIntentIgnored(intentId: string): Promise<boolean> {
|
||||
const ignoredIntents = await this.readIntentIgnore()
|
||||
return ignoredIntents.includes(intentId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue