docs: finalize interim submission artifacts (intent_map and hooks directory)

This commit is contained in:
Yakob Dereje 2026-02-18 21:37:28 +03:00
parent d9eaead66a
commit ab4b161f2e
13 changed files with 243 additions and 11 deletions

View file

@ -1,12 +1,3 @@
<!-- # Architecture Notes
## Findings
- Tool Execution: Controlled via `presentAssistantMessage.ts` and individual tool handles like `WriteToFileTool.ts`.
- Prompt Logic: The system prompt is constructed in `src/core/task/Task.ts`.
- Governance Layer: We have initialized a `.orchestration/` sidecar for intent-traceability.
- Integration Goal: We will implement a Pre-Hook in `WriteToFileTool.ts` that validates actions against `.orchestration/active_intents.yaml`. -->
# Architecture Notes: Master Thinker Edition
## 1. Data Flow Map (Request-to-Execution)
@ -24,3 +15,38 @@
## 3. Intent-Code Gap Analysis
Standard Git tracks "What" changed but lacks the "Why." By using a sidecar orchestration layer, we map every Abstract Syntax Tree (AST) change to a specific Requirement ID. This prevents "Context Rot" where agents lose track of architectural constraints during long-running tasks.
# Architectural Design Report
## 1. The Intent-First Protocol (Two-Stage State Machine)
The core of this implementation is a move away from "Vibe Coding" towards a governed, stateful interaction. I have architected a Two-Stage State Machine for every user request:
**Stage 1: The Reasoning Intercept (The Handshake):** The agent is no longer permitted to generate code immediately. It must first analyze the request, identify a valid intent_id from the governance sidecar, and call the select_active_intent tool.
+1
**Stage 2: Contextualized Action:** Only after the "Handshake" is successful and the context is injected can the agent proceed to use destructive tools like write_to_file or execute_command.
+1
## 2. The Deterministic Hook (Gatekeeper Architecture)
To ensure compliance, I implemented a Deterministic Hook System that acts as a strict middleware boundary:
+1
Pre-Hook Implementation: In WriteToFileTool.ts and ExecuteCommandTool.ts, I injected a gatekeeper check at the start of the handle method.
Verification Logic: This hook verifies the presence of a global active intent flag. If the agent attempts a file modification without a validated "checkout," the hook blocks execution and returns a formal governance error: "You must cite a valid active Intent ID".
Fail-Safe: This ensures that the architecture enforces the rules, rather than relying on the LLM's "best effort" to follow instructions.
## 3. Context Engineering (Dynamic Injection vs. Context Rot)
Traditional AI IDEs suffer from "Context Rot" by dumping entire file trees into the prompt. This implementation solves this via Dynamic Context Injection:
+1
**Sidecar Pattern:** All architectural constraints and business intents are stored in .orchestration/active_intents.yaml.
+1
**On-Demand Context:** When select_active_intent is called, the system reads the YAML and constructs a targeted <intent_context> XML block.
**Traceability:** This ensures the agent only operates within its "owned_scope" and respects the "acceptance_criteria" defined in the sidecar, maintaining a high signal-to-noise ratio in the context window.

8
intent_map.md Normal file
View file

@ -0,0 +1,8 @@
# Intent-Code Mapping
| Intent ID | Target Files / Modules | Status |
| --------- | ------------------------------------------------- | ----------- |
| INT-001 | src/core/tools/WriteToFileTool.ts | IN PROGRESS |
| INT-001 | src/core/assistant-msg/presentAssistantMessage.ts | COMPLETED |
[cite_start]**Notes:** This map links the high-level business logic in `.orchestration/active_intents.yaml` to specific AST changes in the source code.

View file

@ -39,6 +39,7 @@ export const toolNames = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"select_active_intent",
"new_task",
"codebase_search",
"update_todo_list",

View file

@ -546,6 +546,14 @@ export class NativeToolCallParser {
}
break
case "select_active_intent":
if (partialArgs.intent_id !== undefined) {
nativeArgs = {
intent_id: partialArgs.intent_id,
}
}
break
case "update_todo_list":
if (partialArgs.todos !== undefined) {
nativeArgs = {
@ -881,6 +889,14 @@ export class NativeToolCallParser {
}
break
case "select_active_intent":
if (args.intent_id !== undefined) {
nativeArgs = {
intent_id: args.intent_id,
} as NativeArgsFor<TName>
}
break
case "update_todo_list":
if (args.todos !== undefined) {
nativeArgs = {

View file

@ -28,6 +28,7 @@ import { useMcpToolTool } from "../tools/UseMcpToolTool"
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
import { switchModeTool } from "../tools/SwitchModeTool"
import { SelectActiveIntentTool } from "../tools/SelectActiveIntentTool"
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
import { newTaskTool } from "../tools/NewTaskTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
@ -41,6 +42,8 @@ import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { formatResponse } from "../prompts/responses"
import { sanitizeToolUseId } from "../../utils/tool-id"
const selectActiveIntentTool = new SelectActiveIntentTool()
/**
* Processes and presents assistant message content to the user interface.
*
@ -365,6 +368,8 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name}]`
case "switch_mode":
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
case "select_active_intent":
return `[${block.name} for '${block.params.intent_id}']`
case "codebase_search":
return `[${block.name} for '${block.params.query}']`
case "read_command_output":
@ -803,6 +808,16 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "select_active_intent": {
try {
const intentId = block.nativeArgs?.intent_id ?? block.params.intent_id ?? ""
const result = await selectActiveIntentTool.handle({ intent_id: intentId }, cline.cwd)
pushToolResult(result)
} catch (error) {
await handleError("loading active intent context", error as Error)
}
break
}
case "new_task":
await checkpointSaveAndMark(cline)
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {

View file

@ -25,6 +25,9 @@ import {
getSkillsSection,
} from "./sections"
const INTENT_DRIVEN_ARCHITECT_RULE =
"You are an Intent-Driven Architect. You CANNOT write code immediately. Your first action MUST be to analyze the user request and call select_active_intent to load the necessary context and constraints."
// Helper function to get prompt component, filtering out empty objects
export function getPromptComponent(
customModePrompts: CustomModePrompts | undefined,
@ -82,7 +85,9 @@ async function generatePrompt(
// Tools catalog is not included in the system prompt.
const toolsCatalog = ""
const basePrompt = `${roleDefinition}
const basePrompt = `${INTENT_DRIVEN_ARCHITECT_RULE}
${roleDefinition}
${markdownFormattingSection()}

View file

@ -13,6 +13,7 @@ import newTask from "./new_task"
import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import selectActiveIntent from "./select_active_intent"
import skill from "./skill"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
readCommandOutput,
createReadFileTool(readFileOptions),
runSlashCommand,
selectActiveIntent,
skill,
searchReplace,
edit_file,

View file

@ -0,0 +1,25 @@
import type OpenAI from "openai"
const SELECT_ACTIVE_INTENT_DESCRIPTION = `Load the active intent context from .orchestration/active_intents.yaml by intent ID. Use this before planning or coding so constraints and scope are explicitly available in the next turn.`
const INTENT_ID_PARAMETER_DESCRIPTION = `Intent ID to load from .orchestration/active_intents.yaml (for example: intent-1)`
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

@ -0,0 +1,37 @@
import * as fs from "fs"
import * as path from "path"
import * as yaml from "yaml"
export class SelectActiveIntentTool {
async handle(params: { intent_id: string }, workspaceRoot: string): Promise<string> {
if (!params.intent_id?.trim()) {
return "ERROR: Missing required parameter 'intent_id'."
}
try {
const content = await fs.promises.readFile(
path.join(workspaceRoot, ".orchestration", "active_intents.yaml"),
"utf-8",
)
const data = yaml.parse(content) as any
const intents = Array.isArray(data)
? data
: Array.isArray(data?.active_intents)
? data.active_intents
: Array.isArray(data?.intents)
? data.intents
: Object.entries(data ?? {}).map(([intent_id, entry]) => ({ intent_id, ...(entry as object) }))
const match = intents.find((intent: any) => (intent?.intent_id ?? intent?.id) === params.intent_id)
if (!match) {
return `ERROR: Intent '${params.intent_id}' not found in .orchestration/active_intents.yaml.`
}
return `<intent_context>${JSON.stringify(match, null, 2)}</intent_context>`
} catch (error) {
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
return "ERROR: Governance sidecar not found at .orchestration/active_intents.yaml. Please initialize Phase 0 first."
}
return `ERROR: Failed to read or parse .orchestration/active_intents.yaml: ${
error instanceof Error ? error.message : String(error)
}`
}
}
}

View file

@ -47,6 +47,17 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
return
}
const hasSelectedActiveIntent = (task.toolUsage.select_active_intent?.attempts ?? 0) > 0
if (!hasSelectedActiveIntent) {
const governanceError = "GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files."
task.consecutiveMistakeCount++
task.recordToolError("write_to_file", governanceError)
task.didToolFailInCurrentTurn = true
pushToolResult(formatResponse.toolError(governanceError))
await task.diffViewProvider.reset()
return
}
const accessAllowed = task.rooIgnoreController?.validateAccess(relPath)
if (!accessAllowed) {

View file

@ -0,0 +1,62 @@
import * as fs from "fs"
import { SelectActiveIntentTool } from "../SelectActiveIntentTool"
describe("SelectActiveIntentTool", () => {
const mockedReadFile = vi.spyOn(fs.promises, "readFile")
const tool = new SelectActiveIntentTool()
const workspaceRoot = "/workspace"
beforeEach(() => {
vi.clearAllMocks()
})
it("returns intent_context XML for matching intent in intents array", async () => {
mockedReadFile.mockResolvedValue(
`intents:
- intent_id: intent-1
constraints:
must_not:
- direct_db_writes
scope:
files:
- src/**
` as never,
)
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toContain("<intent_context>")
expect(result).toContain('"intent_id": "intent-1"')
expect(result).toContain('"constraints"')
expect(result).toContain("direct_db_writes")
expect(result).toContain('"scope"')
expect(result).toContain("src/**")
expect(result).toContain("</intent_context>")
})
it("returns not found error when intent_id does not exist", async () => {
mockedReadFile.mockResolvedValue(
`active_intents:
- intent_id: intent-2
constraints: {}
scope: {}
` as never,
)
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toBe("ERROR: Intent 'intent-1' not found in .orchestration/active_intents.yaml.")
})
it("returns initialization error when sidecar file is missing", async () => {
const missingFileError = Object.assign(new Error("ENOENT"), { code: "ENOENT" })
mockedReadFile.mockRejectedValue(missingFileError)
const result = await tool.handle({ intent_id: "intent-1" }, workspaceRoot)
expect(result).toBe(
"ERROR: Governance sidecar not found at .orchestration/active_intents.yaml. Please initialize Phase 0 first.",
)
})
})

View file

@ -123,6 +123,10 @@ describe("writeToFileTool", () => {
mockCline.cwd = "/"
mockCline.consecutiveMistakeCount = 0
mockCline.didEditFile = false
mockCline.didToolFailInCurrentTurn = false
mockCline.toolUsage = {
select_active_intent: { attempts: 1, failures: 0 },
}
mockCline.diffStrategy = undefined
mockCline.providerRef = {
deref: vi.fn().mockReturnValue({
@ -236,6 +240,22 @@ describe("writeToFileTool", () => {
}
describe("access control", () => {
it("blocks write when no active intent has been selected in this session", async () => {
mockCline.toolUsage = {}
const result = await executeWriteFileTool({})
expect(result).toBe(
"Error: GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
)
expect(mockCline.recordToolError).toHaveBeenCalledWith(
"write_to_file",
"GOVERNANCE ERROR: You must cite a valid active Intent ID before modifying files.",
)
expect(mockCline.diffViewProvider.saveChanges).not.toHaveBeenCalled()
expect(mockCline.diffViewProvider.saveDirectly).not.toHaveBeenCalled()
})
it("validates and allows access when rooIgnoreController permits", async () => {
await executeWriteFileTool({}, { accessAllowed: true })

View file

@ -58,6 +58,7 @@ export const toolParamNames = [
"todos",
"prompt",
"image",
"intent_id",
// read_file parameters (native protocol)
"operations", // search_and_replace parameter for multiple operations
"patch", // apply_patch parameter
@ -112,6 +113,7 @@ export type NativeToolArgs = {
skill: { skill: string; args?: string }
search_files: { path: string; regex: string; file_pattern?: string | null }
switch_mode: { mode_slug: string; reason: string }
select_active_intent: { intent_id: 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 }
@ -282,6 +284,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
ask_followup_question: "ask questions",
attempt_completion: "complete tasks",
switch_mode: "switch modes",
select_active_intent: "load intent context",
new_task: "create new task",
codebase_search: "codebase search",
update_todo_list: "update todo list",
@ -307,7 +310,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["use_mcp_tool", "access_mcp_resource"],
},
modes: {
tools: ["switch_mode", "new_task"],
tools: ["switch_mode", "select_active_intent", "new_task"],
alwaysAvailable: true,
},
}
@ -317,6 +320,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"select_active_intent",
"new_task",
"update_todo_list",
"run_slash_command",