feat: add GitHub workflows to sync agents/ to Devin Knowledge (#26994)

* feat: add GitHub workflows to sync agents/ to Devin Knowledge

- Add parse-to-devin-knowledge.ts to convert agents/ markdown to Devin Knowledge JSON
- Add validate-format.ts to validate rules have frontmatter and knowledge-base sections start with 'When...'
- Add sync-to-devin.ts to sync knowledge entries to Devin API
- Add export-devin-knowledge.sh to backup existing Devin knowledge
- Add validate-agents-format.yml workflow to validate format on PRs
- Add sync-agents-to-devin.yml workflow to sync on merge to main
- Add devin-knowledge.json to .gitignore (generated file)

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: use tsx instead of ts-node for better ESM support in CI

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: add missing knowledge entries from Devin backup

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: move agents scripts to scripts/ folder

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename scripts to devin-knowledge-* for clarity

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: move scripts to scripts/devin/ with clearer names

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* rename DEVIN_API_TOKEN to DEVIN_API_KEY

* docs: fix usage comment script path

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: remove folder creation from sync script (API doesn't support it)

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add -S flag to shebang for proper env execution

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* use pull_request_target

* fix: add -S flag to shebang in sync-knowledge-to-devin.ts for proper env execution

Co-Authored-By: unknown <>

* restructure workflows

* refactor: consolidate agent docs and split knowledge-base into modular rules

- Delete knowledge-base.md, migrate content to 17 new rule files
- Delete coding-standards.md (content duplicated in AGENTS.md and rules)
- Add ci- and reference- prefixes to rules/_sections.md
- Update AGENTS.md to reference new rule files
- Update agents/README.md as rules index
- Clean up parse-local-knowledge.ts (remove deleted file references)

New rule files:
- testing-playwright, testing-mocking, testing-timezone
- ci-check-failures, ci-type-check-first, ci-git-workflow
- data-prisma-migrations, data-prisma-feature-flags
- quality-error-handling, quality-imports, quality-pr-creation, quality-code-comments
- architecture-features-modules
- patterns-workflow-triggers, patterns-app-store
- reference-file-locations, reference-local-dev

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "refactor: consolidate agent docs and split knowledge-base into modular rules"

This reverts commit 8251b6b214c7c01a3bfe2137c6aa9292dd72427e.

* refactor: reorganize agent docs - extract coding rules, keep domain knowledge

- Slim down knowledge-base.md (356 → 96 lines) to domain knowledge only
- Add Business rules section (managed events, orgs/teams, OAuth clients)
- Delete coding-standards.md (content moved to rules)
- Create 19 new rule files for coding guidelines:
  - quality-*: PR creation, error handling, imports, comments, code review
  - testing-*: playwright, mocking, timezone, incremental
  - ci-*: check failures, type-check-first, git workflow
  - data-prisma-*: migrations, feature flags
  - patterns-*: workflow triggers, app store
  - architecture-features-modules, reference-file-locations, reference-local-dev
- Update agents/README.md as rules index (43 total rules)
- Update _sections.md with CI/CD and Reference sections
- Clean up parse-local-knowledge.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: rename knowledge-base sections to start with "When..."

Update section headers to pass validation rules:
- "Business Rules" → "When working with managed events, organizations, or OAuth clients"
- "Product & Codebase Knowledge" → "When you need product or codebase context"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: split knowledge-base business rules into separate sections

Split the combined "When working with managed events, organizations, or OAuth clients"
section into three distinct ## sections for better Devin triggering:
- When working with managed event types
- When working with organizations and teams
- When working with OAuth clients

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify trigger description logic in parse-local-knowledge.ts

Since validate-local-knowledge.ts enforces that all section titles must start
with "When...", we can remove the manual fallback logic and just use the
title directly as the trigger description.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify knowledge-base section validation

Simplify the validation to only check that section titles start with "When..."
since we've standardized on that pattern. Remove the special cases for error,
file naming, PR, and repo note sections.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add script to delete all Devin knowledge entries

Add delete-all-devin-knowledge.ts script that:
- Lists all knowledge entries before deletion
- Requires interactive confirmation (Y) to proceed
- Blocks execution in non-TTY environments (CI, piped input)
- Shows progress while deleting entries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add API reference links to delete-all-devin-knowledge script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: replace NOW() with CURRENT_TIMESTAMP in feature flag migration example

Co-Authored-By: unknown <>

* add -S

* docs: consolidate agent documentation and add AI setup guide

- Remove duplicated Project Structure & Tech Stack from agents/README.md
- Condense Commands section in AGENTS.md, link to agents/commands.md
- Add AI-Assisted Development section to root README.md explaining
  the agents/ folder structure and symlink configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: remove incorrect CLAUDE.md symlink claim from README

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-02-03 15:40:04 +00:00
1 parent 51b3f5c74d
commit 10dd0e2af2
34 files changed
+1823 -485

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env -S npx tsx
/**
* Deletes ALL knowledge entries from Devin's Knowledge API.
*
* WARNING: This is a destructive operation! It will delete all knowledge entries.
* The script requires interactive confirmation to prevent accidental execution.
*
* API Reference:
* - List: https://docs.devin.ai/api-reference/knowledge/list-knowledge
* - Delete: https://docs.devin.ai/api-reference/knowledge/delete-knowledge
*
* Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/delete-all-devin-knowledge.ts
*/
import process from "node:process";
import * as readline from "readline";
interface ApiKnowledgeEntry {
id: string;
name: string;
}
interface ApiListResponse {
folders: { id: string; name: string }[];
knowledge: ApiKnowledgeEntry[];
}
const API_BASE = "https://api.devin.ai/v1";
async function apiRequest<T>(method: string, endpoint: string): Promise<T> {
const token = process.env.DEVIN_API_KEY;
if (!token) {
throw new Error("DEVIN_API_KEY environment variable is not set");
}
const url = `${API_BASE}${endpoint}`;
const options: RequestInit = {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
};
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
if (response.status === 204) {
return {} as T;
}
return response.json() as Promise<T>;
}
async function listKnowledge(): Promise<ApiListResponse> {
return apiRequest<ApiListResponse>("GET", "/knowledge");
}
async function deleteKnowledge(noteId: string): Promise<void> {
await apiRequest<void>("DELETE", `/knowledge/${noteId}`);
}
function askConfirmation(question: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim().toUpperCase() === "Y");
});
});
}
async function main() {
// Check if running in a TTY (interactive terminal)
if (!process.stdin.isTTY) {
console.error("Error: This script must be run interactively (not in CI or piped input).");
console.error("This is a safety measure to prevent accidental deletion.");
process.exit(1);
}
console.log("Fetching existing knowledge from Devin API...\n");
const remoteData = await listKnowledge();
const entryCount = remoteData.knowledge.length;
if (entryCount === 0) {
console.log("No knowledge entries found. Nothing to delete.");
process.exit(0);
}
console.log(`Found ${entryCount} knowledge entries:\n`);
for (const entry of remoteData.knowledge) {
console.log(` - ${entry.name}`);
}
console.log("\n⚠️ WARNING: This will permanently delete ALL knowledge entries listed above!");
console.log("This action cannot be undone.\n");
const confirmed = await askConfirmation("Are you sure you want to delete all entries? (Y/n): ");
if (!confirmed) {
console.log("\nAborted. No entries were deleted.");
process.exit(0);
}
console.log("\nDeleting all knowledge entries...\n");
let deleted = 0;
for (const entry of remoteData.knowledge) {
process.stdout.write(` Deleting: ${entry.name}...`);
await deleteKnowledge(entry.id);
console.log(" ✓");
deleted++;
}
console.log(`\nSuccessfully deleted ${deleted} knowledge entries.`);
}
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env -S npx tsx
/**
* Export all Devin Knowledge to a backup JSON file
* Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/export-devin-knowledge.ts
*/
import process from "node:process";
import * as fs from "fs";
import * as path from "path";
const API_BASE = "https://api.devin.ai/v1";
interface ApiFolder {
id: string;
name: string;
description: string;
created_at: string;
}
interface ApiKnowledgeEntry {
id: string;
name: string;
body: string;
trigger_description: string;
created_at: string;
created_by?: {
full_name: string;
id: string;
};
parent_folder_id?: string;
}
interface ApiListResponse {
folders: ApiFolder[];
knowledge: ApiKnowledgeEntry[];
}
async function main() {
const token = process.env.DEVIN_API_KEY;
if (!token) {
console.error("Error: DEVIN_API_KEY environment variable is not set");
console.error("Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/export-devin-knowledge.ts");
console.error("");
console.error("Get your API token from: https://app.devin.ai/settings/api-keys");
process.exit(1);
}
console.log("Exporting Devin Knowledge...");
const response = await fetch(`${API_BASE}/knowledge`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Error: API request failed with status ${response.status}`);
console.error(`Response: ${errorText}`);
process.exit(1);
}
const data: ApiListResponse = await response.json();
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "_").slice(0, 15);
const backupFile = `devin-knowledge-backup-${timestamp}.json`;
const outputPath = path.join(process.cwd(), backupFile);
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
console.log(`Success! Backup saved to: ${backupFile}`);
console.log(` - Folders: ${data.folders.length}`);
console.log(` - Knowledge entries: ${data.knowledge.length}`);
}
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env -S npx ts-node
import * as fs from "fs";
import * as path from "path";
interface DevinKnowledgeEntry {
name: string;
body: string;
trigger_description: string;
folder?: string;
}
interface DevinKnowledgeFolder {
name: string;
description: string;
}
interface DevinKnowledgeOutput {
folders: DevinKnowledgeFolder[];
knowledge: DevinKnowledgeEntry[];
}
interface RuleFrontmatter {
title: string;
impact: string;
impactDescription: string;
tags: string;
}
function parseFrontmatter(content: string): {
frontmatter: RuleFrontmatter | null;
body: string;
} {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { frontmatter: null, body: content };
}
const frontmatterStr = match[1];
const body = match[2].trim();
const frontmatter: Partial<RuleFrontmatter> = {};
const lines = frontmatterStr.split("\n");
for (const line of lines) {
const colonIndex = line.indexOf(":");
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
(frontmatter as Record<string, string>)[key] = value;
}
}
return {
frontmatter: frontmatter as RuleFrontmatter,
body,
};
}
function parseRuleFile(filePath: string, fileName: string): DevinKnowledgeEntry {
const content = fs.readFileSync(filePath, "utf-8");
const { frontmatter, body } = parseFrontmatter(content);
const name = frontmatter?.title || fileName.replace(".md", "").replace(/-/g, " ");
const tags = frontmatter?.tags || "";
const impact = frontmatter?.impact || "";
const impactDesc = frontmatter?.impactDescription || "";
let triggerDescription = `Use this rule when working on Cal.com codebase`;
if (tags) {
triggerDescription += ` and the task involves: ${tags}`;
}
if (impact) {
triggerDescription += `. Impact: ${impact}`;
}
return {
name: `[Rule] ${name}`,
body,
trigger_description: triggerDescription,
folder: "Rules",
};
}
function parseKnowledgeBaseSections(filePath: string): DevinKnowledgeEntry[] {
const content = fs.readFileSync(filePath, "utf-8");
const entries: DevinKnowledgeEntry[] = [];
const sectionRegex = /^## (.+)$/gm;
const sections: { title: string; startIndex: number }[] = [];
let match;
while ((match = sectionRegex.exec(content)) !== null) {
sections.push({
title: match[1],
startIndex: match.index,
});
}
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
const nextSection = sections[i + 1];
const endIndex = nextSection ? nextSection.startIndex : content.length;
const sectionContent = content.substring(section.startIndex, endIndex).trim();
const title = section.title;
// Section titles in knowledge-base.md must start with "When..." (enforced by validate-local-knowledge.ts)
// so we use the title directly as the trigger description
entries.push({
name: title,
body: sectionContent,
trigger_description: title,
folder: "Domain Knowledge",
});
}
return entries;
}
function parseCommandsFile(filePath: string): DevinKnowledgeEntry {
const content = fs.readFileSync(filePath, "utf-8");
return {
name: "Cal.com Build, Test & Development Commands",
body: content,
trigger_description:
"When you need to run commands in the Cal.com repository such as build, test, lint, type-check, database operations, or development server",
folder: "Commands",
};
}
function main() {
const agentsDir = path.join(__dirname, "..", "..", "agents");
const rulesDir = path.join(agentsDir, "rules");
const output: DevinKnowledgeOutput = {
folders: [
{
name: "Rules",
description: "Engineering rules and standards for Cal.com development",
},
{
name: "Domain Knowledge",
description: "Product and domain-specific knowledge for Cal.com",
},
{
name: "Commands",
description: "Build, test, and development commands for Cal.com",
},
],
knowledge: [],
};
// Parse rules directory
if (fs.existsSync(rulesDir)) {
const ruleFiles = fs
.readdirSync(rulesDir)
.filter((f) => f.endsWith(".md") && f !== "README.md" && f !== "_template.md" && f !== "_sections.md");
for (const ruleFile of ruleFiles) {
const filePath = path.join(rulesDir, ruleFile);
const entry = parseRuleFile(filePath, ruleFile);
output.knowledge.push(entry);
}
}
// Parse knowledge-base.md
const knowledgeBasePath = path.join(agentsDir, "knowledge-base.md");
if (fs.existsSync(knowledgeBasePath)) {
const sections = parseKnowledgeBaseSections(knowledgeBasePath);
output.knowledge.push(...sections);
}
// Parse commands.md
const commandsPath = path.join(agentsDir, "commands.md");
if (fs.existsSync(commandsPath)) {
const entry = parseCommandsFile(commandsPath);
output.knowledge.push(entry);
}
// Write output
const outputPath = path.join(agentsDir, "devin-knowledge.json");
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
console.log(`Generated ${output.knowledge.length} knowledge entries in ${output.folders.length} folders`);
console.log(`Output written to: ${outputPath}`);
// Print summary
const folderCounts: Record<string, number> = {};
for (const entry of output.knowledge) {
const folder = entry.folder || "Uncategorized";
folderCounts[folder] = (folderCounts[folder] || 0) + 1;
}
console.log("\nSummary by folder:");
for (const [folder, count] of Object.entries(folderCounts)) {
console.log(` ${folder}: ${count} entries`);
}
}
main();
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env -S npx tsx
/**
* Syncs knowledge from the generated JSON file to Devin's Knowledge API.
* - Creates new knowledge entries
* - Updates existing entries (matched by name)
* - Optionally deletes entries that no longer exist in the source
*
* Note: The Devin API v1 does not support creating folders via API.
* Entries will be created without folder assignment. You can organize
* them into folders manually in the Devin UI if needed.
*
* Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/sync-knowledge-to-devin.ts [--delete-removed]
*/
import process from "node:process";
import * as fs from "fs";
import * as path from "path";
interface DevinKnowledgeEntry {
name: string;
body: string;
trigger_description: string;
}
interface DevinKnowledgeOutput {
knowledge: DevinKnowledgeEntry[];
}
interface ApiKnowledgeEntry {
id: string;
name: string;
body: string;
trigger_description: string;
created_at: string;
created_by?: {
full_name: string;
id: string;
};
parent_folder_id?: string;
}
interface ApiListResponse {
folders: { id: string; name: string }[];
knowledge: ApiKnowledgeEntry[];
}
const API_BASE = "https://api.devin.ai/v1";
async function apiRequest<T>(method: string, endpoint: string, body?: Record<string, unknown>): Promise<T> {
const token = process.env.DEVIN_API_KEY;
if (!token) {
throw new Error("DEVIN_API_KEY environment variable is not set");
}
const url = `${API_BASE}${endpoint}`;
const options: RequestInit = {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
if (response.status === 204) {
return {} as T;
}
return response.json() as Promise<T>;
}
async function listKnowledge(): Promise<ApiListResponse> {
return apiRequest<ApiListResponse>("GET", "/knowledge");
}
async function createKnowledge(
name: string,
body: string,
triggerDescription: string
): Promise<ApiKnowledgeEntry> {
return apiRequest<ApiKnowledgeEntry>("POST", "/knowledge", {
name,
body,
trigger_description: triggerDescription,
});
}
async function updateKnowledge(
noteId: string,
name: string,
body: string,
triggerDescription: string
): Promise<ApiKnowledgeEntry> {
return apiRequest<ApiKnowledgeEntry>("PUT", `/knowledge/${noteId}`, {
name,
body,
trigger_description: triggerDescription,
});
}
async function deleteKnowledge(noteId: string): Promise<void> {
await apiRequest<void>("DELETE", `/knowledge/${noteId}`);
}
async function main() {
const args = process.argv.slice(2);
const deleteRemoved = args.includes("--delete-removed");
const agentsDir = path.join(path.dirname(__filename), "..", "..", "agents");
const jsonPath = path.join(agentsDir, "devin-knowledge.json");
if (!fs.existsSync(jsonPath)) {
console.error("Error: devin-knowledge.json not found. Run parse-to-devin-knowledge.ts first.");
process.exit(1);
}
const localData: DevinKnowledgeOutput = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
console.log("Fetching existing knowledge from Devin API...");
const remoteData = await listKnowledge();
console.log(
`Found ${remoteData.folders.length} folders and ${remoteData.knowledge.length} entries in Devin\n`
);
// Build knowledge name -> entry map for remote
const remoteKnowledgeMap = new Map<string, ApiKnowledgeEntry>();
for (const entry of remoteData.knowledge) {
remoteKnowledgeMap.set(entry.name, entry);
}
// Track which remote entries we've seen
const seenRemoteIds = new Set<string>();
// Sync knowledge entries
console.log("\nSyncing knowledge entries...");
let created = 0;
let updated = 0;
let unchanged = 0;
for (const entry of localData.knowledge) {
const existing = remoteKnowledgeMap.get(entry.name);
if (existing) {
seenRemoteIds.add(existing.id);
const bodyChanged = existing.body !== entry.body;
const triggerChanged = existing.trigger_description !== entry.trigger_description;
if (bodyChanged || triggerChanged) {
console.log(` Updating: ${entry.name}`);
await updateKnowledge(existing.id, entry.name, entry.body, entry.trigger_description);
updated++;
} else {
unchanged++;
}
} else {
console.log(` Creating: ${entry.name}`);
await createKnowledge(entry.name, entry.body, entry.trigger_description);
created++;
}
}
// Delete removed entries if flag is set
let deleted = 0;
if (deleteRemoved) {
console.log("\nChecking for entries to delete...");
for (const entry of remoteData.knowledge) {
if (!seenRemoteIds.has(entry.id)) {
console.log(` Deleting: ${entry.name}`);
await deleteKnowledge(entry.id);
deleted++;
}
}
}
// Summary
console.log("\n--- Sync Summary ---");
console.log(` Created: ${created}`);
console.log(` Updated: ${updated}`);
console.log(` Unchanged: ${unchanged}`);
if (deleteRemoved) {
console.log(` Deleted: ${deleted}`);
}
console.log("Sync complete!");
}
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env -S npx ts-node
/**
* Validates the format of files in the agents/ directory.
* - Rules files must have proper frontmatter (title, tags)
* - Knowledge-base sections must start with "When..."
*
* Exit code 0 = valid, Exit code 1 = invalid
*/
import process from "node:process";
import * as fs from "fs";
import * as path from "path";
interface ValidationError {
file: string;
message: string;
}
interface RuleFrontmatter {
title?: string;
impact?: string;
impactDescription?: string;
tags?: string;
}
function parseFrontmatter(content: string): {
frontmatter: RuleFrontmatter | null;
body: string;
} {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { frontmatter: null, body: content };
}
const frontmatterStr = match[1];
const body = match[2].trim();
const frontmatter: RuleFrontmatter = {};
const lines = frontmatterStr.split("\n");
for (const line of lines) {
const colonIndex = line.indexOf(":");
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
(frontmatter as Record<string, string>)[key] = value;
}
}
return { frontmatter, body };
}
function validateRuleFile(filePath: string, fileName: string): ValidationError[] {
const errors: ValidationError[] = [];
const content = fs.readFileSync(filePath, "utf-8");
const { frontmatter } = parseFrontmatter(content);
if (!frontmatter) {
errors.push({
file: `rules/${fileName}`,
message: "Missing YAML frontmatter. Rules must have frontmatter with title and tags.",
});
return errors;
}
if (!frontmatter.title || frontmatter.title.trim() === "") {
errors.push({
file: `rules/${fileName}`,
message: "Missing 'title' in frontmatter. Add a descriptive title for this rule.",
});
}
if (!frontmatter.tags || frontmatter.tags.trim() === "") {
errors.push({
file: `rules/${fileName}`,
message:
"Missing 'tags' in frontmatter. Add comma-separated tags to help Devin know when to apply this rule.",
});
}
return errors;
}
function validateKnowledgeBase(filePath: string): ValidationError[] {
const errors: ValidationError[] = [];
const content = fs.readFileSync(filePath, "utf-8");
// Find all ## headers
const sectionRegex = /^## (.+)$/gm;
let match;
const invalidSections: string[] = [];
while ((match = sectionRegex.exec(content)) !== null) {
const title = match[1].trim();
// Section titles must start with "When..." for clear trigger descriptions
if (!title.toLowerCase().startsWith("when ")) {
invalidSections.push(title);
}
}
if (invalidSections.length > 0) {
errors.push({
file: "knowledge-base.md",
message: `The following sections don't have clear trigger descriptions. Consider renaming them to start with "When..." to help Devin know when to use this knowledge:\n${invalidSections.map((s) => ` - "${s}"`).join("\n")}`,
});
}
return errors;
}
function main() {
const agentsDir = path.join(path.dirname(__filename), "..", "..", "agents");
const rulesDir = path.join(agentsDir, "rules");
const knowledgeBasePath = path.join(agentsDir, "knowledge-base.md");
const allErrors: ValidationError[] = [];
console.log("Validating agents/ directory format...\n");
// Validate rules files
if (fs.existsSync(rulesDir)) {
const ruleFiles = fs
.readdirSync(rulesDir)
.filter((f) => f.endsWith(".md") && f !== "README.md" && f !== "_template.md" && f !== "_sections.md");
console.log(`Checking ${ruleFiles.length} rule files...`);
for (const ruleFile of ruleFiles) {
const filePath = path.join(rulesDir, ruleFile);
const errors = validateRuleFile(filePath, ruleFile);
allErrors.push(...errors);
}
}
// Validate knowledge-base.md
if (fs.existsSync(knowledgeBasePath)) {
console.log("Checking knowledge-base.md...");
const errors = validateKnowledgeBase(knowledgeBasePath);
allErrors.push(...errors);
}
// Report results
console.log("");
if (allErrors.length === 0) {
console.log("All files are valid!");
process.exit(0);
} else {
console.log(`Found ${allErrors.length} validation error(s):\n`);
for (const error of allErrors) {
console.log(`[ERROR] ${error.file}`);
console.log(` ${error.message}\n`);
}
console.log("Please fix the above errors before merging.");
process.exit(1);
}
}
main();