feat: implement intent-governed HookEngine and orchestration data model

- Add HookEngine middleware with pre/post-hooks for tool governance
- Implement OrchestrationDataModel for .orchestration/ directory management
- Create select_active_intent tool enforcing Reasoning Loop protocol
- Integrate hooks into presentAssistantMessage for all destructive tools
- Add UI-blocking authorization (HITL) for intent evolution
- Implement scope enforcement and trace logging with content hashing
This commit is contained in:
Sumeyaaaa 2026-02-18 14:01:09 +03:00
parent d878d57e2a
commit dedb07ed42
13 changed files with 1116 additions and 8 deletions

36
.orchestration/AGENT.md Normal file
View file

@ -0,0 +1,36 @@
# Shared Knowledge Base
This file contains persistent knowledge shared across parallel sessions (Architect/Builder/Tester). Contains "Lessons Learned" and project-specific stylistic rules.
## Lessons Learned
<!--
Example entry:
### 2026-02-16: Authentication Refactoring
- **Issue:** Initial JWT implementation caused circular dependency
- **Solution:** Extracted token validation to separate utility module
- **Impact:** Reduced coupling, improved testability
- **Related Intent:** INT-001
-->
## Project-Specific Rules
<!--
Example entry:
### Code Style
- Always use async/await, never raw Promises
- Prefer named exports over default exports
- Use TypeScript strict mode
-->
## Architectural Decisions
<!--
Example entry:
### 2026-02-16: Database Schema Change
- **Decision:** Migrate from SQLite to PostgreSQL
- **Rationale:** Need better concurrent access for parallel agents
- **Impact:** All database queries must be updated
- **Related Intent:** INT-002
-->

View file

@ -0,0 +1,222 @@
active_intents:
- id: INT-001
name: INT-001 — Intent-Code Traceability (Spec)
status: IN_PROGRESS
owned_scope:
- src/core/assistant-message/**
- src/core/tools/**
- src/core/hooks/**
- src/core/orchestration/**
- src/core/prompts/**
- .orchestration/**
constraints:
- Must enforce **intent selection before any destructive tool**
(`write_to_file`, `edit_file`, `apply_diff`, etc.).
- "Must keep **privilege separation**: UI emits events; extension host
executes privileged actions; hooks are middleware."
- Must log **spatially independent** traces via content hashing.
acceptance_criteria:
- Agent cannot write code before calling `select_active_intent(intent_id)`.
- "When a file is written, a JSONL entry is appended to
`.orchestration/agent_trace.jsonl` that includes:"
- intent id
- file path
- line range (best-effort)
- "`sha256:` content hash of the modified block"
- "`.orchestration/active_intents.yaml` exists and contains this intent."
created_at: 2026-02-18T08:56:57.063Z
updated_at: 2026-02-18T08:56:57.086Z
spec_hash: sha256:7966563e9a7886587d3c421761708195d8a1ce21addd4632f560965411fd839b
spec_file: specs/INT-001-intent-code-traceability.md
- id: INT-002
name: INT-002 — Hook System Implementation
status: IN_PROGRESS
owned_scope:
- src/core/hooks/**
- src/core/assistant-message/presentAssistantMessage.ts
- src/core/tools/**
- .orchestration/**
constraints:
- Must integrate with existing `presentAssistantMessage()` function
without breaking current tool execution flow.
- Pre-hooks must run **before** `tool.handle()` is called.
- Post-hooks must run **after** `tool.execute()` completes but before
result is returned.
- Hook system must be non-blocking for non-destructive tools (read-only
operations).
- Must maintain backward compatibility with existing tools.
acceptance_criteria:
- "`HookEngine` class exists in `src/core/hooks/HookEngine.ts`."
- Pre-hook validates intent selection for destructive tools
(`write_to_file`, `edit_file`, `execute_command`, etc.).
- Pre-hook enforces scope validation (file path must be within intent's
`owned_scope`).
- Post-hook logs trace entries to `.orchestration/agent_trace.jsonl` for
mutating actions.
- "`presentAssistantMessage()` integrates `HookEngine` with Pre-Hook and
Post-Hook calls."
- All existing tests pass after hook integration.
created_at: 2026-02-18T08:56:57.094Z
updated_at: 2026-02-18T08:56:57.094Z
spec_hash: sha256:e10e923c607996643684be16016bc8305d7259b543eae623e92b5e49db8b902c
spec_file: specs/INT-002-hook-system-implementation.md
- id: INT-003
name: INT-003 — Two-Stage Reasoning Loop
status: IN_PROGRESS
owned_scope:
- src/core/hooks/HookEngine.ts
- src/core/prompts/sections/tool-use-guidelines.ts
- src/core/tools/SelectActiveIntentTool.ts
- src/core/task/Task.ts
constraints:
- "**Stage 1 (Reasoning Intercept):** Agent MUST call
`select_active_intent(intent_id)` before any destructive tool."
- "**Stage 2 (Contextualized Action):** Agent receives intent context and
must include it when making code changes."
- System prompt must enforce this protocol in tool-use guidelines.
- Intent context must be injected into the agent's context before code
generation.
acceptance_criteria:
- System prompt includes instructions requiring `select_active_intent`
before code changes.
- "`SelectActiveIntentTool` returns XML `<intent_context>` block with
scope, constraints, and acceptance criteria."
- Pre-hook blocks destructive tools if no active intent is selected.
- Agent receives intent context in subsequent tool calls.
- Intent context is logged in `agent_trace.jsonl` entries.
created_at: 2026-02-18T08:56:57.098Z
updated_at: 2026-02-18T08:56:57.098Z
spec_hash: sha256:a75817698c5b1479c68e43dd41b04752d3a6df7c4af71f3ab60fdedf705e4dda
spec_file: specs/INT-003-reasoning-loop.md
- id: INT-004
name: INT-004 — Orchestration Directory Management
status: IN_PROGRESS
owned_scope:
- src/core/orchestration/OrchestrationDataModel.ts
- .orchestration/active_intents.yaml
- .orchestration/agent_trace.jsonl
- .orchestration/intent_map.md
- .orchestration/AGENT.md
constraints:
- "`.orchestration/` directory must be machine-managed (not user-edited
directly)."
- "`active_intents.yaml` must be valid YAML and follow the schema defined
in `document.md`."
- "`agent_trace.jsonl` must be append-only (no modifications, only
appends)."
- All file operations must be atomic (write to temp file, then rename).
- Directory and files must be initialized on first use.
acceptance_criteria:
- "`OrchestrationDataModel` class provides methods:"
- "`initialize()`: Creates directory and initializes files if missing."
- "`readActiveIntents()`: Parses and returns active intents."
- "`appendAgentTrace()`: Appends trace entry to JSONL file."
- "`updateIntentMap()`: Updates intent-to-file mapping."
- "`appendAgentKnowledge()`: Appends to AGENT.md."
- All methods handle errors gracefully and log failures.
- Files are created with proper templates if missing.
- YAML parsing validates schema and reports errors clearly.
created_at: 2026-02-18T08:56:57.099Z
updated_at: 2026-02-18T08:56:57.099Z
spec_hash: sha256:659bd435cecc3223171c3fce81f671c26baac9ce354c9a6e3c967164983ed9fb
spec_file: specs/INT-004-orchestration-directory.md
- id: INT-005
name: INT-005 — Logging & Traceability
status: IN_PROGRESS
owned_scope:
- src/core/hooks/HookEngine.ts` (Post-Hook implementation)
- src/core/orchestration/OrchestrationDataModel.ts
- .orchestration/agent_trace.jsonl
- src/utils/git.ts` (for VCS revision tracking)
constraints:
- Trace entries must include `sha256:` content hash of modified code
blocks.
- Line ranges must be best-effort (may be approximate for complex edits).
- "Each trace entry must link to:"
- Intent ID
- File path (relative to workspace root)
- VCS revision (Git SHA)
- Timestamp
- Model identifier
- Content hashing must be spatially independent (same code block = same
hash regardless of file location).
acceptance_criteria:
- Post-hook computes SHA-256 hash of modified content for file tools.
- "Trace entry includes all required fields per `document.md` schema:"
- "`id` (UUID)"
- "`timestamp` (ISO 8601)"
- "`vcs.revision_id` (Git SHA)"
- "`files[]` with `relative_path`, `conversations[]`, `ranges[]`,
`content_hash`"
- Trace entries are appended atomically to `agent_trace.jsonl`.
- "Content hash format: `sha256:<hex>`."
- Git SHA is retrieved from workspace root (handles non-Git repos
gracefully).
created_at: 2026-02-18T08:56:57.099Z
updated_at: 2026-02-18T08:56:57.099Z
spec_hash: sha256:2b3421a22c9e27a817e27aea652f3332cdbc821338b52e112ff654ff88c36317
spec_file: specs/INT-005-logging-traceability.md
- id: INT-006
name: INT-006 — Testing & Validation
status: IN_PROGRESS
owned_scope:
- src/core/hooks/**/*.test.ts
- src/core/orchestration/**/*.test.ts
- src/core/tools/SelectActiveIntentTool.test.ts
- tests/integration/hook-system.test.ts
- tests/e2e/intent-traceability.test.ts
constraints:
- Tests must not modify production `.orchestration/` files (use temp
directories).
- Tests must be deterministic and isolated (no shared state).
- Integration tests must verify hook system works with real tool execution.
- E2E tests must simulate full agent workflow (intent selection → code
change → trace logging).
acceptance_criteria:
- Unit tests for `HookEngine.preHook()` and `HookEngine.postHook()`.
- Unit tests for `OrchestrationDataModel` file operations.
- Unit tests for `SelectActiveIntentTool` intent loading and context
generation.
- "Integration test: Verify Pre-Hook blocks destructive tool without
intent."
- "Integration test: Verify Post-Hook logs trace entry after file write."
- "E2E test: Full workflow from intent selection to trace logging."
- All tests pass in CI/CD pipeline.
- Test coverage > 80% for hook and orchestration modules.
created_at: 2026-02-18T08:56:57.099Z
updated_at: 2026-02-18T08:56:57.100Z
spec_hash: sha256:f8343d2839370244e2f1d7b6622494a96438c86b21f42b98b159eb34e33a59c6
spec_file: specs/INT-006-testing-validation.md
- id: INT-007
name: INT-007 — Documentation & Knowledge Base
status: IN_PROGRESS
owned_scope:
- ARCHITECTURE_NOTES.md
- README.md` (Intent-Code Traceability section)
- .orchestration/AGENT.md
- docs/intent-traceability/
- CHANGELOG.md` (relevant entries)
constraints:
- "`ARCHITECTURE_NOTES.md` must document all injection points and hook
integration."
- '`AGENT.md` must be append-only knowledge base for "Lessons Learned".'
- Documentation must be kept in sync with code changes.
- API documentation must include examples for each public method.
acceptance_criteria:
- "`ARCHITECTURE_NOTES.md` includes:"
- Tool execution flow diagram
- Hook injection points with line numbers
- System prompt modification points
- Data model schemas
- "`AGENT.md` includes:"
- Lessons learned from implementation
- Common pitfalls and solutions
- Performance optimizations
- Stylistic rules for intent specifications
- README includes setup instructions and usage examples.
- All public APIs are documented with JSDoc comments.
- Documentation is reviewed and updated with each major change.
created_at: 2026-02-18T08:56:57.100Z
updated_at: 2026-02-18T08:56:57.100Z
spec_hash: sha256:50566c7bdbddce41c4f739cab9631172321f2880e8cb7a35fdf8c0d1a1aa56b4
spec_file: specs/INT-007-documentation.md

View file

@ -0,0 +1,7 @@
# Agent Trace Ledger (JSONL format - one JSON object per line)
# Append-only, machine-readable history of every mutating action.
# Links abstract Intent to concrete Code Hash for spatial independence.
#
# Example entry:
# {"id":"trace-1234567890-abc","timestamp":"2026-02-16T12:00:00Z","vcs":{"revision_id":"abc123def456"},"files":[{"relative_path":"src/auth/middleware.ts","conversations":[{"url":"task-xyz","contributor":{"entity_type":"AI","model_identifier":"claude-3-5-sonnet"},"ranges":[{"start_line":15,"end_line":45,"content_hash":"sha256:a8f5f167f44f4964e6c998dee827110c"}],"related":[{"type":"intent","value":"INT-001"}]}]}]}

View file

@ -0,0 +1,19 @@
# Intent Map
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.
## Intents
<!--
Example entry:
## INT-001: JWT Authentication Migration
- **Status:** IN_PROGRESS
- **Files:**
- `src/auth/middleware.ts` (lines 15-45)
- `src/middleware/jwt.ts` (entire file)
- **AST Nodes:**
- `JwtAuthMiddleware` class
- `validateToken()` function
- **Last Updated:** 2026-02-16T12:00:00Z
-->

View file

@ -46,6 +46,7 @@ export const toolNames = [
"skill",
"generate_image",
"custom_tool",
"select_active_intent",
] as const
export const toolNamesSchema = z.enum(toolNames)

View file

@ -37,9 +37,11 @@ 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"
import { HookEngine } from "../hooks/HookEngine"
/**
* Processes and presents assistant message content to the user interface.
@ -675,15 +677,45 @@ export async function presentAssistantMessage(cline: Task) {
}
}
// Initialize hook engine for this task
const hookEngine = new HookEngine(cline.cwd)
await hookEngine.initialize()
// Pre-Hook: Intercept tool execution
const preHookResult = await hookEngine.preHook(block.name as ToolName, block, cline)
if (!preHookResult.shouldProceed) {
pushToolResult(formatResponse.toolError(preHookResult.errorMessage || "Tool execution blocked by hook"))
break
}
switch (block.name) {
case "write_to_file":
await checkpointSaveAndMark(cline)
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
case "select_active_intent":
await selectActiveIntentTool.handle(cline, block as ToolUse<"select_active_intent">, {
askApproval,
handleError,
pushToolResult,
})
break
case "write_to_file":
await checkpointSaveAndMark(cline)
let writeSuccess = false
let writeResult: string | undefined
try {
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
askApproval,
handleError,
pushToolResult: (result) => {
writeResult = typeof result === "string" ? result : JSON.stringify(result)
pushToolResult(result)
},
})
writeSuccess = true
} catch (error) {
writeSuccess = false
}
// Post-Hook: Log trace entry
await hookEngine.postHook(block.name as ToolName, block, cline, writeSuccess, writeResult)
break
case "update_todo_list":
await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
askApproval,
@ -718,11 +750,23 @@ export async function presentAssistantMessage(cline: Task) {
break
case "edit_file":
await checkpointSaveAndMark(cline)
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
askApproval,
handleError,
pushToolResult,
})
let editSuccess = false
let editResult: string | undefined
try {
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
askApproval,
handleError,
pushToolResult: (result) => {
editResult = typeof result === "string" ? result : JSON.stringify(result)
pushToolResult(result)
},
})
editSuccess = true
} catch (error) {
editSuccess = false
}
// Post-Hook: Log trace entry
await hookEngine.postHook(block.name as ToolName, block, cline, editSuccess, editResult)
break
case "apply_patch":
await checkpointSaveAndMark(cline)

View file

@ -0,0 +1,339 @@
import { Task } from "../task/Task"
import type { ToolUse, ToolName } from "../../shared/tools"
import { OrchestrationDataModel, type ActiveIntent } from "../orchestration/OrchestrationDataModel"
import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
import { execSync } from "child_process"
/**
* Hook execution result
*/
export interface HookResult {
shouldProceed: boolean
errorMessage?: string
injectedContext?: string
}
/**
* Hook Engine - Middleware boundary for tool execution
* Implements Pre-Hook and Post-Hook interception
*/
export class HookEngine {
private dataModel: OrchestrationDataModel
constructor(workspaceRoot: string) {
this.dataModel = new OrchestrationDataModel(workspaceRoot)
}
/**
* UI-blocking authorization (HITL): pause execution until the user approves/rejects.
*
* We intentionally make this a modal dialog to "pause the Promise chain" as required by `document.md`.
* To avoid excessive prompts, we authorize once per intent per task session.
*/
private async ensureIntentAuthorized(
task: Task,
intent: ActiveIntent,
toolName: ToolName,
details?: { filePath?: string; command?: string },
): Promise<{ approved: boolean; message?: string }> {
const approvedSetKey = "__approvedIntentIds" as const
const approved = ((task as any)[approvedSetKey] ?? []) as string[]
// Authorize once per intent per task session.
if (approved.includes(intent.id)) {
return { approved: true }
}
const detailLines: string[] = []
if (details?.filePath) detailLines.push(`File: ${details.filePath}`)
if (details?.command) detailLines.push(`Command: ${details.command}`)
if (detailLines.length === 0) detailLines.push(`Tool: ${toolName}`)
const message =
`Approve intent evolution for this task?\n\n` +
`Intent: ${intent.id}${intent.name}\n` +
detailLines.join("\n") +
`\n\nThis will allow destructive actions under this intent for the current task session.`
const answer = await vscode.window.showWarningMessage(message, { modal: true }, "Approve", "Reject")
if (answer !== "Approve") {
return { approved: false, message: "User rejected the intent evolution request." }
}
;(task as any)[approvedSetKey] = [...approved, intent.id]
return { approved: true }
}
/**
* Initialize orchestration directory
*/
async initialize(): Promise<void> {
await this.dataModel.initialize()
}
/**
* Pre-Hook: Intercept tool execution before it happens
* Enforces intent context injection and scope validation
*/
async preHook(toolName: ToolName, toolUse: ToolUse, task: Task): Promise<HookResult> {
// Check if this is select_active_intent - allow it through
if (toolName === "select_active_intent") {
return { shouldProceed: true }
}
// For all other tools, check if active intent is set
const activeIntentId = (task as any).activeIntentId as string | undefined
let activeIntent = (task as any).activeIntent as ActiveIntent | undefined
// Destructive tools require intent selection
const destructiveTools: ToolName[] = [
"write_to_file",
"edit_file",
"apply_diff",
"apply_patch",
"edit",
"search_replace",
"search_and_replace",
"execute_command",
]
if (destructiveTools.includes(toolName)) {
if (!activeIntentId) {
return {
shouldProceed: false,
errorMessage:
"You must cite a valid active Intent ID. Call select_active_intent(intent_id) before making code changes.",
}
}
// Load intent details (for authorization prompt + scope checks)
if (!activeIntent) {
activeIntent = await this.dataModel.getIntent(activeIntentId)
if (activeIntent) {
;(task as any).activeIntent = activeIntent
}
}
if (!activeIntent) {
return {
shouldProceed: false,
errorMessage: `Intent "${activeIntentId}" not found in active_intents.yaml. Please select a valid intent ID.`,
}
}
// Validate scope for write operations
if (toolName === "write_to_file" || toolName === "edit_file") {
const filePath = (toolUse.params as any).path as string | undefined
if (filePath) {
const scopeValid = await this.validateScope(activeIntentId, filePath, task.cwd)
if (!scopeValid.valid) {
return {
shouldProceed: false,
errorMessage: `Scope Violation: ${activeIntentId} is not authorized to edit ${filePath}. ${scopeValid.message}`,
}
}
// UI-blocking authorization (HITL)
const auth = await this.ensureIntentAuthorized(task, activeIntent, toolName, { filePath })
if (!auth.approved) {
return {
shouldProceed: false,
errorMessage: auth.message || "Operation rejected by user.",
}
}
}
} else if (toolName === "execute_command") {
const command = (toolUse.params as any).command as string | undefined
const auth = await this.ensureIntentAuthorized(task, activeIntent, toolName, { command })
if (!auth.approved) {
return {
shouldProceed: false,
errorMessage: auth.message || "Operation rejected by user.",
}
}
} else {
// Other destructive tools: still require UI-blocking authorization.
const auth = await this.ensureIntentAuthorized(task, activeIntent, toolName)
if (!auth.approved) {
return {
shouldProceed: false,
errorMessage: auth.message || "Operation rejected by user.",
}
}
}
}
return { shouldProceed: true }
}
/**
* Post-Hook: Execute after tool completes
* Updates trace logs and intent state
*/
async postHook(toolName: ToolName, toolUse: ToolUse, task: Task, success: boolean, result?: string): Promise<void> {
const activeIntentId = (task as any).activeIntentId as string | undefined
// Only log destructive operations
const destructiveTools: ToolName[] = [
"write_to_file",
"edit_file",
"apply_diff",
"apply_patch",
"edit",
"search_replace",
"search_and_replace",
]
if (destructiveTools.includes(toolName) && activeIntentId && success) {
await this.logTraceEntry(toolName, toolUse, task, activeIntentId, result)
}
}
/**
* Validate that a file path is within the intent's owned scope
*/
private async validateScope(
intentId: string,
filePath: string,
workspaceRoot: string,
): Promise<{ valid: boolean; message?: string }> {
try {
const intent = await this.dataModel.getIntent(intentId)
if (!intent) {
return { valid: false, message: "Intent not found" }
}
const normalizedPath = path.normalize(filePath)
const absolutePath = path.resolve(workspaceRoot, normalizedPath)
// Check if file matches any scope pattern
for (const scopePattern of intent.owned_scope) {
// Simple glob matching (can be enhanced with minimatch later)
if (this.matchesPattern(normalizedPath, scopePattern)) {
return { valid: true }
}
}
return {
valid: false,
message: `File is outside intent scope. Request scope expansion or use a different intent.`,
}
} catch (error) {
console.error("Scope validation error:", error)
return { valid: true } // Fail open on error
}
}
/**
* Simple pattern matching (supports ** and *)
*/
private matchesPattern(filePath: string, pattern: string): boolean {
// Convert glob pattern to regex
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/")
const regex = new RegExp(`^${regexPattern}$`)
return regex.test(filePath)
}
/**
* Log trace entry to agent_trace.jsonl
*/
private async logTraceEntry(
toolName: ToolName,
toolUse: ToolUse,
task: Task,
intentId: string,
result?: string,
): Promise<void> {
try {
// Get current git revision
let gitRevision = "unknown"
try {
gitRevision = execSync("git rev-parse HEAD", { cwd: task.cwd, encoding: "utf-8" }).trim()
} catch {
// Git not available or not a git repo
}
// Extract file path and content from tool params
const params = toolUse.params as any
const filePath = params.path as string | undefined
if (!filePath) {
return // Can't log without file path
}
// Read file content to compute hash
const absolutePath = path.resolve(task.cwd, filePath)
let fileContent = ""
let startLine = 1
let endLine = 1
try {
fileContent = await fs.readFile(absolutePath, "utf-8")
const lines = fileContent.split("\n")
endLine = lines.length
// If we have line numbers in params, use them
if (params.start_line !== undefined && params.end_line !== undefined) {
startLine = params.start_line
endLine = params.end_line
}
} catch {
// File doesn't exist or can't be read
return
}
// Extract relevant code block
const lines = fileContent.split("\n")
const relevantLines = lines.slice(Math.max(0, startLine - 1), endLine)
const codeBlock = relevantLines.join("\n")
const contentHash = this.dataModel.computeContentHash(codeBlock)
// Get model identifier from task
const modelId = task.api.getModel().id
// Build trace entry
const traceEntry = {
id: `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
vcs: {
revision_id: gitRevision,
},
files: [
{
relative_path: filePath,
conversations: [
{
url: `task-${task.taskId}`,
contributor: {
entity_type: "AI" as const,
model_identifier: modelId,
},
ranges: [
{
start_line: startLine,
end_line: endLine,
content_hash: `sha256:${contentHash}`,
},
],
related: [
{
type: "intent" as const,
value: intentId,
},
],
},
],
},
],
}
await this.dataModel.appendTraceEntry(traceEntry)
} catch (error) {
console.error("Failed to log trace entry:", error)
// Don't throw - logging failures shouldn't break tool execution
}
}
}

View file

@ -0,0 +1,259 @@
import * as fs from "fs/promises"
import * as path from "path"
import * as crypto from "crypto"
import * as yaml from "yaml"
/**
* Intent specification structure matching the architecture spec
*/
export interface ActiveIntent {
id: string
name: string
status: "TODO" | "IN_PROGRESS" | "DONE" | "BLOCKED"
owned_scope: string[]
constraints: string[]
acceptance_criteria: string[]
created_at?: string
updated_at?: string
}
export interface ActiveIntentsData {
active_intents: ActiveIntent[]
}
/**
* Agent trace entry structure matching the architecture spec
*/
export interface AgentTraceRange {
start_line: number
end_line: number
content_hash: string
}
export interface AgentTraceConversation {
url: string
contributor: {
entity_type: "AI" | "HUMAN"
model_identifier?: string
}
ranges: AgentTraceRange[]
related: Array<{
type: "specification" | "intent" | "requirement"
value: string
}>
}
export interface AgentTraceFile {
relative_path: string
conversations: AgentTraceConversation[]
}
export interface AgentTraceEntry {
id: string
timestamp: string
vcs: {
revision_id: string
}
files: AgentTraceFile[]
}
/**
* Orchestration Data Model
* Manages the .orchestration/ directory and its files
*/
export class OrchestrationDataModel {
private orchestrationDir: string
constructor(workspaceRoot: string) {
this.orchestrationDir = path.join(workspaceRoot, ".orchestration")
}
/**
* Initialize the .orchestration/ directory structure
*/
async initialize(): Promise<void> {
try {
await fs.mkdir(this.orchestrationDir, { recursive: true })
// Initialize active_intents.yaml if it doesn't exist
const intentsPath = path.join(this.orchestrationDir, "active_intents.yaml")
try {
await fs.access(intentsPath)
} catch {
// File doesn't exist, create it
const initialData: ActiveIntentsData = { active_intents: [] }
await fs.writeFile(intentsPath, yaml.stringify(initialData), "utf-8")
}
// Initialize agent_trace.jsonl if it doesn't exist
const tracePath = path.join(this.orchestrationDir, "agent_trace.jsonl")
try {
await fs.access(tracePath)
} catch {
// File doesn't exist, create empty file
await fs.writeFile(tracePath, "", "utf-8")
}
// Initialize intent_map.md if it doesn't exist
const mapPath = path.join(this.orchestrationDir, "intent_map.md")
try {
await fs.access(mapPath)
} catch {
// File doesn't exist, create it with header
const header = `# Intent Map
This file maps high-level business intents to physical files and AST nodes.
## Intents
`
await fs.writeFile(mapPath, header, "utf-8")
}
// Initialize AGENT.md if it doesn't exist
const agentPath = path.join(this.orchestrationDir, "AGENT.md")
try {
await fs.access(agentPath)
} catch {
// File doesn't exist, create it with header
const header = `# Shared Knowledge Base
This file contains persistent knowledge shared across parallel sessions (Architect/Builder/Tester).
## Lessons Learned
`
await fs.writeFile(agentPath, header, "utf-8")
}
} catch (error) {
console.error("Failed to initialize orchestration directory:", error)
throw error
}
}
/**
* Read active intents from YAML file
*/
async readActiveIntents(): Promise<ActiveIntentsData> {
const intentsPath = path.join(this.orchestrationDir, "active_intents.yaml")
try {
const content = await fs.readFile(intentsPath, "utf-8")
return yaml.parse(content) as ActiveIntentsData
} catch (error) {
console.error("Failed to read active_intents.yaml:", error)
return { active_intents: [] }
}
}
/**
* Write active intents to YAML file
*/
async writeActiveIntents(data: ActiveIntentsData): Promise<void> {
const intentsPath = path.join(this.orchestrationDir, "active_intents.yaml")
await fs.writeFile(intentsPath, yaml.stringify(data), "utf-8")
}
/**
* Get a specific intent by ID
*/
async getIntent(intentId: string): Promise<ActiveIntent | null> {
const data = await this.readActiveIntents()
return data.active_intents.find((intent) => intent.id === intentId) || null
}
/**
* Update an intent (create if doesn't exist)
*/
async updateIntent(intent: ActiveIntent): Promise<void> {
const data = await this.readActiveIntents()
const index = data.active_intents.findIndex((i) => i.id === intent.id)
intent.updated_at = new Date().toISOString()
if (!intent.created_at) {
intent.created_at = intent.updated_at
}
if (index >= 0) {
data.active_intents[index] = intent
} else {
data.active_intents.push(intent)
}
await this.writeActiveIntents(data)
}
/**
* Append a trace entry to agent_trace.jsonl
*/
async appendTraceEntry(entry: AgentTraceEntry): Promise<void> {
const tracePath = path.join(this.orchestrationDir, "agent_trace.jsonl")
const line = JSON.stringify(entry) + "\n"
await fs.appendFile(tracePath, line, "utf-8")
}
/**
* Get recent trace entries for a specific intent ID
* Returns the most recent entries (up to limit) that reference this intent
* This is used for Phase 1: Context Loader to provide recent history
*/
async getTraceEntriesForIntent(intentId: string, limit: number = 10): Promise<AgentTraceEntry[]> {
const tracePath = path.join(this.orchestrationDir, "agent_trace.jsonl")
try {
const content = await fs.readFile(tracePath, "utf-8")
const lines = content.trim().split("\n").filter((line) => line.trim() && !line.startsWith("#"))
const entries: AgentTraceEntry[] = []
// Parse each line and filter by intent ID
for (const line of lines) {
try {
const entry = JSON.parse(line) as AgentTraceEntry
// 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,
),
),
)
if (referencesIntent) {
entries.push(entry)
}
} catch (error) {
// Skip invalid JSON lines (comments, etc.)
continue
}
}
// Sort by timestamp (most recent first) and return up to limit
entries.sort((a, b) => {
const timeA = new Date(a.timestamp).getTime()
const timeB = new Date(b.timestamp).getTime()
return timeB - timeA // Descending order (newest first)
})
return entries.slice(0, limit)
} catch (error) {
// File doesn't exist or can't be read - return empty array
console.error("Failed to read agent_trace.jsonl:", error)
return []
}
}
/**
* Compute SHA-256 hash of content for spatial independence
*/
computeContentHash(content: string): string {
return crypto.createHash("sha256").update(content).digest("hex")
}
/**
* Get orchestration directory path
*/
getOrchestrationDir(): string {
return this.orchestrationDir
}
}

View file

@ -1,6 +1,28 @@
export function getToolUseGuidelinesSection(): string {
return `# Tool Use Guidelines
## Intent-Driven Architecture (Reasoning Loop)
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(intent_id) to load the necessary context.
**CRITICAL PROTOCOL:**
1. When the user requests code changes (refactoring, new features, bug fixes), you MUST first:
- Analyze the request to identify which intent it relates to
- Call select_active_intent(intent_id) with a valid intent ID from active_intents.yaml
- Wait for the intent context to be loaded
- Only then proceed with code changes
2. You CANNOT use write_to_file, edit_file, apply_diff, or any other code modification tools without first calling select_active_intent.
3. If you attempt to write code without selecting an intent, the system will block your action and return an error.
4. The intent context will provide you with:
- Owned scope (which files/directories you can modify)
- Constraints (rules you must follow)
- Acceptance criteria (definition of done)
## General Tool Use
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.

View file

@ -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"
@ -47,6 +48,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
}
return [
selectActiveIntent, // Must be first - required before code changes
accessMcpResource,
apply_diff,
applyPatch,

View file

@ -0,0 +1,27 @@
import type OpenAI from "openai"
/**
* Tool for selecting an active intent before making code changes.
* This enforces the Reasoning Loop protocol.
*/
const selectActiveIntent: OpenAI.Chat.ChatCompletionFunctionTool = {
type: "function",
function: {
name: "select_active_intent",
description:
"Select an active intent from active_intents.yaml before making code changes. This is REQUIRED before using any code modification tools (write_to_file, edit_file, etc.). The intent provides context about scope, constraints, and acceptance criteria.",
parameters: {
type: "object",
properties: {
intent_id: {
type: "string",
description:
"The ID of the intent to activate (e.g., 'INT-001'). Must exist in .orchestration/active_intents.yaml",
},
},
required: ["intent_id"],
},
},
}
export default selectActiveIntent

View file

@ -0,0 +1,128 @@
import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
import { OrchestrationDataModel } from "../orchestration/OrchestrationDataModel"
interface SelectActiveIntentParams {
intent_id: string
}
/**
* Tool for selecting an active intent before code changes.
* This enforces the Reasoning Loop: agents must select an intent before writing code.
*/
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 { intent_id } = params
const { pushToolResult, handleError } = callbacks
try {
if (!intent_id) {
task.consecutiveMistakeCount++
task.recordToolError("select_active_intent")
pushToolResult(await task.sayAndCreateMissingParamError("select_active_intent", "intent_id"))
return
}
// Initialize orchestration data model
const dataModel = new OrchestrationDataModel(task.cwd)
await dataModel.initialize()
// Load the intent from active_intents.yaml
const intent = await dataModel.getIntent(intent_id)
if (!intent) {
task.consecutiveMistakeCount++
task.recordToolError("select_active_intent")
pushToolResult(
formatResponse.toolError(
`Intent "${intent_id}" not found in active_intents.yaml. Please use a valid intent ID.`,
),
)
return
}
// Get recent trace entries for this intent (Phase 1 requirement: Context Loader)
// This provides recent history to help the agent understand what has been done
const traceEntries = await dataModel.getTraceEntriesForIntent(intent_id, 5)
// Store active intent in task instance
;(task as any).activeIntentId = intent_id
;(task as any).activeIntent = intent
// Build context XML block for injection into prompt (now includes trace entries)
const contextXml = this.buildIntentContextXml(intent, traceEntries)
// Reset mistake count on success
task.consecutiveMistakeCount = 0
// Return context as tool result (will be injected into next prompt)
pushToolResult(contextXml)
return
} catch (error) {
await handleError("selecting active intent", error as Error)
return
}
}
/**
* Build XML block containing intent context for prompt injection
* Now includes recent trace entries for context (Phase 1: Context Loader)
*/
private buildIntentContextXml(intent: any, traceEntries: any[] = []): string {
const scopeList = intent.owned_scope.map((s: string) => ` - ${s}`).join("\n")
const constraintsList = intent.constraints.map((c: string) => ` - ${c}`).join("\n")
const criteriaList = intent.acceptance_criteria.map((c: string) => ` - ${c}`).join("\n")
// Build recent history section from trace entries
let recentHistorySection = ""
if (traceEntries.length > 0) {
const historyItems = traceEntries.map((entry) => {
const files = entry.files
.map((f: any) => {
const ranges = f.conversations[0]?.ranges?.[0]
if (ranges) {
return ` - ${f.relative_path} (lines ${ranges.start_line}-${ranges.end_line})`
}
return ` - ${f.relative_path}`
})
.join("\n")
const timestamp = new Date(entry.timestamp).toISOString().split("T")[0]
return ` - ${timestamp}: Modified files:\n${files}`
})
recentHistorySection = `<recent_history>
${historyItems.join("\n")}
</recent_history>`
} else {
recentHistorySection = `<recent_history>
No recent changes found for this intent.
</recent_history>`
}
return `<intent_context>
<intent_id>${intent.id}</intent_id>
<intent_name>${intent.name}</intent_name>
<status>${intent.status}</status>
<owned_scope>
${scopeList}
</owned_scope>
<constraints>
${constraintsList}
</constraints>
<acceptance_criteria>
${criteriaList}
</acceptance_criteria>
${recentHistorySection}
</intent_context>`
}
override async handlePartial(task: Task, block: ToolUse<"select_active_intent">): Promise<void> {
// No partial handling needed for intent selection
}
}
export const selectActiveIntentTool = new SelectActiveIntentTool()

View file

@ -265,6 +265,7 @@ export type ToolGroupConfig = {
}
export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
select_active_intent: "select active intent",
execute_command: "run commands",
read_file: "read files",
read_command_output: "read command output",
@ -314,6 +315,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
// Tools that are always available to all modes.
export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"select_active_intent",
"ask_followup_question",
"attempt_completion",
"switch_mode",