a8b50f5109
---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
270 lines
8.4 KiB
TypeScript
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");
|
|
}
|
|
} |