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
This commit is contained in:
@@ -1,5 +1,57 @@
|
||||
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.";
|
||||
@@ -8,6 +60,7 @@ export class ProjectResearcherAgent extends BaseAgent {
|
||||
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,
|
||||
@@ -15,14 +68,203 @@ export class ProjectResearcherAgent extends BaseAgent {
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
|
||||
const summary = this.mechanicalProjectResearch(context.project_path);
|
||||
const output = this.formatSummary(summary);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "Project research requires an intelligence backend.",
|
||||
success: true,
|
||||
output,
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
decisions: summary.technologyDecisions.length,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user