Files
ci/src/agents/project-researcher.ts
T
Jon Chery a8b50f5109
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled
feat(ci): v0.9.0 — Distribution & Expansion milestone complete
---ci---
project: ci
phase: 6
milestone: v0.9
status: complete
artifacts:
  tags: [v0.9.0]
decisions:
  - id: D-047
    decision: v0.9 theme = Distribution & Expansion
    rationale: npm publish + OpenAI/Anthropic backends + agent flesh + parallel execution
    confidence: 0.92
  - id: D-049
    decision: Feature milestone — patch tags v0.8.1-v0.8.6 then v0.9.0
    rationale: OpenAI backend, agent flesh, npm publish all feat
    confidence: 0.95
  - id: D-059
    decision: Rename OllamaBaseBackend to LLMBaseBackend + thin OllamaBaseBackend subclass
    rationale: 15 of 17 methods backend-agnostic
    confidence: 0.92
  - id: D-060
    decision: OpenAI/Anthropic backends use native fetch() not SDK packages
    rationale: No dependency bloat; fetch native in Node 18+
    confidence: 0.85
  - id: D-066
    decision: Concurrency limiter internal (no p-limit dependency)
    rationale: 15 lines; avoids dependency for trivial feature
    confidence: 0.90
  - id: D-067
    decision: Promise.allSettled for review agents at orchestrator lines 373-400
    rationale: Current sequential loop replaced with parallel execution
    confidence: 0.88
requirements:
  covered: [PUBLISH-01, PUBLISH-02, PUBLISH-03, PUBLISH-04, OPENAI-01, OPENAI-02, OPENAI-03, OPENAI-04, OPENAI-05, FLESH-01, FLESH-02, FLESH-03, FLESH-04, FLESH-05, ANTHROPIC-01, ANTHROPIC-02, FLESH-06, FLESH-07, NPM-01, NPM-02, PARALLEL-01, PARALLEL-02, PARALLEL-03, INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05]
---/ci---

6 phases, 28 tasks, 4077 net lines added, 57 test suites, 527 tests, zero stub agents
2026-05-30 02:19:44 +00:00

270 lines
8.4 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
interface EcosystemSummary {
frameworks: string[];
apis: string[];
patterns: string[];
tooling: string[];
technologyDecisions: Array<{ id: string; decision: string; confidence: number }>;
}
const FRAMEWORK_PATTERNS: Record<string, string[]> = {
react: ["react"],
vue: ["vue"],
angular: ["@angular/core"],
svelte: ["svelte"],
express: ["express"],
fastify: ["fastify"],
nestjs: ["@nestjs/core"],
next: ["next"],
nuxt: ["nuxt"],
koa: ["koa"],
jest: ["jest"],
vitest: ["vitest"],
};
const API_PATTERNS: Record<string, string[]> = {
graphql: ["graphql", "apollo", "@apollo"],
rest: ["express", "fastify", "restana"],
grpc: ["grpc", "@grpc"],
websocket: ["ws", "socket.io"],
};
const PATTERN_PATTERNS: Record<string, string[]> = {
microservices: ["@nestjs/microservices", "amqplib", "kafkajs"],
middleware: ["express", "koa", "fastify"],
cqrs: ["@nestjs/cqrs"],
dependency_injection: ["inversify", "tsyringe", "@nestjs/core"],
test_driven: ["jest", "vitest", "mocha"],
};
const TOOLING_PATTERNS: Record<string, string[]> = {
typescript: ["typescript"],
eslint: ["eslint"],
prettier: ["prettier"],
webpack: ["webpack"],
vite: ["vite"],
rollup: ["rollup"],
esbuild: ["esbuild"],
docker: [],
ci_cd: [],
};
export class ProjectResearcherAgent extends BaseAgent {
readonly name = "project-researcher";
readonly description = "Researches the domain ecosystem for a new project.";
readonly workflow = "research";
async execute(context: AgentContext): Promise<AgentResult> {
const start = Date.now();
this.log("Researching project domain ecosystem...");
if (context.backend) {
const result = await this.executeViaBackend(
context,
`Research the domain ecosystem for: ${context.specification}`
);
return { ...result, duration_ms: Date.now() - start };
}
const summary = this.mechanicalProjectResearch(context.project_path);
const output = this.formatSummary(summary);
return {
success: true,
output,
artifacts_created: [],
decisions: summary.technologyDecisions.length,
escalations: 0,
duration_ms: Date.now() - start,
};
}
mechanicalProjectResearch(projectPath: string): EcosystemSummary {
const pkg = this.readPackageJson(projectPath);
const tsconfig = this.readTsconfig(projectPath);
const techDecisions = this.readTechDecisions(projectPath);
const summary = this.categorizeFindings(pkg, tsconfig, techDecisions);
return summary;
}
readPackageJson(projectPath: string): Record<string, unknown> {
const pkgPath = path.join(projectPath, "package.json");
if (!fs.existsSync(pkgPath)) return {};
try {
return JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
} catch {
return {};
}
}
readTsconfig(projectPath: string): Record<string, unknown> {
const tsconfigPath = path.join(projectPath, "tsconfig.json");
if (!fs.existsSync(tsconfigPath)) return {};
try {
return JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
} catch {
return {};
}
}
readTechDecisions(projectPath: string): Array<{ id: string; decision: string; confidence: number }> {
const decisions: Array<{ id: string; decision: string; confidence: number }> = [];
const techCategories = ["technology_choice", "implementation_approach", "architecture"];
try {
const { execSync } = require("node:child_process");
const logContent = execSync(
`git log --all --format="%B" -100`,
{ cwd: projectPath, encoding: "utf-8", timeout: 5000 }
);
const categoryRegex = /category:\s*(\S+)/g;
const decisionRegex = /decisions:\s*\n((?:\s+-\s+.+\n?)+)/g;
let catMatch;
let match;
const blocks: Array<{ categories: string[]; items: string[] }> = [];
let currentCategories: string[] = [];
while ((catMatch = categoryRegex.exec(logContent)) !== null) {
currentCategories.push(catMatch[1].toLowerCase());
}
while ((match = decisionRegex.exec(logContent)) !== null) {
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
blocks.push({
categories: [...currentCategories],
items: items.map((i: string) => i.replace(/^\s*-\s*/, "").trim()),
});
}
for (const block of blocks) {
const isTech = block.categories.some((c) => techCategories.includes(c));
if (!isTech && block.categories.length > 0) continue;
for (const item of block.items) {
const idMatch = item.match(/D-(\d+)/);
const id = idMatch ? `D-${idMatch[1]}` : `D-${decisions.length + 1}`;
const confMatch = item.match(/confidence[:\s]+(\d+\.?\d*)/);
const confidence = confMatch ? parseFloat(confMatch[1]) : 0.5;
decisions.push({ id, decision: item, confidence });
}
}
} catch {
// git not available or no commits
}
return decisions;
}
categorizeFindings(
pkg: Record<string, unknown>,
tsconfig: Record<string, unknown>,
techDecisions: Array<{ id: string; decision: string; confidence: number }>
): EcosystemSummary {
const allDeps: string[] = [];
const deps = pkg.dependencies as Record<string, string> | undefined;
const devDeps = pkg.devDependencies as Record<string, string> | undefined;
if (deps) allDeps.push(...Object.keys(deps));
if (devDeps) allDeps.push(...Object.keys(devDeps));
const frameworks: string[] = [];
for (const [name, depPatterns] of Object.entries(FRAMEWORK_PATTERNS)) {
if (depPatterns.some((p) => allDeps.includes(p))) {
frameworks.push(name);
}
}
const apis: string[] = [];
for (const [name, depPatterns] of Object.entries(API_PATTERNS)) {
if (depPatterns.some((p) => allDeps.includes(p))) {
apis.push(name);
}
}
const patterns: string[] = [];
for (const [name, depPatterns] of Object.entries(PATTERN_PATTERNS)) {
if (depPatterns.some((p) => allDeps.includes(p))) {
patterns.push(name);
}
}
const tooling: string[] = [];
for (const [name, depPatterns] of Object.entries(TOOLING_PATTERNS)) {
if (depPatterns.some((p) => allDeps.includes(p))) {
tooling.push(name);
}
}
const compilerOptions = tsconfig.compilerOptions as Record<string, unknown> | undefined;
if (compilerOptions) {
const target = compilerOptions.target as string | undefined;
if (target) tooling.push(`es_target:${target.toLowerCase()}`);
const module = compilerOptions.module as string | undefined;
if (module) tooling.push(`module_system:${module.toLowerCase()}`);
}
const scripts = pkg.scripts as Record<string, string> | undefined;
if (scripts) {
if (scripts.build) tooling.push("build_script");
if (scripts.test) tooling.push("test_script");
if (scripts.lint) tooling.push("lint_script");
}
const engines = pkg.engines as Record<string, string> | undefined;
if (engines && engines.node) {
tooling.push(`node:${engines.node}`);
}
return {
frameworks,
apis,
patterns,
tooling,
technologyDecisions: techDecisions,
};
}
private formatSummary(summary: EcosystemSummary): string {
const lines: string[] = ["Ecosystem Summary:", ""];
lines.push("Frameworks:");
for (const f of summary.frameworks) {
lines.push(` - ${f}`);
}
if (summary.frameworks.length === 0) lines.push(" (none detected)");
lines.push("");
lines.push("APIs:");
for (const a of summary.apis) {
lines.push(` - ${a}`);
}
if (summary.apis.length === 0) lines.push(" (none detected)");
lines.push("");
lines.push("Patterns:");
for (const p of summary.patterns) {
lines.push(` - ${p}`);
}
if (summary.patterns.length === 0) lines.push(" (none detected)");
lines.push("");
lines.push("Tooling:");
for (const t of summary.tooling) {
lines.push(` - ${t}`);
}
if (summary.tooling.length === 0) lines.push(" (none detected)");
lines.push("");
lines.push("Technology Decisions:");
for (const d of summary.technologyDecisions) {
lines.push(` [${d.id}|conf=${d.confidence.toFixed(2)}] ${d.decision}`);
}
if (summary.technologyDecisions.length === 0) lines.push(" (none found)");
return lines.join("\n");
}
}