Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Current detectors cover common JavaScript/TypeScript, Python, Ruby, PHP, C#, Jav
- package manager: npm, pnpm, yarn, bun, pip, bundler, composer, dotnet, maven, gradle, cargo, go
- commands: install, dev, start, build, test, lint, format, backend install, local services
- docs: README, architecture docs, ADR directories, existing agent docs
- CI: GitHub Actions workflows
- CI: GitHub Actions, GitLab CI, and CircleCI workflow files
- monorepos: npm/pnpm workspaces, Turborepo, Nx, Lerna, Rush
- frameworks/tools: React, Vite, Next.js, Next.js App Router, Vue, Nuxt, Astro, Svelte, SvelteKit, Express, NestJS, Playwright, Storybook, FastAPI, Django, Flask, Pytest, Rails, Laravel, .NET, Spring Boot, Rust web frameworks, Gin, Docker, Docker Compose
- framework signals: package dependencies, framework config files, and common route/component conventions such as `app/`, `pages/`, `src/routes/`, and `.astro` files
Expand Down
2 changes: 1 addition & 1 deletion docs/detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This page summarizes the current detector coverage. Claims here should stay alig
| Go | `go.mod` | Gin | `go build ./...`, `go test ./...`, `go vet ./...`, `gofmt -w .` |
| Containers | `Dockerfile`, `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, `compose.yaml` | Docker, Docker Compose | `docker compose up -d`, `docker compose down` |
| Monorepos | `workspaces`, `pnpm-workspace.yaml`, `turbo.json`, `nx.json`, `lerna.json`, `rush.json` | npm workspaces, pnpm workspaces, Turborepo, Nx, Lerna, Rush | root package manager scripts when present |
| CI and docs | `.github/workflows/*.yml`, `README.md`, architecture docs, `docs/adr/`, existing agent docs | GitHub Actions, AGENTS.md, Claude, Cursor, Gemini, Copilot shims | readiness reporting and generated instructions reference detected paths |
| CI and docs | `.github/workflows/*.yml`, `.github/workflows/*.yaml`, `.gitlab-ci.yml`, `.gitlab-ci.yaml`, `.circleci/config.yml`, `.circleci/config.yaml`, `README.md`, architecture docs, `docs/adr/`, existing agent docs | GitHub Actions, GitLab CI, CircleCI, AGENTS.md, Claude, Cursor, Gemini, Copilot shims | readiness reporting and generated instructions reference detected paths |

## Framework Signals

Expand Down
13 changes: 11 additions & 2 deletions src/generator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function buildAgentsMd(profile) {
const languages = profile.languages.length
? profile.languages.map((language) => `${language.name} (${language.files} files)`).join(", ")
: "No source files detected";
const ci = profile.ci.githubActions.length ? profile.ci.githubActions.join(", ") : "No GitHub Actions workflows detected";
const ci = formatCi(profile.ci);
const architecture = profile.docs.architecture || "Not found";
const adr = profile.docs.adrDirectory || "Not found";
const localServices = localServicesSection(profile);
Expand Down Expand Up @@ -285,7 +285,7 @@ ${profile.name} is primarily a ${profile.primaryLanguage} repository. This docum
- Frameworks/tools: ${frameworks}
- Package manager: ${profile.packageManager}
- Monorepo: ${profile.monorepo?.detected ? "yes" : "no"}
- CI: ${profile.ci.githubActions.length ? profile.ci.githubActions.join(", ") : "No GitHub Actions workflows detected"}
- CI: ${formatCi(profile.ci)}

## Repository Layout

Expand Down Expand Up @@ -423,6 +423,15 @@ function commandLines(commands) {
.join("\n");
}

function formatCi(ci) {
const files = [
...(ci?.githubActions || []),
...(ci?.gitlabCi || []),
...(ci?.circleCi || []),
];
return files.length ? files.join(", ") : "No CI workflows detected";
}

function verificationChecklist(profile) {
const ordered = ["test", "lint", "build", "format"];
const lines = ordered
Expand Down
12 changes: 10 additions & 2 deletions src/linter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export async function lintRepo(profile) {
if (!profile.docs.readme) {
findings.push(finding("warning", "missing-readme", "README.md is missing.", "README.md", "Add a README with install, usage, and contribution basics."));
}
if (!profile.ci.githubActions.length) {
findings.push(finding("info", "missing-ci", "No GitHub Actions workflow was detected.", ".github/workflows", "Add CI so agents can trust the verification path."));
if (!ciFiles(profile.ci).length) {
findings.push(finding("info", "missing-ci", "No CI workflow was detected.", ".github/workflows", "Add CI so agents can trust the verification path."));
}

findings.push(...await validatePackageScripts(profile));
Expand Down Expand Up @@ -154,6 +154,14 @@ function packageScriptName(command) {
return match?.[1] || "";
}

function ciFiles(ci) {
return [
...(ci?.githubActions || []),
...(ci?.gitlabCi || []),
...(ci?.circleCi || []),
];
}

function finding(severity, ruleId, message, file, fixSuggestion) {
return { severity, ruleId, message, file, fixSuggestion };
}
Expand Down
12 changes: 12 additions & 0 deletions src/reporter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function renderScanSummary(profile) {
`Monorepo: ${formatMonorepo(profile.monorepo)}`,
`Package manager: ${profile.packageManager}`,
`Commands: ${Object.entries(profile.commands).filter(([, value]) => value).map(([key, value]) => `${key}=${value}`).join("; ") || "none"}`,
`CI: ${formatCi(profile.ci)}`,
`Agent docs: ${profile.agentDocs.map((doc) => doc.file).join(", ") || "none"}`,
].join("\n");
}
Expand Down Expand Up @@ -425,6 +426,7 @@ export function renderMarkdownReport(profile, findings, score) {
- Frameworks: ${profile.frameworks.join(", ") || "none"}
- Monorepo: ${formatMonorepo(profile.monorepo)}
- Package manager: ${profile.packageManager}
- CI: ${formatCi(profile.ci)}

${score.summary}

Expand Down Expand Up @@ -469,6 +471,7 @@ export function renderSnapshot(snapshot) {
- Primary language: ${profile.primaryLanguage}
- Package manager: ${profile.packageManager}
- Monorepo: ${formatMonorepo(profile.monorepo)}
- CI: ${formatCi(profile.ci)}

${score.summary}

Expand Down Expand Up @@ -539,6 +542,15 @@ function formatMonorepo(monorepo) {
return `${tools}${workspaces}`;
}

function formatCi(ci) {
const files = [
...(ci?.githubActions || []),
...(ci?.gitlabCi || []),
...(ci?.circleCi || []),
];
return files.length ? files.join(", ") : "none";
}

function badgeColor(score) {
if (score >= 90) return "brightgreen";
if (score >= 75) return "green";
Expand Down
10 changes: 10 additions & 0 deletions src/scanner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,19 @@ async function detectCi(root) {
.sort();
return {
githubActions: workflows,
gitlabCi: await detectExistingFiles(root, [".gitlab-ci.yml", ".gitlab-ci.yaml"]),
circleCi: await detectExistingFiles(root, [".circleci/config.yml", ".circleci/config.yaml"]),
};
}

async function detectExistingFiles(root, candidates) {
const results = [];
for (const candidate of candidates) {
if (await pathExists(path.join(root, candidate))) results.push(candidate);
}
return results;
}

function parseTomlValue(text, key) {
if (!text) return "";
const match = text.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*["']([^"']+)["']`, "m"));
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/multi-ci-app/.circleci/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2.1

jobs:
lint:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run: npm run lint
9 changes: 9 additions & 0 deletions test/fixtures/multi-ci-app/.circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2.1

jobs:
test:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run: npm test
12 changes: 12 additions & 0 deletions test/fixtures/multi-ci-app/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
3 changes: 3 additions & 0 deletions test/fixtures/multi-ci-app/.gitlab-ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lint:
script:
- npm run lint
3 changes: 3 additions & 0 deletions test/fixtures/multi-ci-app/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test:
script:
- npm test
7 changes: 7 additions & 0 deletions test/fixtures/multi-ci-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "fixture-multi-ci-app",
"type": "module",
"scripts": {
"test": "node --test"
}
}
23 changes: 18 additions & 5 deletions test/scanner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { improveRepo } from "../src/improver.mjs";
import { lintRepo, scoreRepo } from "../src/linter.mjs";
import { buildAgentMatrix } from "../src/matrix.mjs";
import { resolvePreset } from "../src/presets.mjs";
import { renderAgentMatrix, renderAnnotations, renderBenchmarkReport, renderComparison, renderDoctor, renderExamplesCatalog, renderExplanation, renderImprovement, renderImprovementIssue, renderLeaderboard, renderMarkdownReport, renderRoadmap, renderShareComment } from "../src/reporter.mjs";
import { renderAgentMatrix, renderAnnotations, renderBenchmarkReport, renderComparison, renderDoctor, renderExamplesCatalog, renderExplanation, renderImprovement, renderImprovementIssue, renderLeaderboard, renderMarkdownReport, renderRoadmap, renderShareComment, renderSnapshot } from "../src/reporter.mjs";
import { scanRepo } from "../src/scanner.mjs";
import { snapshotRepo } from "../src/snapshot.mjs";
import { renderCiWorkflow, writeCiWorkflow } from "../src/workflow.mjs";
Expand All @@ -35,6 +35,14 @@ test("scan detects a Node app profile", async () => {
assert.deepEqual(profile.ci.githubActions, [".github/workflows/ci.yml"]);
});

test("scan detects GitHub Actions, GitLab CI, and CircleCI files", async () => {
const profile = await scanRepo(fixture("multi-ci-app"));

assert.deepEqual(profile.ci.githubActions, [".github/workflows/ci.yml"]);
assert.deepEqual(profile.ci.gitlabCi, [".gitlab-ci.yml", ".gitlab-ci.yaml"]);
assert.deepEqual(profile.ci.circleCi, [".circleci/config.yml", ".circleci/config.yaml"]);
});

test("scan detects Python, Rust, and Go repositories", async () => {
const python = await scanRepo(fixture("python-app"));
const rust = await scanRepo(fixture("rust-app"));
Expand Down Expand Up @@ -306,23 +314,28 @@ test("score is explainable and bounded", async () => {
});

test("markdown report includes commands, docs, and findings", async () => {
const profile = await scanRepo(fixture("node-app"));
const profile = await scanRepo(fixture("multi-ci-app"));
const findings = await lintRepo(profile);
const score = scoreRepo(profile, findings);
const report = renderMarkdownReport(profile, findings, score);

assert.match(report, /Agent Readiness Report/);
assert.match(report, /fixture-node-app/);
assert.match(report, /fixture-multi-ci-app/);
assert.match(report, /npm run test/);
assert.match(report, /\.gitlab-ci\.yml/);
assert.match(report, /\.circleci\/config\.yml/);
});

test("snapshot summarizes score, compatibility, and findings", async () => {
const snapshot = await snapshotRepo(fixture("node-app"));
const snapshot = await snapshotRepo(fixture("multi-ci-app"));
const report = renderSnapshot(snapshot);

assert.equal(snapshot.repository.name, "fixture-node-app");
assert.equal(snapshot.repository.name, "fixture-multi-ci-app");
assert.equal(snapshot.matrix.summary.total, 5);
assert.equal(typeof snapshot.score.score, "number");
assert.equal(snapshot.summary.findings, snapshot.findings.length);
assert.match(report, /\.gitlab-ci\.yml/);
assert.match(report, /\.circleci\/config\.yml/);
});

test("examples catalog links copy-ready sample files", () => {
Expand Down