diff --git a/src/agents/executor.ts b/src/agents/executor.ts index 92bb2db..c45de79 100644 --- a/src/agents/executor.ts +++ b/src/agents/executor.ts @@ -1,4 +1,21 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js"; +import { execSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +export interface ExecutorResult { + success: boolean; + tasksExecuted: number; + tasksCommitted: number; + testsPassing: boolean; + mustHavesChecked: { name: string; passed: boolean }[]; + error?: string; +} + +interface MustHaveItem { + name: string; + passed: boolean; +} export class ExecutorAgent extends BaseAgent { readonly name = "executor"; @@ -8,21 +25,160 @@ export class ExecutorAgent extends BaseAgent { async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Executing tasks..."); + if (context.backend) { - const result = await this.executeViaBackend( - context, - `Execute implementation for stage ${context.stage}, phase ${context.phase}. Specification: ${context.specification}` - ); - return { ...result, duration_ms: Date.now() - start }; + const taskPrompt = await this.buildBackendTaskPrompt(context); + const backendResult = await this.executeViaBackend(context, taskPrompt); + + const verification = await this.verifyExecution(context); + + return { + ...backendResult, + output: `${backendResult.output}\nVerification: tests=${verification.testsPassing ? "passing" : "failing"}, must-haves checked=${verification.mustHavesChecked.length}`, + duration_ms: Date.now() - start, + }; } + return { success: false, - output: "Execution requires an intelligence backend. Configure one with: ci init --backend", + output: "Executor requires intelligence backend for code implementation", artifacts_created: [], decisions: 0, escalations: 0, duration_ms: Date.now() - start, - error: "No intelligence backend available", + error: "Executor requires intelligence backend for code implementation", }; } + + private async buildBackendTaskPrompt(context: AgentContext): Promise { + const parts: string[] = [ + `Execute implementation for stage ${context.stage}, phase ${context.phase}.`, + "", + "## Specification", + context.specification || "No specification provided", + ]; + + const planContent = this.readPlanFile(context); + if (planContent) { + parts.push("", "## Plan", planContent); + } + + const ciDir = path.join(context.project_path, ".ciagent"); + const roadmapPath = path.join(ciDir, "ROADMAP.md"); + const archPath = path.join(ciDir, "ARCHITECTURE.md"); + + if (fs.existsSync(roadmapPath)) { + try { + const roadmap = fs.readFileSync(roadmapPath, "utf-8"); + parts.push("", "## Roadmap Context", roadmap.slice(0, 2000)); + } catch {} + } + + if (fs.existsSync(archPath)) { + try { + const arch = fs.readFileSync(archPath, "utf-8"); + parts.push("", "## Architecture Boundaries", arch.slice(0, 2000)); + } catch {} + } + + parts.push("", "## Execution Rules"); + parts.push("- Execute one task at a time"); + parts.push("- Commit after each task with ---ci--- block"); + parts.push("- Never pause for checkpoints"); + parts.push("- Create automated verification for traditionally human tasks"); + + return parts.join("\n"); + } + + private readPlanFile(context: AgentContext): string | null { + const planPath = path.join(context.project_path, ".ciagent", "PLAN.md"); + try { + if (fs.existsSync(planPath)) { + return fs.readFileSync(planPath, "utf-8"); + } + } catch {} + return null; + } + + private async verifyExecution(context: AgentContext): Promise { + const mustHavesChecked: MustHaveItem[] = this.checkMustHaves(context); + let testsPassing = false; + let tasksExecuted = 0; + let tasksCommitted = 0; + + try { + const logOutput = execSync("git log --max-count=20 --oneline", { + cwd: context.project_path, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + const commitLines = logOutput.split("\n").filter(Boolean); + tasksCommitted = commitLines.filter((l) => /feat|fix|test/.test(l)).length; + tasksExecuted = tasksCommitted; + } catch {} + + try { + execSync("npm test", { + cwd: context.project_path, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 120000, + }); + testsPassing = true; + } catch { + testsPassing = false; + } + + return { + success: mustHavesChecked.every((m) => m.passed) && testsPassing, + tasksExecuted, + tasksCommitted, + testsPassing, + mustHavesChecked, + }; + } + + private checkMustHaves(context: AgentContext): MustHaveItem[] { + const planPath = path.join(context.project_path, ".ciagent", "PLAN.md"); + const results: MustHaveItem[] = []; + + try { + if (!fs.existsSync(planPath)) return results; + const planContent = fs.readFileSync(planPath, "utf-8"); + const mustHaveRegex = /-\s*\[x\]\s*(.+)/g; + let match; + while ((match = mustHaveRegex.exec(planContent)) !== null) { + const name = match[1].trim(); + const passed = this.verifyMustHaveItem(name, context); + results.push({ name, passed }); + } + } catch {} + + return results; + } + + private verifyMustHaveItem(item: string, context: AgentContext): boolean { + const fileMatch = item.match(/(?:exists|created?|present).*?[\s:]+([^\s]+\.(ts|js|json|md))/i); + if (fileMatch) { + const filePath = path.join(context.project_path, fileMatch[1]); + return fs.existsSync(filePath); + } + + const testMatch = item.match(/(?:test|tests?)\s+(?:pass|passing)/i); + if (testMatch) { + try { + execSync("npm test", { + cwd: context.project_path, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 120000, + }); + return true; + } catch { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/src/agents/index.ts b/src/agents/index.ts index 5cbd9b7..b8cf692 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -1,9 +1,9 @@ export { BaseAgent, AgentContext, AgentResult, backendResultToAgentResult } from "./base.js"; export { OrchestratorAgent } from "./orchestrator.js"; -export { PlannerAgent } from "./planner.js"; -export { ExecutorAgent } from "./executor.js"; -export { VerifierAgent } from "./verifier.js"; -export { ResearcherAgent } from "./researcher.js"; +export { PlannerAgent, PlannerResult } from "./planner.js"; +export { ExecutorAgent, ExecutorResult } from "./executor.js"; +export { VerifierAgent, VerifierResult } from "./verifier.js"; +export { ResearcherAgent, ResearcherResult } from "./researcher.js"; export { ChallengerAgent } from "./challenger.js"; export { SecurityAuditorAgent } from "./security-auditor.js"; export { DebuggerAgent } from "./debugger.js"; @@ -17,7 +17,7 @@ export { ProjectResearcherAgent } from "./project-researcher.js"; export { ResearchSynthesizerAgent } from "./research-synthesizer.js"; export { SolutionWriterAgent } from "./solution-writer.js"; export { PhaseResearcherAgent } from "./phase-researcher.js"; -export { TesterAgent } from "./tester.js"; +export { TesterAgent, TesterResult } from "./tester.js"; import { AgentName } from "../types/config.js"; import { BaseAgent as BaseAgentType } from "./base.js"; diff --git a/src/agents/planner.ts b/src/agents/planner.ts index 86470e1..e22ca36 100644 --- a/src/agents/planner.ts +++ b/src/agents/planner.ts @@ -1,4 +1,27 @@ import { BaseAgent, AgentContext, AgentResult } from "./base.js"; +import { CIAgentFiles, RequirementsMd, RoadmapMd, ArchitectureMd } from "../core/ciagent-files.js"; +import { GitContext } from "../core/git-context.js"; +import { CommitBuilder } from "../core/commit-builder.js"; +import { writeFile, readFile, ensureDir } from "../utils/file.js"; +import { execSync } from "node:child_process"; +import * as path from "node:path"; + +export interface PlannerResult { + success: boolean; + planCount: number; + waves: { wave: number; plans: string[] }[]; + decisions: number; + error?: string; +} + +interface PlanEntry { + name: string; + wave: number; + requirements: string[]; + dependsOn: string[]; + tasks: string[]; + mustHaves: string[]; +} export class PlannerAgent extends BaseAgent { readonly name = "planner"; @@ -8,21 +31,306 @@ export class PlannerAgent extends BaseAgent { async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Creating phase plan..."); + if (context.backend) { - const result = await this.executeViaBackend( - context, - `Create a phase plan for stage ${context.stage}, phase ${context.phase}. Specification: ${context.specification}` - ); + const taskPrompt = await this.buildBackendTaskPrompt(context); + const result = await this.executeViaBackend(context, taskPrompt); return { ...result, duration_ms: Date.now() - start }; } + + return this.executeMechanical(context, start); + } + + private async buildBackendTaskPrompt(context: AgentContext): Promise { + const ciFiles = new CIAgentFiles(context.project_path); + const parts: string[] = [ + `Create a phase plan for stage ${context.stage}, phase ${context.phase}.`, + "", + "## Project Context", + ]; + + const roadmap = ciFiles.readRoadmapMd(); + if (roadmap) { + const currentPhase = roadmap.phases.find((p) => p.number === context.phase); + if (currentPhase) { + parts.push("", "### Phase Goal", currentPhase.description); + parts.push("", "### Phase Requirements", currentPhase.requirements.join(", ") || "None specified"); + parts.push("", "### Phase Dependencies", currentPhase.dependsOn.length > 0 ? currentPhase.dependsOn.map((d) => `Phase ${d}`).join(", ") : "None"); + parts.push("", "### Success Criteria", ...currentPhase.successCriteria.map((sc) => `- ${sc}`)); + } + } + + const requirements = ciFiles.readRequirementsMd(); + if (requirements) { + const phaseReqs = requirements.traceability.filter((t) => t.phase === context.phase); + if (phaseReqs.length > 0) { + parts.push("", "### Requirements for Phase", ...phaseReqs.map((t) => `- ${t.requirement} (${t.status})`)); + } + } + + const architecture = ciFiles.readArchitectureMd(); + if (architecture) { + parts.push("", "### Architecture Boundaries", ...architecture.components.map((c) => `- ${c.name}: ${c.boundaries}`)); + parts.push("", "### Build Order", ...architecture.buildOrder.map((bo) => `${bo}`)); + } + + parts.push("", "## Specification", context.specification || "No specification provided"); + + return parts.join("\n"); + } + + private executeMechanical(context: AgentContext, start: number): AgentResult { + const ciFiles = new CIAgentFiles(context.project_path); + ciFiles.ensureCIDir(); + + const requirements = ciFiles.readRequirementsMd(); + const roadmap = ciFiles.readRoadmapMd(); + const architecture = ciFiles.readArchitectureMd(); + + if (!requirements && !roadmap) { + return { + success: false, + output: "Planning requires either .ciagent/REQUIREMENTS.md or .ciagent/ROADMAP.md. Initialize the project first.", + artifacts_created: [], + decisions: 0, + escalations: 0, + duration_ms: Date.now() - start, + error: "No requirements or roadmap found for mechanical planning", + }; + } + + let gitLogSummary = ""; + try { + gitLogSummary = execSync("git log --max-count=20 --oneline", { + cwd: context.project_path, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + } catch { + gitLogSummary = "(no git history available)"; + } + + const phaseGoal = this.extractPhaseGoal(roadmap, context.phase); + const phaseRequirements = this.extractPhaseRequirements(requirements, context.phase); + const componentBoundaries = architecture ? architecture.components.map((c) => c.name) : []; + + const plans = this.buildPlans(phaseRequirements, componentBoundaries, context.phase); + + const planFileContent = this.formatPlanFile(context.phase, phaseGoal, plans); + + const planFilePath = path.join(context.project_path, ".ciagent", "PLAN.md"); + ensureDir(path.dirname(planFilePath)); + writeFile(planFilePath, planFileContent); + + const decisionCount = plans.length > 0 ? 1 : 0; + + if (this.shouldCommit(context)) { + try { + const commitMessage = CommitBuilder.buildTaskCommit({ + type: "docs", + phase: context.phase, + milestone: "v1.0", + plan: "01", + task: "01-01", + subject: `create ${plans.length} phase plans`, + status: "plan", + decisions: decisionCount > 0 ? [{ + id: "D-001", + decision: `Decomposed phase ${context.phase} into ${plans.length} vertical-slice plans`, + rationale: "Requirements grouped by dependency analysis — independent requirements in wave 1, dependent in wave 2+", + confidence: 0.75, + alternatives: ["single monolithic plan", "per-requirement plans"], + }] : undefined, + }); + execSync(`git add -A && git commit -m "${commitMessage.replace(/"/g, '\\"')}" --allow-empty`, { + cwd: context.project_path, + stdio: "pipe", + }); + } catch { + this.warn("Plan commit failed"); + } + } + + const waves = this.groupPlansByWave(plans); + const plannerResult: PlannerResult = { + success: true, + planCount: plans.length, + waves, + decisions: decisionCount, + }; + return { - success: false, - output: "Planning requires an intelligence backend. Configure one with: ci init --backend", - artifacts_created: [], - decisions: 0, + success: true, + output: `Created ${plans.length} plan(s) across ${waves.length} wave(s) for phase ${context.phase}`, + artifacts_created: [".ciagent/PLAN.md"], + decisions: decisionCount, escalations: 0, duration_ms: Date.now() - start, - error: "No intelligence backend available", }; } + + private extractPhaseGoal(roadmap: RoadmapMd | null, phase: number): string { + if (!roadmap) return "No roadmap available"; + const phaseEntry = roadmap.phases.find((p) => p.number === phase); + if (phaseEntry) return `${phaseEntry.name}: ${phaseEntry.description}`; + return `Phase ${phase} (no roadmap entry)`; + } + + private extractPhaseRequirements(requirements: RequirementsMd | null, phase: number): Array<{ id: string; description: string; phase: number; status: string }> { + if (!requirements) return []; + return requirements.traceability + .filter((t) => t.phase === phase) + .map((t) => { + let description = t.requirement; + for (const cat of [...requirements.v1, ...requirements.v2]) { + const item = cat.items.find((i) => i.id === t.requirement); + if (item) { + description = `${t.requirement}: ${item.description}`; + break; + } + } + return { id: t.requirement, description, phase: t.phase, status: t.status }; + }); + } + + private buildPlans( + phaseRequirements: Array<{ id: string; description: string; phase: number; status: string }>, + componentBoundaries: string[], + phase: number + ): PlanEntry[] { + if (phaseRequirements.length === 0) { + return [{ + name: `Phase ${phase} Core Implementation`, + wave: 1, + requirements: [], + dependsOn: [], + tasks: [`Implement phase ${phase} deliverables as specified in ROADMAP.md`], + mustHaves: [`Phase ${phase} deliverables exist and pass verification`], + }]; + } + + const independentReqs = phaseRequirements.filter((r) => r.status !== "blocked"); + const blockedReqs = phaseRequirements.filter((r) => r.status === "blocked"); + + const plans: PlanEntry[] = []; + + if (independentReqs.length > 0) { + const taskChunks = this.chunkByComponent(independentReqs, componentBoundaries); + for (const chunk of taskChunks) { + plans.push({ + name: this.inferPlanName(chunk, phase), + wave: 1, + requirements: chunk.map((r) => r.id), + dependsOn: [], + tasks: chunk.map((r) => `Implement ${r.id}: ${r.description.split(": ").slice(1).join(": ") || r.description}`), + mustHaves: chunk.map((r) => `${r.id} implemented and testable`), + }); + } + } + + if (blockedReqs.length > 0) { + const taskChunks = this.chunkByComponent(blockedReqs, componentBoundaries); + for (const chunk of taskChunks) { + plans.push({ + name: this.inferPlanName(chunk, phase), + wave: plans.length > 0 ? Math.max(...plans.map((p) => p.wave)) + 1 : 2, + requirements: chunk.map((r) => r.id), + dependsOn: plans.slice(0, plans.length > 0 ? 1 : 0).map((p) => p.name), + tasks: chunk.map((r) => `Implement ${r.id}: ${r.description.split(": ").slice(1).join(": ") || r.description}`), + mustHaves: chunk.map((r) => `${r.id} implemented and testable`), + }); + } + } + + if (plans.length === 0) { + plans.push({ + name: `Phase ${phase} Default`, + wave: 1, + requirements: [], + dependsOn: [], + tasks: [`Implement phase ${phase} deliverables`], + mustHaves: [`Phase ${phase} deliverables pass verification`], + }); + } + + return plans; + } + + private chunkByComponent( + reqs: Array<{ id: string; description: string; phase: number; status: string }>, + _componentBoundaries: string[] + ): Array> { + if (reqs.length <= 3) return [reqs]; + const chunks: Array> = []; + const chunkSize = Math.ceil(reqs.length / Math.ceil(reqs.length / 3)); + for (let i = 0; i < reqs.length; i += chunkSize) { + chunks.push(reqs.slice(i, i + chunkSize)); + } + return chunks; + } + + private inferPlanName(chunk: Array<{ id: string; description: string; phase: number; status: string }>, phase: number): string { + if (chunk.length === 1) return `Phase ${phase}: ${chunk[0].id}`; + return `Phase ${phase}: ${chunk[0].id}–${chunk[chunk.length - 1].id}`; + } + + private groupPlansByWave(plans: PlanEntry[]): { wave: number; plans: string[] }[] { + const waveMap = new Map(); + for (const plan of plans) { + const existing = waveMap.get(plan.wave) || []; + existing.push(plan.name); + waveMap.set(plan.wave, existing); + } + return Array.from(waveMap.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([wave, names]) => ({ wave, plans: names })); + } + + private formatPlanFile(phase: number, phaseGoal: string, plans: PlanEntry[]): string { + const lines: string[] = [ + `# Phase ${phase} Plan`, + "", + "## Phase Goal", + phaseGoal, + "", + "## Plans", + "", + ]; + + for (let i = 0; i < plans.length; i++) { + const plan = plans[i]; + const planNum = i + 1; + lines.push(`### Plan ${planNum}: ${plan.name}`); + lines.push(`- Wave: ${plan.wave}`); + if (plan.requirements.length > 0) { + lines.push(`- Requirements: [${plan.requirements.join(", ")}]`); + } + if (plan.dependsOn.length > 0) { + lines.push(`- Depends on: ${plan.dependsOn.join(", ")}`); + } + lines.push("- Tasks:"); + for (const task of plan.tasks) { + lines.push(` 1. ${task}`); + } + lines.push("- Must-haves:"); + for (const mh of plan.mustHaves) { + lines.push(` - [x] ${mh}`); + } + lines.push(""); + } + + return lines.join("\n"); + } + + private shouldCommit(context: AgentContext): boolean { + try { + execSync("git rev-parse --is-inside-work-tree", { + cwd: context.project_path, + stdio: "pipe", + }); + return true; + } catch { + return false; + } + } } \ No newline at end of file diff --git a/src/agents/researcher.ts b/src/agents/researcher.ts index 5e2559e..c71974b 100644 --- a/src/agents/researcher.ts +++ b/src/agents/researcher.ts @@ -1,4 +1,20 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; +import { GitContext } from "../core/git-context.js"; +import { CIAgentFiles, ArchitectureMd, ProjectMd } from "../core/ciagent-files.js"; +import { CommitBuilder } from "../core/commit-builder.js"; +import { CommitDecision } from "../types/commit-meta.js"; +import { fileExists, readFile } from "../utils/file.js"; +import { execSync } from "node:child_process"; + +export interface ResearcherResult { + success: boolean; + findingsCount: number; + decisionsLogged: number; + filesUpdated: string[]; + error?: string; +} export class ResearcherAgent extends BaseAgent { readonly name = "researcher"; @@ -8,21 +24,239 @@ export class ResearcherAgent extends BaseAgent { async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Researching domain..."); + if (context.backend) { const result = await this.executeViaBackend( context, - `Research the domain for: ${context.specification}` + `Research the domain for phase ${context.phase}. Specification: ${context.specification}. Read git history (last 50 commits), .ciagent/PROJECT.md, .ciagent/ARCHITECTURE.md, .ciagent/REQUIREMENTS.md. Scan src/ directory structure. Generate findings about module boundaries, risks, and approach. Update .ciagent/ARCHITECTURE.md with component boundary conclusions. Update .ciagent/PROJECT.md key decisions if warranted. Commit findings with CommitBuilder.buildResearchCommit().` ); return { ...result, duration_ms: Date.now() - start }; } + + const result = await this.runMechanicalResearch(context); + const output = JSON.stringify(result, null, 2); + return { - success: false, - output: "Research requires an intelligence backend. Configure one with: ci init --backend", - artifacts_created: [], - decisions: 0, + success: result.success, + output, + artifacts_created: result.filesUpdated, + decisions: result.decisionsLogged, escalations: 0, duration_ms: Date.now() - start, - error: "No intelligence backend available", + error: result.error, }; } + + private async runMechanicalResearch(context: AgentContext): Promise { + try { + const gitContext = new GitContext(context.project_path); + const ciFiles = new CIAgentFiles(context.project_path); + + const findings: string[] = []; + const decisions: CommitDecision[] = []; + const filesUpdated: string[] = []; + + const commits = gitContext.getRecentCommits(50); + if (commits.length > 0) { + findings.push(`Analyzed ${commits.length} recent commits for project history`); + const researchCommits = commits.filter(c => c.ci?.status === "research"); + if (researchCommits.length > 0) { + findings.push(`Found ${researchCommits.length} prior research commits`); + } + } + + const projectMd = ciFiles.readProjectMd(); + if (projectMd) { + findings.push(`Project: ${projectMd.name} — core value: ${projectMd.coreValue.slice(0, 80)}`); + findings.push(`Active requirements: ${projectMd.requirements.active.length}, validated: ${projectMd.requirements.validated.length}`); + } else { + findings.push("No PROJECT.md found — project context unavailable"); + } + + const archMd = ciFiles.readArchitectureMd(); + if (archMd) { + findings.push(`Architecture: ${archMd.components.length} components, ${archMd.buildOrder.length} build steps`); + for (const comp of archMd.components) { + findings.push(` Component: ${comp.name} — boundaries: ${comp.boundaries.slice(0, 60)}, deps: ${comp.dependsOn.join(", ") || "none"}`); + } + } else { + findings.push("No ARCHITECTURE.md found — architecture analysis unavailable"); + } + + const reqsMd = ciFiles.readRequirementsMd(); + if (reqsMd) { + const totalReqs = reqsMd.traceability.length; + const covered = reqsMd.traceability.filter(t => t.status === "complete").length; + const phaseReqs = reqsMd.traceability.filter(t => t.phase === context.phase); + findings.push(`Requirements: ${totalReqs} total, ${covered} complete, ${phaseReqs.length} for phase ${context.phase}`); + } + + const srcDir = path.join(context.project_path, "src"); + if (fs.existsSync(srcDir)) { + const moduleDirs = fs.readdirSync(srcDir, { withFileTypes: true }) + .filter(d => d.isDirectory() && d.name !== "node_modules") + .map(d => d.name); + findings.push(`Source modules: ${moduleDirs.join(", ")}`); + + const updatedArch = this.deriveArchitectureFromSource(srcDir, archMd, moduleDirs); + if (updatedArch) { + ciFiles.writeArchitectureMd(updatedArch); + filesUpdated.push(".ciagent/ARCHITECTURE.md"); + findings.push("Updated ARCHITECTURE.md with source-derived component boundaries"); + + decisions.push({ + id: `D-P${context.phase}-001`, + decision: "Updated component boundaries from source scan", + rationale: "Source directory structure reveals actual module boundaries", + confidence: 0.75, + alternatives: ["manual architecture review", "no update"], + }); + } + } + + if (projectMd && archMd) { + const updatedProject = this.maybeUpdateKeyDecisions(projectMd, findings); + if (updatedProject) { + ciFiles.writeProjectMd(updatedProject, "research findings update"); + filesUpdated.push(".ciagent/PROJECT.md"); + findings.push("Updated PROJECT.md key decisions from research"); + + decisions.push({ + id: `D-P${context.phase}-002`, + decision: "Logged research-based decisions to PROJECT.md", + rationale: "Research findings warrant recording as key decisions", + confidence: 0.70, + alternatives: ["defer decision logging", "log after execution"], + }); + } + } + + this.commitFindings(context, findings, decisions); + + return { + success: true, + findingsCount: findings.length, + decisionsLogged: decisions.length, + filesUpdated, + }; + } catch (err) { + return { + success: false, + findingsCount: 0, + decisionsLogged: 0, + filesUpdated: [], + error: `Research failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } + } + + private deriveArchitectureFromSource(srcDir: string, existing: ArchitectureMd | null, moduleDirs: string[]): ArchitectureMd | null { + const newComponents = moduleDirs.map(dir => { + const dirPath = path.join(srcDir, dir); + const fileCount = this.countTsFiles(dirPath); + const existingComp = existing?.components.find(c => c.name.toLowerCase() === dir.toLowerCase()); + + return { + name: existingComp?.name || this.capitalize(dir), + description: existingComp?.description || `${dir} module with ${fileCount} source files`, + boundaries: existingComp?.boundaries || `src/${dir}/ — ${fileCount} files, internal module`, + dependsOn: existingComp?.dependsOn || [], + }; + }); + + if (existing) { + const existingNames = new Set(existing.components.map(c => c.name.toLowerCase())); + const hasNew = newComponents.some(c => !existingNames.has(c.name.toLowerCase())); + if (!hasNew) { + return { + ...existing, + components: existing.components.map(comp => { + const updated = newComponents.find(n => n.name.toLowerCase() === comp.name.toLowerCase()); + return updated || comp; + }), + }; + } + + const merged = [...existing.components]; + for (const nc of newComponents) { + if (!existingNames.has(nc.name.toLowerCase())) { + merged.push(nc); + } + } + return { ...existing, components: merged }; + } + + return { + overview: "Architecture derived from source directory scan", + components: newComponents, + dataFlow: "Modules communicate via typed interfaces and shared utilities", + buildOrder: moduleDirs.map(d => `Build ${d} module`), + }; + } + + private maybeUpdateKeyDecisions(projectMd: ProjectMd, findings: string[]): ProjectMd | null { + const researchDecisions = findings + .filter(f => f.includes("Updated") || f.includes("Found") || f.includes("derived")) + .map(f => ({ + decision: f.slice(0, 50), + rationale: "Derived from mechanical source analysis", + outcome: "logged by researcher", + })); + + if (researchDecisions.length === 0) return null; + + const existingDecisions = projectMd.keyDecisions || []; + const existingDecisionTexts = new Set(existingDecisions.map(d => d.decision)); + + const novelDecisions = researchDecisions.filter(d => !existingDecisionTexts.has(d.decision)); + if (novelDecisions.length === 0) return null; + + return { + ...projectMd, + keyDecisions: [...existingDecisions, ...novelDecisions], + }; + } + + private commitFindings(context: AgentContext, findings: string[], decisions: CommitDecision[]): void { + try { + const gitContext = new GitContext(context.project_path); + const projectState = gitContext.reconstructState(); + const milestone = projectState.currentMilestone || "v1.0"; + + const commitMsg = CommitBuilder.buildResearchCommit( + context.phase, + milestone, + `phase ${context.phase} domain research`, + findings, + decisions.length > 0 ? decisions : undefined, + ); + + if (fileExists(path.join(context.project_path, ".git"))) { + execSync(`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" --allow-empty`, { + cwd: context.project_path, + stdio: "pipe", + }); + } + } catch (err) { + this.warn(`Research commit failed: ${err instanceof Error ? err.message : String(err)}`); + } + } + + private countTsFiles(dir: string): number { + if (!fs.existsSync(dir)) return 0; + let count = 0; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name !== "node_modules") { + count += this.countTsFiles(path.join(dir, entry.name)); + } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) { + count++; + } + } + return count; + } + + private capitalize(s: string): string { + return s.split("-").map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("-"); + } } \ No newline at end of file diff --git a/src/agents/tester.ts b/src/agents/tester.ts index cccb0a3..cdc7c00 100644 --- a/src/agents/tester.ts +++ b/src/agents/tester.ts @@ -1,28 +1,181 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; +import { execSync } from "node:child_process"; + +export interface TesterResult { + success: boolean; + integrationTestsFound: number; + integrationTestsPassed: number; + e2eTestsFound: number; + e2eTestsPassed: number; + overallPassed: boolean; + error?: string; +} export class TesterAgent extends BaseAgent { readonly name = "tester"; - readonly description = "Runs automated tests and validates test coverage."; + readonly description = "Runs integration, e2e, functional tests. Validates non-unit test coverage."; readonly workflow = "test"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Running automated tests..."); + if (context.backend) { const result = await this.executeViaBackend( context, - `Run automated tests for: ${context.specification}` + `Run integration, e2e, and functional tests for phase ${context.phase}. Specification: ${context.specification}. Detect *.integration.test.ts, *.e2e.test.ts, *.functional.test.ts files. Run npm test. Parse output for pass/fail counts per category. Report structured TesterResult. Do NOT write any test files — only detect and run existing ones.` ); return { ...result, duration_ms: Date.now() - start }; } + + const result = await this.runMechanicalTests(context); + const output = JSON.stringify(result, null, 2); + return { - success: false, - output: "Testing requires an intelligence backend.", + success: result.success, + output, artifacts_created: [], decisions: 0, - escalations: 0, + escalations: result.overallPassed ? 0 : 1, duration_ms: Date.now() - start, - error: "No intelligence backend available", + error: result.error, }; } + + private async runMechanicalTests(context: AgentContext): Promise { + try { + const srcDir = path.join(context.project_path, "src"); + const integrationFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.integration\.test\.ts$/) : []; + const e2eFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.e2e\.test\.ts$/) : []; + const functionalFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.functional\.test\.ts$/) : []; + + const integrationTestsFound = integrationFiles.length; + const e2eTestsFound = e2eFiles.length + functionalFiles.length; + + let overallPassed = false; + let integrationTestsPassed = 0; + let e2eTestsPassed = 0; + + try { + const testOutput = execSync("npm test 2>&1", { + cwd: context.project_path, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 120000, + }); + overallPassed = true; + + const passCounts = this.parseTestOutput(testOutput); + integrationTestsPassed = integrationTestsFound > 0 ? integrationTestsFound : 0; + e2eTestsPassed = e2eTestsFound > 0 ? e2eTestsFound : 0; + + if (integrationTestsFound > 0) { + integrationTestsPassed = this.estimateCategoryPassed(testOutput, "integration"); + } + if (e2eTestsFound > 0) { + e2eTestsPassed = this.estimateCategoryPassed(testOutput, "e2e"); + } + } catch (err) { + const output = err instanceof Error && "stdout" in err + ? (err as unknown as { stdout: string }).stdout || "" + : ""; + const stderr = err instanceof Error && "stderr" in err + ? (err as unknown as { stderr: string }).stderr || "" + : ""; + + const combined = `${output}\n${stderr}`; + overallPassed = false; + + const passCounts = this.parseTestOutput(combined); + + if (integrationTestsFound > 0) { + integrationTestsPassed = this.estimateCategoryPassed(combined, "integration"); + } + if (e2eTestsFound > 0) { + e2eTestsPassed = this.estimateCategoryPassed(combined, "e2e"); + } + + return { + success: false, + integrationTestsFound, + integrationTestsPassed, + e2eTestsFound, + e2eTestsPassed, + overallPassed: false, + error: `npm test failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } + + return { + success: overallPassed, + integrationTestsFound, + integrationTestsPassed, + e2eTestsFound, + e2eTestsPassed, + overallPassed, + }; + } catch (err) { + return { + success: false, + integrationTestsFound: 0, + integrationTestsPassed: 0, + e2eTestsFound: 0, + e2eTestsPassed: 0, + overallPassed: false, + error: `Test execution failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } + } + + private findTestFiles(dir: string, pattern: RegExp): string[] { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && entry.name !== "node_modules") { + files.push(...this.findTestFiles(fullPath, pattern)); + } else if (pattern.test(entry.name)) { + files.push(fullPath); + } + } + return files; + } + + private parseTestOutput(output: string): { total: number; passed: number; failed: number } { + const jestSummary = output.match(/Tests:\s+(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/); + if (jestSummary) { + const passed = parseInt(jestSummary[1], 10) || 0; + const failed = parseInt(jestSummary[2], 10) || 0; + return { total: passed + failed, passed, failed }; + } + + const jestAlt = output.match(/(\d+)\s+passing/); + const jestAltFail = output.match(/(\d+)\s+failing/); + if (jestAlt) { + const passed = parseInt(jestAlt[1], 10) || 0; + const failed = jestAltFail ? parseInt(jestAltFail[1], 10) || 0 : 0; + return { total: passed + failed, passed, failed }; + } + + return { total: 0, passed: 0, failed: 0 }; + } + + private estimateCategoryPassed(output: string, category: string): number { + const categoryPattern = category === "integration" + ? /\.integration\.test\.ts/g + : /\.e2e\.test\.ts|\.functional\.test\.ts/g; + + const mentions = (output.match(categoryPattern) || []).length; + if (mentions > 0) { + const failPattern = /FAIL|failed|error/i; + const lines = output.split("\n").filter(l => categoryPattern.test(l)); + const failed = lines.filter(l => failPattern.test(l)).length; + return Math.max(mentions - failed, 0); + } + + return 0; + } } \ No newline at end of file diff --git a/src/agents/verifier.ts b/src/agents/verifier.ts index bfb712b..142e763 100644 --- a/src/agents/verifier.ts +++ b/src/agents/verifier.ts @@ -1,4 +1,22 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; +import { VerificationPipeline } from "../verification/index.js"; +import { CommitBuilder, VerifyCommitInput } from "../core/commit-builder.js"; +import { GitContext } from "../core/git-context.js"; +import { CIAgentFiles } from "../core/ciagent-files.js"; +import { fileExists } from "../utils/file.js"; +import { execSync } from "node:child_process"; + +export interface VerifierResult { + success: boolean; + mustHaveScore: number; + requirementsCovered: string[]; + requirementsPartial: string[]; + integrationChecks: { import: string; resolved: boolean }[]; + layers: { name: string; passed: boolean }[]; + error?: string; +} export class VerifierAgent extends BaseAgent { readonly name = "verifier"; @@ -8,21 +26,215 @@ export class VerifierAgent extends BaseAgent { async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Verifying phase output..."); + if (context.backend) { const result = await this.executeViaBackend( context, - `Verify phase ${context.phase} output. Specification: ${context.specification}` + `Verify phase ${context.phase} output against must-haves, requirement coverage, and integration links. Specification: ${context.specification}. Check all .ciagent/ reference files. Run the 4-layer verification pipeline (structural, behavioral, security, quality). Verify imports resolve. Report structured VerifierResult.` ); return { ...result, duration_ms: Date.now() - start }; } + + const result = await this.runMechanicalVerification(context); + const output = JSON.stringify(result, null, 2); + return { - success: false, - output: "Verification requires an intelligence backend. Configure one with: ci init --backend", + success: result.success, + output, artifacts_created: [], decisions: 0, - escalations: 0, + escalations: result.success ? 0 : 1, duration_ms: Date.now() - start, - error: "No intelligence backend available", + error: result.error, }; } + + private async runMechanicalVerification(context: AgentContext): Promise { + try { + const pipeline = new VerificationPipeline(context.project_path); + const pipelineResult = await pipeline.run(context.phase); + + const layers: { name: string; passed: boolean }[] = [ + { name: pipelineResult.structural.name, passed: pipelineResult.structural.passed }, + { name: pipelineResult.behavioral.name, passed: pipelineResult.behavioral.passed }, + { name: pipelineResult.security.name, passed: pipelineResult.security.passed }, + { name: pipelineResult.quality.name, passed: pipelineResult.quality.passed }, + ]; + + const gitContext = new GitContext(context.project_path); + const ciFiles = new CIAgentFiles(context.project_path); + + const mustHaveScore = this.checkMustHaves(context, gitContext, ciFiles); + const reqCoverage = this.checkRequirementCoverage(gitContext, ciFiles); + const integrationChecks = this.checkIntegrationLinks(context.project_path); + + const allPassed = pipelineResult.all_passed && + mustHaveScore >= 1.0 && + reqCoverage.partial.length === 0; + + const result: VerifierResult = { + success: allPassed, + mustHaveScore, + requirementsCovered: reqCoverage.covered, + requirementsPartial: reqCoverage.partial, + integrationChecks, + layers, + }; + + if (!allPassed) { + result.error = `Verification gaps: mustHaveScore=${mustHaveScore}, partialReqs=${reqCoverage.partial.join(",")}, layerFailures=${layers.filter(l => !l.passed).map(l => l.name).join(",")}`; + } + + this.commitVerificationResult(context, result, ciFiles); + + return result; + } catch (err) { + return { + success: false, + mustHaveScore: 0, + requirementsCovered: [], + requirementsPartial: [], + integrationChecks: [], + layers: [], + error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } + } + + private checkMustHaves(context: AgentContext, gitContext: GitContext, ciFiles: CIAgentFiles): number { + const roadmap = ciFiles.readRoadmapMd(); + if (!roadmap) return 0; + + const currentPhase = roadmap.phases.find(p => p.number === context.phase); + if (!currentPhase) return 0; + + const successCriteria = currentPhase.successCriteria; + if (successCriteria.length === 0) return 1; + + let passing = 0; + for (const criterion of successCriteria) { + const fileHint = criterion.match(/(?:file|exists|present|created|written)[:\s]+([^\s,;]+)/i); + if (fileHint) { + const candidate = path.join(context.project_path, fileHint[1]); + if (fileExists(candidate)) { + passing++; + continue; + } + } + + if (fileExists(path.join(context.project_path, ".ciagent"))) { + passing++; + } + } + + return Math.min(passing / successCriteria.length, 1); + } + + private checkRequirementCoverage(gitContext: GitContext, ciFiles: CIAgentFiles): { covered: string[]; partial: string[] } { + const gitCoverage = gitContext.getRequirementsCoverage(); + const reqsMd = ciFiles.readRequirementsMd(); + + if (!reqsMd || reqsMd.traceability.length === 0) { + return { covered: gitCoverage.covered, partial: gitCoverage.partial }; + } + + const covered = new Set(gitCoverage.covered); + const partial = new Set(gitCoverage.partial); + + for (const t of reqsMd.traceability) { + if (t.status === "complete") { + covered.add(t.requirement); + partial.delete(t.requirement); + } else if (t.status === "in_progress" || t.status === "blocked") { + partial.add(t.requirement); + } + } + + return { + covered: [...covered].sort(), + partial: [...partial].sort(), + }; + } + + private checkIntegrationLinks(projectPath: string): { import: string; resolved: boolean }[] { + const checks: { import: string; resolved: boolean }[] = []; + const srcDir = path.join(projectPath, "src"); + + if (!fs.existsSync(srcDir)) return checks; + + const tsFiles = this.collectTsFiles(srcDir); + const importPattern = /import\s+.*from\s+['"](\.\/[^'"]+)['"]/g; + + for (const file of tsFiles) { + const content = fs.readFileSync(file, "utf-8"); + let match: RegExpExecArray | null; + while ((match = importPattern.exec(content)) !== null) { + const importPath = match[1]; + const resolved = this.resolveImport(file, importPath); + if (importPath.startsWith(".")) { + checks.push({ import: `${path.relative(projectPath, file)}:${importPath}`, resolved: resolved !== null }); + } + } + } + + return checks; + } + + private commitVerificationResult(context: AgentContext, result: VerifierResult, ciFiles: CIAgentFiles): void { + try { + const projectState = new GitContext(context.project_path).reconstructState(); + const milestone = projectState.currentMilestone || "v1.0"; + + const verifyInput: VerifyCommitInput = { + phase: context.phase, + milestone, + subject: result.success ? "passed" : "gaps_found", + requirements: { + covered: result.requirementsCovered, + partial: result.requirementsPartial, + }, + lessons: result.success ? ["All verification checks passed"] : [`Must-have score: ${result.mustHaveScore}`, `Layer failures: ${result.layers.filter(l => !l.passed).map(l => l.name).join(", ")}`], + }; + + const commitMsg = CommitBuilder.buildVerifyCommit(verifyInput); + if (fileExists(path.join(context.project_path, ".git"))) { + execSync(`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" --allow-empty`, { + cwd: context.project_path, + stdio: "pipe", + }); + } + } catch (err) { + this.warn(`Verification commit failed: ${err instanceof Error ? err.message : String(err)}`); + } + } + + private collectTsFiles(dir: string): string[] { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && entry.name !== "node_modules") { + files.push(...this.collectTsFiles(fullPath)); + } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) { + files.push(fullPath); + } + } + return files; + } + + private resolveImport(fromFile: string, importPath: string): string | null { + if (!importPath.startsWith(".")) return null; + const dir = path.dirname(fromFile); + const candidates = [ + path.resolve(dir, importPath + ".ts"), + path.resolve(dir, importPath + ".js"), + path.resolve(dir, importPath, "index.ts"), + path.resolve(dir, importPath, "index.js"), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate; + } + return null; + } } \ No newline at end of file diff --git a/src/backends/ollama-base.ts b/src/backends/ollama-base.ts index 266b831..4d8eb29 100644 --- a/src/backends/ollama-base.ts +++ b/src/backends/ollama-base.ts @@ -15,16 +15,26 @@ import { import { AgentName, ModelProfile } from "../types/config.js"; import { Decision } from "../types/decisions.js"; import { Escalation } from "../types/escalation.js"; -import { ToolRegistry, ToolCall, ToolResult } from "./tool-registry.js"; +import { ToolRegistry, ToolCall, ToolResult, ToolDefinition } from "./tool-registry.js"; const MAX_TOOL_ROUNDS = 50; +const PERSONA_TOOL_MAP: Record = { + read: "readFile", + write: "writeFile", + edit: "editFile", + bash: "runBash", + glob: "glob", + grep: "grep", +}; + export abstract class OllamaBaseBackend implements IntelligenceBackend { abstract readonly name: string; readonly type: BackendType = "llm"; protected config: LLMBackendConfig; protected projectPath: string; + protected filteredToolSchema: Array> | null = null; constructor(config: LLMBackendConfig | undefined) { this.config = config || { base_url: "http://localhost:11434", model_profile: "balanced" }; @@ -42,6 +52,9 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend { const model = this.resolveModel(); const toolRegistry = new ToolRegistry(request.context.project_path); + const allowedTools = this.parsePersonaTools(personaContent); + const filteredDefinitions = this.filterToolDefinitions(toolRegistry.getDefinitions(), allowedTools); + this.filteredToolSchema = this.definitionsToOpenAISchema(filteredDefinitions); const messages: OllamaMessage[] = []; messages.push({ @@ -62,7 +75,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend { while (round < MAX_TOOL_ROUNDS) { round++; - const response = await this.callModel(messages, model, toolRegistry); + const response = await this.callModelWithTools(messages, model, filteredDefinitions); totalInputTokens += response.usage?.prompt_tokens || 0; totalOutputTokens += response.usage?.completion_tokens || 0; @@ -124,6 +137,65 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend { } } + protected parsePersonaTools(personaContent: string): string[] | null { + const frontmatterMatch = personaContent.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return null; + + const frontmatter = frontmatterMatch[1]; + const toolsMatch = frontmatter.match(/tools:\s*\n((?:\s+\w+:.+\n?)+)/); + if (!toolsMatch) { + const inlineMatch = frontmatter.match(/tools:\s*\[([^\]]+)\]/); + if (inlineMatch) { + return inlineMatch[1] + .split(",") + .map((t) => t.trim()) + .filter(Boolean) + .map((t) => PERSONA_TOOL_MAP[t] || t); + } + return null; + } + + const toolsBlock = toolsMatch[1]; + const toolNames: string[] = []; + const lineRegex = /^\s+(\w+):/gm; + let lineMatch; + while ((lineMatch = lineRegex.exec(toolsBlock)) !== null) { + const personaToolName = lineMatch[1]; + toolNames.push(PERSONA_TOOL_MAP[personaToolName] || personaToolName); + } + + return toolNames.length > 0 ? toolNames : null; + } + + protected filterToolDefinitions(definitions: ToolDefinition[], allowedTools: string[] | null): ToolDefinition[] { + if (!allowedTools) return definitions; + const allowedSet = new Set(allowedTools); + return definitions.filter((def) => allowedSet.has(def.name)); + } + + protected async callModelWithTools( + messages: OllamaMessage[], + model: string, + toolDefinitions: ToolDefinition[] + ): Promise { + return this.callModel(messages, model, new ToolRegistry(this.projectPath)); + } + + protected definitionsToOpenAISchema(definitions: ToolDefinition[]): Array> { + return definitions.map((def) => ({ + type: "function", + function: { + name: def.name, + description: def.description, + parameters: def.parameters, + }, + })); + } + + protected getActiveToolSchema(toolRegistry: ToolRegistry): Array> { + return this.filteredToolSchema || toolRegistry.getOpenAIToolSchema(); + } + protected abstract callModel( messages: OllamaMessage[], model: string, diff --git a/src/backends/ollama-cloud.ts b/src/backends/ollama-cloud.ts index 28ef92d..5a87770 100644 --- a/src/backends/ollama-cloud.ts +++ b/src/backends/ollama-cloud.ts @@ -61,7 +61,7 @@ export class OllamaCloudBackend extends OllamaBaseBackend { if (m.tool_calls) msg.tool_calls = m.tool_calls; return msg; }), - tools: toolRegistry.getOpenAIToolSchema(), + tools: this.getActiveToolSchema(toolRegistry), stream: false, }; diff --git a/src/backends/ollama-local.ts b/src/backends/ollama-local.ts index 85a831e..80448b8 100644 --- a/src/backends/ollama-local.ts +++ b/src/backends/ollama-local.ts @@ -48,7 +48,7 @@ export class OllamaLocalBackend extends OllamaBaseBackend { if (m.tool_calls) msg.tool_calls = m.tool_calls; return msg; }), - tools: toolRegistry.getOpenAIToolSchema(), + tools: this.getActiveToolSchema(toolRegistry), stream: false, }; diff --git a/src/index.ts b/src/index.ts index 4a73991..1f5b0e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,8 @@ export { BehavioralVerification } from "./verification/behavioral.js"; export { SecurityVerification } from "./verification/security.js"; export { QualityVerification } from "./verification/quality.js"; export { getAgent, getAvailableAgents } from "./agents/index.js"; +export type { PlannerResult } from "./agents/planner.js"; +export type { ExecutorResult } from "./agents/executor.js"; export { initCIAgent, loadConfig, saveConfig, isCIAgentInitialized } from "./core/config.js"; export { DEFAULT_CIAGENT_CONFIG } from "./types/config.js"; export { confidenceToLevel, shouldEscalate } from "./types/decisions.js";