/**
* Agent Artifact Detection System
*
* Detects, classifies, and provides recommendations for artifacts
* generated by AI coding agents during workflows.
*/
import { promises as fs } from "fs";
import path from "path";
import { globby } from "globby";
export interface AgentArtifact {
path: string;
type:
| "file"
| "directory"
| "inline-comment"
| "block-comment"
| "code-block";
category: "planning" | "debug" | "temporary" | "state" | "documentation";
confidence: number; // 0-1 how sure we are this is agent-generated
recommendation: "delete" | "review" | "keep" | "archive";
context?: string; // surrounding content for review
detectedBy: string; // which pattern matched
}
export interface ArtifactScanResult {
scannedFiles: number;
artifacts: AgentArtifact[];
summary: {
totalArtifacts: number;
byCategory: Record<string, number>;
byRecommendation: Record<string, number>;
};
}
export interface ArtifactCleanupConfig {
// Detection settings
patterns: {
files: string[]; // glob patterns for artifact files
directories: string[]; // agent state directories
inlineMarkers: string[]; // comment markers to detect
blockPatterns: RegExp[]; // multi-line patterns
};
// Behavior settings
autoDeleteThreshold: number; // confidence threshold for auto-delete
preserveGitIgnored: boolean; // skip .gitignored artifacts
archiveBeforeDelete: boolean; // safety backup
// Exclusions
excludePaths: string[];
excludePatterns: string[];
}
/**
* Default configuration for artifact detection
*/
export const DEFAULT_CONFIG: ArtifactCleanupConfig = {
patterns: {
files: [
"TODO.md",
"TODOS.md",
"PLAN.md",
"PLANNING.md",
"NOTES.md",
"SCRATCH.md",
"AGENT-*.md",
"*.agent.md",
],
directories: [
".claude",
".cursor",
".aider",
".copilot",
".codeium",
".agent-workspace",
],
inlineMarkers: [
"// @agent-temp",
"// TODO(agent):",
"# AGENT-NOTE:",
"<!-- agent:ephemeral -->",
"// FIXME(claude):",
"// FIXME(cursor):",
"// FIXME(copilot):",
"// TODO(claude):",
"// TODO(cursor):",
"// TODO(copilot):",
],
blockPatterns: [
/\/\*\s*AGENT[-_](?:START|BEGIN)[\s\S]*?AGENT[-_](?:END|FINISH)\s*\*\//gi,
/<!--\s*agent:ephemeral\s*-->[\s\S]*?<!--\s*\/agent:ephemeral\s*-->/gi,
/\/\/\s*@agent-temp-start[\s\S]*?\/\/\s*@agent-temp-end/gi,
],
},
autoDeleteThreshold: 0.9,
preserveGitIgnored: true,
archiveBeforeDelete: true,
excludePaths: ["node_modules", ".git", "dist", "build", ".documcp"],
excludePatterns: ["*.lock", "package-lock.json", "yarn.lock"],
};
/**
* Main Artifact Detector class
*/
export class ArtifactDetector {
private config: ArtifactCleanupConfig;
private projectPath: string;
constructor(projectPath: string, config?: Partial<ArtifactCleanupConfig>) {
this.projectPath = projectPath;
this.config = {
...DEFAULT_CONFIG,
...config,
patterns: {
...DEFAULT_CONFIG.patterns,
...(config?.patterns || {}),
},
};
}
/**
* Scan for agent artifacts in the project
*/
async scan(): Promise<ArtifactScanResult> {
const artifacts: AgentArtifact[] = [];
let scannedFiles = 0;
// Detect file-based artifacts
const fileArtifacts = await this.detectFileArtifacts();
artifacts.push(...fileArtifacts);
// Detect directory artifacts
const dirArtifacts = await this.detectDirectoryArtifacts();
artifacts.push(...dirArtifacts);
// Detect inline and block artifacts in files
const inlineArtifacts = await this.detectInlineArtifacts();
artifacts.push(...inlineArtifacts.artifacts);
scannedFiles = inlineArtifacts.scannedFiles;
// Generate summary
const summary = this.generateSummary(artifacts);
return {
scannedFiles,
artifacts,
summary,
};
}
/**
* Detect file-based artifacts (e.g., TODO.md, PLAN.md)
*/
private async detectFileArtifacts(): Promise<AgentArtifact[]> {
const artifacts: AgentArtifact[] = [];
// Build glob patterns with exclusions
const patterns = this.config.patterns.files.map((pattern) =>
path.join(this.projectPath, "**", pattern),
);
try {
const files = await globby(patterns, {
ignore: this.config.excludePaths.map((p) =>
path.join(this.projectPath, p, "**"),
),
absolute: true,
onlyFiles: true,
});
for (const filePath of files) {
const relativePath = path.relative(this.projectPath, filePath);
const fileName = path.basename(filePath);
// Determine category and confidence based on file name
const { category, confidence, detectedBy } =
this.categorizeFile(fileName);
artifacts.push({
path: relativePath,
type: "file",
category,
confidence,
recommendation: this.getRecommendation(confidence, category),
detectedBy,
});
}
} catch (error) {
// Silently handle globby errors (e.g., permission denied)
console.error(`Error scanning files: ${error}`);
}
return artifacts;
}
/**
* Detect directory-based artifacts (e.g., .claude/, .cursor/)
*/
private async detectDirectoryArtifacts(): Promise<AgentArtifact[]> {
const artifacts: AgentArtifact[] = [];
for (const dirName of this.config.patterns.directories) {
const dirPath = path.join(this.projectPath, dirName);
try {
const stats = await fs.stat(dirPath);
if (stats.isDirectory()) {
artifacts.push({
path: dirName,
type: "directory",
category: "state",
confidence: 0.95,
recommendation: "archive",
detectedBy: `Directory pattern: ${dirName}`,
});
}
} catch {
// Directory doesn't exist, skip
}
}
return artifacts;
}
/**
* Detect inline comments and block artifacts in code files
*/
private async detectInlineArtifacts(): Promise<{
artifacts: AgentArtifact[];
scannedFiles: number;
}> {
const artifacts: AgentArtifact[] = [];
let scannedFiles = 0;
// Scan common code file types
const patterns = [
"**/*.ts",
"**/*.js",
"**/*.tsx",
"**/*.jsx",
"**/*.py",
"**/*.rb",
"**/*.go",
"**/*.rs",
"**/*.java",
"**/*.md",
"**/*.html",
"**/*.css",
"**/*.scss",
].map((p) => path.join(this.projectPath, p));
try {
const files = await globby(patterns, {
ignore: this.config.excludePaths.map((p) =>
path.join(this.projectPath, p, "**"),
),
absolute: true,
onlyFiles: true,
});
for (const filePath of files) {
scannedFiles++;
const content = await fs.readFile(filePath, "utf-8");
const relativePath = path.relative(this.projectPath, filePath);
// Check for inline markers
const inlineArtifacts = this.detectInlineMarkers(content, relativePath);
artifacts.push(...inlineArtifacts);
// Check for block patterns
const blockArtifacts = this.detectBlockPatterns(content, relativePath);
artifacts.push(...blockArtifacts);
}
} catch (error) {
console.error(`Error scanning inline artifacts: ${error}`);
}
return { artifacts, scannedFiles };
}
/**
* Detect inline comment markers
*/
private detectInlineMarkers(
content: string,
filePath: string,
): AgentArtifact[] {
const artifacts: AgentArtifact[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const marker of this.config.patterns.inlineMarkers) {
if (line.includes(marker)) {
// Get context (2 lines before and after)
const contextStart = Math.max(0, i - 2);
const contextEnd = Math.min(lines.length, i + 3);
const context = lines.slice(contextStart, contextEnd).join("\n");
artifacts.push({
path: `${filePath}:${i + 1}`,
type: "inline-comment",
category: this.categorizeMarker(marker),
confidence: 0.85,
recommendation: "review",
context,
detectedBy: `Inline marker: ${marker}`,
});
}
}
}
return artifacts;
}
/**
* Detect block patterns
*/
private detectBlockPatterns(
content: string,
filePath: string,
): AgentArtifact[] {
const artifacts: AgentArtifact[] = [];
for (const pattern of this.config.patterns.blockPatterns) {
const matches = content.matchAll(pattern);
for (const match of matches) {
if (match[0]) {
// Get first 200 chars as context
const context = match[0].substring(0, 200);
artifacts.push({
path: filePath,
type: "block-comment",
category: "temporary",
confidence: 0.9,
recommendation: "delete",
context,
detectedBy: `Block pattern: ${pattern.source.substring(0, 50)}...`,
});
}
}
}
return artifacts;
}
/**
* Categorize a file based on its name
*/
private categorizeFile(fileName: string): {
category: AgentArtifact["category"];
confidence: number;
detectedBy: string;
} {
const upperName = fileName.toUpperCase();
if (
upperName.includes("TODO") ||
upperName.includes("PLAN") ||
upperName.includes("SCRATCH")
) {
return {
category: "planning",
confidence: 0.95,
detectedBy: `File name pattern: ${fileName}`,
};
}
if (upperName.includes("NOTES") || upperName.includes("AGENT")) {
return {
category: "documentation",
confidence: 0.9,
detectedBy: `File name pattern: ${fileName}`,
};
}
return {
category: "temporary",
confidence: 0.8,
detectedBy: `File name pattern: ${fileName}`,
};
}
/**
* Categorize a marker
*/
private categorizeMarker(marker: string): AgentArtifact["category"] {
if (marker.includes("TODO") || marker.includes("FIXME")) {
return "planning";
}
if (marker.includes("temp") || marker.includes("ephemeral")) {
return "temporary";
}
if (marker.includes("NOTE")) {
return "documentation";
}
return "debug";
}
/**
* Get recommendation based on confidence and category
*/
private getRecommendation(
confidence: number,
category: AgentArtifact["category"],
): AgentArtifact["recommendation"] {
if (confidence >= this.config.autoDeleteThreshold) {
if (category === "temporary" || category === "debug") {
return "delete";
}
return "archive";
}
if (confidence >= 0.7) {
return "review";
}
return "keep";
}
/**
* Generate summary statistics
*/
private generateSummary(
artifacts: AgentArtifact[],
): ArtifactScanResult["summary"] {
const byCategory: Record<string, number> = {};
const byRecommendation: Record<string, number> = {};
for (const artifact of artifacts) {
byCategory[artifact.category] = (byCategory[artifact.category] || 0) + 1;
byRecommendation[artifact.recommendation] =
(byRecommendation[artifact.recommendation] || 0) + 1;
}
return {
totalArtifacts: artifacts.length,
byCategory,
byRecommendation,
};
}
}
/**
* Detect agent artifacts in a project
*/
export async function detectArtifacts(
projectPath: string,
config?: Partial<ArtifactCleanupConfig>,
): Promise<ArtifactScanResult> {
const detector = new ArtifactDetector(projectPath, config);
return detector.scan();
}