schema trigger implemntation done

This commit is contained in:
Bethel Yohannes 2026-02-21 15:50:07 +03:00
parent 04965a7cf0
commit e226d1e2a5
11 changed files with 139 additions and 12 deletions

View file

@ -468,6 +468,12 @@ export class NativeToolCallParser {
nativeArgs = {
path: partialArgs.path,
content: partialArgs.content,
intent_id: partialArgs.intent_id,
mutation_class: partialArgs.mutation_class as
| "AST_REFACTOR"
| "INTENT_EVOLUTION"
| "NEW_FILE"
| undefined,
}
}
break
@ -905,6 +911,12 @@ export class NativeToolCallParser {
nativeArgs = {
path: args.path,
content: args.content,
intent_id: args.intent_id,
mutation_class: args.mutation_class as
| "AST_REFACTOR"
| "INTENT_EVOLUTION"
| "NEW_FILE"
| undefined,
} as NativeArgsFor<TName>
}
break

View file

@ -676,18 +676,48 @@ export async function presentAssistantMessage(cline: Task) {
}
}
// Hook Engine: Post-Hook for write_to_file appends to agent_trace.jsonl
const preHook = new PreHook({
cwd: cline.cwd,
getActiveIntentId: () => cline.getActiveIntentId(),
setActiveIntentId: (id) => cline.setActiveIntentId(id),
requireIntentForDestructiveOnly: true,
})
const hookMiddleware = new HookMiddleware({
preHook,
getActiveIntentId: () => cline.getActiveIntentId(),
getCwd: () => cline.cwd,
getReqId: () => cline.taskId,
getSessionLogId: () => undefined,
getModelId: () => cline.api.getModel()?.id,
getVcsRevisionId: () => undefined,
})
switch (block.name) {
case "select_active_intent":
// Handled entirely by pre-hook (injectResult pushed above)
break
case "write_to_file":
case "write_to_file": {
await checkpointSaveAndMark(cline)
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
askApproval,
handleError,
pushToolResult,
onWriteToFileSuccess: async (p) => {
await hookMiddleware.postToolUse(
"write_to_file",
{
path: p.path,
content: p.content,
intent_id: p.intent_id,
mutation_class: p.mutation_class,
},
{},
)
},
})
break
}
case "update_todo_list":
await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
askApproval,

View file

@ -1,5 +1,7 @@
import type OpenAI from "openai"
const MUTATION_CLASS_ENUM = ["AST_REFACTOR", "INTENT_EVOLUTION", "NEW_FILE"] as const
const WRITE_TO_FILE_DESCRIPTION = `Request to write content to a file. This tool is primarily used for creating new files or for scenarios where a complete rewrite of an existing file is intentionally required. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
@ -8,13 +10,22 @@ When using this tool, use it directly with the desired content. You do not need
When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
**Traceability:** You MUST provide intent_id (the active intent from select_active_intent) and mutation_class:
- AST_REFACTOR: Syntax/structural change, same intent (e.g. rename, format, extract function).
- INTENT_EVOLUTION: New feature or behavior change tied to the intent.
- NEW_FILE: Creating a file that did not exist before.
Example: Writing a configuration file
{ "path": "frontend-config.json", "content": "{\\n \\"apiEndpoint\\": \\"https://api.example.com\\",\\n \\"theme\\": {\\n \\"primaryColor\\": \\"#007bff\\"\\n }\\n}" }`
{ "path": "frontend-config.json", "content": "{\\n \\"apiEndpoint\\": \\"https://api.example.com\\",\\n \\"theme\\": {\\n \\"primaryColor\\": \\"#007bff\\"\\n }\\n}", "intent_id": "INT-001", "mutation_class": "INTENT_EVOLUTION" }`
const PATH_PARAMETER_DESCRIPTION = `The path of the file to write to (relative to the current workspace directory)`
const CONTENT_PARAMETER_DESCRIPTION = `The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.`
const INTENT_ID_DESCRIPTION = `The ID of the active intent (from select_active_intent) that this write serves. Required for traceability.`
const MUTATION_CLASS_DESCRIPTION = `Semantic classification: AST_REFACTOR (syntax change, same intent), INTENT_EVOLUTION (new feature/behavior), or NEW_FILE (creating a new file).`
export default {
type: "function",
function: {
@ -32,8 +43,17 @@ export default {
type: "string",
description: CONTENT_PARAMETER_DESCRIPTION,
},
intent_id: {
type: "string",
description: INTENT_ID_DESCRIPTION,
},
mutation_class: {
type: "string",
enum: MUTATION_CLASS_ENUM,
description: MUTATION_CLASS_DESCRIPTION,
},
},
required: ["path", "content"],
required: ["path", "content", "intent_id", "mutation_class"],
additionalProperties: false,
},
},

View file

@ -7,6 +7,7 @@ import type { ToolUse, HandleError, PushToolResult, AskApproval, NativeToolArgs
export interface WriteToFileSuccessParams {
path: string
content: string
intent_id?: string
mutation_class?: string
}

View file

@ -180,9 +180,11 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
pushToolResult(message)
const mutationClass = (params as Record<string, unknown>).mutation_class as string | undefined
const intentId = (params as Record<string, unknown>).intent_id as string | undefined
await callbacks.onWriteToFileSuccess?.({
path: relPath,
content: newContent,
intent_id: intentId,
mutation_class: mutationClass,
})

View file

@ -3,14 +3,23 @@ import crypto from "crypto"
const HASH_PREFIX = "sha256:"
/**
* Compute a SHA-256 content hash for spatial independence.
* If lines move, the hash of the content block remains valid.
* Generate a SHA-256 hash of string content (spatial hashing utility).
* Returns a prefixed hex string (e.g. "sha256:abc123...") for traceability.
* Content hash remains valid even if line positions change.
*/
export function contentHash(content: string): string {
const hash = crypto.createHash("sha256").update(content, "utf8").digest("hex")
return `${HASH_PREFIX}${hash}`
}
/**
* Alias for contentHash - generates SHA-256 hash of string content.
* Use for spatial hashing in agent trace and diff operations.
*/
export function sha256Hash(content: string): string {
return contentHash(content)
}
/**
* Extract a logical code block (e.g. by line range) and return its hash.
*/

31
src/hooks/format.ts Normal file
View file

@ -0,0 +1,31 @@
/**
* Minimal tool error formatters for pre-hook.
* Avoids importing from core/prompts/responses to prevent vscode dependency in Node scripts.
*/
export const toolErrorFormat = {
toolError: (error?: string) =>
JSON.stringify({
status: "error",
message: "The tool execution failed",
error,
}),
toolErrorScopeViolation: (intentId: string, filename: string) =>
JSON.stringify({
status: "error",
type: "scope_violation",
message: `Scope Violation: ${intentId} is not authorized to edit [${filename}]. Request scope expansion.`,
intent_id: intentId,
path: filename,
suggestion: "Request scope expansion in .orchestration/active_intents.yaml or choose another intent.",
}),
toolErrorUserRejected: (toolName?: string) =>
JSON.stringify({
status: "error",
type: "user_rejected",
message: "The user rejected this operation.",
tool: toolName,
suggestion: "Do not retry the same operation; try a different approach or ask the user for permission.",
}),
}

View file

@ -6,6 +6,8 @@ export interface HookMiddlewareOptions {
preHook: PreHook
getActiveIntentId: () => string | null
getCwd: () => string
/** REQ-ID from Phase 1 - injected into agent_trace related array */
getReqId?: () => string | undefined
getSessionLogId?: () => string | undefined
getModelId?: () => string | undefined
getVcsRevisionId?: () => string | undefined
@ -39,7 +41,8 @@ export class HookMiddleware {
const contentParam = params.content
if (typeof pathParam !== "string" || typeof contentParam !== "string") return
const intentId = this.options.getActiveIntentId()
const intentId =
(typeof params.intent_id === "string" ? params.intent_id.trim() : null) || this.options.getActiveIntentId()
const mutationClass = (params.mutation_class as "AST_REFACTOR" | "INTENT_EVOLUTION" | "NEW_FILE") ?? "UNKNOWN"
await appendAgentTrace(this.options.getCwd(), {
@ -47,6 +50,7 @@ export class HookMiddleware {
content: contentParam,
intentId,
mutationClass,
reqId: this.options.getReqId?.(),
sessionLogId: this.options.getSessionLogId?.(),
modelIdentifier: this.options.getModelId?.(),
vcsRevisionId: this.options.getVcsRevisionId?.(),

View file

@ -13,6 +13,8 @@ export interface PostHookWriteParams {
content: string
intentId: string | null
mutationClass?: MutationClass
/** REQ-ID from Phase 1 - injected into related array for traceability */
reqId?: string
sessionLogId?: string
modelIdentifier?: string
vcsRevisionId?: string
@ -28,6 +30,7 @@ export async function appendAgentTrace(cwd: string, params: PostHookWriteParams)
content,
intentId,
mutationClass = "UNKNOWN",
reqId,
sessionLogId,
modelIdentifier = "unknown",
vcsRevisionId,
@ -39,11 +42,15 @@ export async function appendAgentTrace(cwd: string, params: PostHookWriteParams)
const lines = content.split("\n")
const fullRangeHash = contentHash(content)
const related: Array<{ type: string; value: string }> = []
if (intentId) related.push({ type: "specification", value: intentId })
if (reqId) related.push({ type: "request", value: reqId })
const conversation: AgentTraceConversation = {
url: sessionLogId,
contributor: { entity_type: "AI", model_identifier: modelIdentifier },
ranges: [{ start_line: 1, end_line: lines.length, content_hash: fullRangeHash }],
related: intentId ? [{ type: "specification", value: intentId }] : [],
related,
}
const fileEntry: AgentTraceFileEntry = {

View file

@ -3,7 +3,10 @@ import path from "path"
import type { HookResult } from "./types"
import { DESTRUCTIVE_TOOLS } from "./types"
import { loadIntentContext, buildConsolidatedIntentContextXml } from "./context-loader"
import { loadIntentIgnore, isPathIgnored, isIntentExcluded } from "./intent-ignore"
import type { IntentIgnoreResult } from "./intent-ignore"
import { pathInScope } from "./scope"
import { toolErrorFormat } from "./format"
/** Block paths that escape workspace (.. or absolute outside cwd). */
function isPathTraversal(relPath: string, cwd: string): boolean {
@ -76,7 +79,7 @@ export class PreHook {
if (isIntentExcluded(activeId, ignore.excludedIntentIds)) {
return {
blocked: true,
error: formatResponse.toolError(
error: toolErrorFormat.toolError(
`Intent ${activeId} is listed in .intentignore and cannot be modified. Choose another intent or ask the user to update .intentignore.`,
),
}
@ -107,7 +110,7 @@ export class PreHook {
if (isPathIgnored(relPath, ignore.pathPatterns)) {
return {
blocked: true,
error: formatResponse.toolError(
error: toolErrorFormat.toolError(
`Path "${relPath}" is excluded by .intentignore. You are not authorized to edit it.`,
),
}
@ -116,7 +119,7 @@ export class PreHook {
if (context && context.owned_scope.length > 0 && !pathInScope(relPath, context.owned_scope, cwd)) {
return {
blocked: true,
error: formatResponse.toolErrorScopeViolation(activeId, relPath),
error: toolErrorFormat.toolErrorScopeViolation(activeId, relPath),
}
}
}
@ -128,7 +131,7 @@ export class PreHook {
if (!approved) {
return {
blocked: true,
error: formatResponse.toolErrorUserRejected(toolName),
error: toolErrorFormat.toolErrorUserRejected(toolName),
}
}
}

View file

@ -80,6 +80,9 @@ export const toolParamNames = [
// read_file legacy format parameter (backward compatibility)
"files",
"line_ranges",
// write_to_file traceability (AI-Native Git Layer)
"intent_id",
"mutation_class",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@ -114,7 +117,12 @@ export type NativeToolArgs = {
switch_mode: { mode_slug: string; reason: string }
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 }
write_to_file: {
path: string
content: string
intent_id?: string
mutation_class?: "AST_REFACTOR" | "INTENT_EVOLUTION" | "NEW_FILE"
}
// Add more tools as they are migrated to native protocol
}