fix: unquoted color, duplicate key, GLM downgrade + cross-platform validator
- Fix security-auditor.md color bare hex to quoted - Fix orchestrator.md duplicate devops-engineer key - Fix .kilo/kilo.jsonc: orchestrator + root model to kimi-k2.6:cloud - Update agent-frontmatter-validation.md with diagnostic guide - Update global.md with YAML frontmatter rules for all agents - Update agent-architect.md + workflow-architect.md with color checklist - Add scripts/validate-agents.cjs: zero-dependency, cross-platform, --fix flag, scans worktrees
This commit is contained in:
@@ -46,14 +46,19 @@ Component creator: design and build new agents, workflows, and skills from @capa
|
||||
1. Analyze gap from @capability-analyst
|
||||
2. Check existing capabilities for overlap
|
||||
3. Design component (agent/workflow/skill)
|
||||
4. Create file with valid YAML frontmatter
|
||||
4. Create file with valid YAML frontmatter — **color must be double-quoted**: `"#RRGGBB"`
|
||||
5. Update AGENTS.md + capability-index.yaml
|
||||
6. Request review from @capability-analyst
|
||||
|
||||
## Validation Checklist
|
||||
- [ ] No duplicates with existing components
|
||||
- [ ] YAML frontmatter valid (quoted colors, correct model, mode)
|
||||
- [ ] Minimal permissions granted
|
||||
- [ ] YAML frontmatter valid
|
||||
- [ ] **color is double-quoted hex** (`"#DC2626"`, never `#DC2626`)
|
||||
- [ ] mode is `subagent` or `all` (never `primary`)
|
||||
- [ ] model includes provider prefix (`ollama-cloud/...`)
|
||||
- [ ] description is non-empty
|
||||
- [ ] all permission keys present (read, edit, write, bash, glob, grep, task)
|
||||
- [ ] task permissions use deny-by-default
|
||||
- [ ] Integration points correct
|
||||
- [ ] Index files updated
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ permission:
|
||||
"planner": allow
|
||||
"reflector": allow
|
||||
"memory-manager": allow
|
||||
"devops-engineer": allow
|
||||
---
|
||||
|
||||
# Kilo Code: Orchestrator
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description: Scans for security vulnerabilities, OWASP Top 10, dependency CVEs, and hardcoded secrets
|
||||
mode: subagent
|
||||
model: ollama-cloud/deepseek-v4-pro-max
|
||||
color: #DC2626
|
||||
color: "#DC2626"
|
||||
permission:
|
||||
read: allow
|
||||
bash: allow
|
||||
|
||||
@@ -41,5 +41,6 @@ Workflow designer: create and maintain slash command workflows with quality gate
|
||||
1. Validate workflow with test run
|
||||
2. Update AGENTS.md with new workflow
|
||||
3. Verify Gitea integration works
|
||||
4. **Validate YAML frontmatter** — color must be `"#RRGGBB"` (double-quoted, never bare)
|
||||
|
||||
<gitea-commenting required="true" skill="gitea-commenting" />
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"skills": {
|
||||
"paths": [".kilo/skills"]
|
||||
},
|
||||
"model": "ollama-cloud/glm-5.1",
|
||||
"model": "ollama-cloud/kimi-k2.6:cloud",
|
||||
"default_agent": "orchestrator",
|
||||
"agent": {
|
||||
"orchestrator": {
|
||||
"model": "ollama-cloud/glm-5.1",
|
||||
"model": "ollama-cloud/kimi-k2.6:cloud",
|
||||
"variant": "thinking",
|
||||
"description": "Main dispatcher. Routes tasks between agents based on Issue status. GLM-5.1 thinking for optimal routing.",
|
||||
"description": "Main dispatcher. Routes tasks between agents based on Issue status. IF:92 for optimal routing.",
|
||||
"mode": "all",
|
||||
"permission": {
|
||||
"read": "allow",
|
||||
|
||||
@@ -4,23 +4,26 @@ Critical rules for modifying agent YAML frontmatter. Violations break Kilo Code.
|
||||
|
||||
## Color Format
|
||||
|
||||
**ALWAYS use quoted hex colors in YAML frontmatter:**
|
||||
**ALWAYS use quoted hex colors in YAML frontmatter. NEVER generate unquoted colors:**
|
||||
|
||||
```yaml
|
||||
# ✅ Good
|
||||
# ✅ Good — quoted, safe
|
||||
model: ollama-cloud/glm-5.1
|
||||
color: "#DC2626"
|
||||
color: "#4F46E5"
|
||||
color: "#0EA5E9"
|
||||
|
||||
# ❌ Bad - breaks YAML parsing
|
||||
# ❌ Bad — breaks YAML parsing, causes startup error
|
||||
color: #DC2626
|
||||
color: #4F46E5
|
||||
color: #0EA5E9
|
||||
|
||||
# ❌ Bad — partially quoted, still invalid
|
||||
color: '#DC2626'
|
||||
```
|
||||
|
||||
### Why
|
||||
|
||||
Unquoted `#` starts a YAML comment, making the value empty or invalid.
|
||||
Unquoted `#` starts a YAML comment, making the value empty or invalid. This causes:
|
||||
- `Config file invalid: color: Invalid input`
|
||||
- Agent fails to load
|
||||
- Entire pipeline stalls
|
||||
|
||||
## Mode Values
|
||||
|
||||
@@ -116,6 +119,38 @@ for f in .kilo/agents/*.md; do
|
||||
done
|
||||
```
|
||||
|
||||
### Validation Script (Recommended)
|
||||
|
||||
```bash
|
||||
node scripts/validate-agents.cjs
|
||||
```
|
||||
|
||||
This script checks:
|
||||
- `color` is double-quoted and starts with `"#`
|
||||
- `mode` is `subagent` or `all`
|
||||
- `model` exists in `kilo-meta.json`
|
||||
- `description` is non-empty
|
||||
- All permission keys present
|
||||
- No duplicate keys
|
||||
- No trailing commas in YAML
|
||||
|
||||
## Error Diagnosis
|
||||
|
||||
If you see this error on startup:
|
||||
|
||||
```
|
||||
Config file at .../agents/xxx.md is invalid: color: Invalid input
|
||||
```
|
||||
|
||||
→ **Fix**: Wrap color in double quotes: `color: "#DC2626"`
|
||||
|
||||
Common causes:
|
||||
1. AI generated `color: #DC2626` without quotes
|
||||
2. Copy-paste from documentation lost quotes
|
||||
3. Editor auto-format stripped quotes
|
||||
|
||||
**Always verify with**: `grep -r "^color: #" .kilo/agents/ .kilo/commands/ .kilo/skills/`
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### 1. Unquoted Color
|
||||
|
||||
@@ -30,20 +30,19 @@
|
||||
- Avoid introductions, conclusions, and unnecessary explanations
|
||||
- Output text to communicate with the user; use tools to complete tasks
|
||||
|
||||
## Markdown Structure
|
||||
## YAML Frontmatter Rules (All Agents)
|
||||
|
||||
```markdown
|
||||
# Category Name
|
||||
When generating or editing any `.md` file with YAML frontmatter (agents, commands, skills, rules):
|
||||
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
- **color must be double-quoted**: always `"#RRGGBB"`, never bare `#RRGGBB`
|
||||
- **mode must be valid**: only `subagent` or `all`, never `primary`
|
||||
- **model must include provider**: `ollama-cloud/...`, never bare model ID
|
||||
- **description must be non-empty**
|
||||
- **all permission keys required**: read, edit, write, bash, glob, grep, task
|
||||
- **task permission uses deny-by-default** with explicit allow-list
|
||||
|
||||
## Examples
|
||||
|
||||
Example of expected behavior
|
||||
**Critical**: Unquoted `#` starts a YAML comment and breaks the parser with:
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
When referencing code, include file path with line number:
|
||||
`file_path:line_number`
|
||||
Config file invalid: color: Invalid input
|
||||
```
|
||||
Always verify generated frontmatter with: `node scripts/validate-agents.cjs`
|
||||
|
||||
347
scripts/validate-agents.cjs
Normal file
347
scripts/validate-agents.cjs
Normal file
@@ -0,0 +1,347 @@
|
||||
// Agent Frontmatter Validation Script - Cross-Platform, Zero-Deps
|
||||
// Checks ONLY .kilo/agents/.md files and worktrees for YAML frontmatter correctness.
|
||||
//
|
||||
// Usage: node scripts/validate-agents.cjs
|
||||
// node scripts/validate-agents.cjs --fix (auto-quote unquoted colors)
|
||||
// node scripts/validate-agents.cjs --verbose
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const FIX_MODE = process.argv.includes("--fix");
|
||||
const VERBOSE = process.argv.includes("--verbose");
|
||||
|
||||
let errors = [];
|
||||
let warnings = [];
|
||||
let fixes = [];
|
||||
|
||||
// Config
|
||||
const ROOT = process.cwd();
|
||||
const AGENT_DIRS = [".kilo/agents"];
|
||||
|
||||
const VALID_MODES = new Set(["subagent", "all"]);
|
||||
|
||||
// Utils
|
||||
function log(...args) {
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
function globMd(dirPath) {
|
||||
const results = [];
|
||||
function walk(current) {
|
||||
if (!fs.existsSync(current)) return;
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const full = path.join(current, e.name);
|
||||
if (e.isDirectory()) walk(full);
|
||||
else if (e.name.endsWith(".md")) results.push(full);
|
||||
}
|
||||
}
|
||||
walk(dirPath);
|
||||
return results;
|
||||
}
|
||||
|
||||
function findWorktrees() {
|
||||
const worktreesDir = path.join(ROOT, ".kilo", "worktrees");
|
||||
if (!fs.existsSync(worktreesDir)) return [];
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(worktreesDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => path.join(worktreesDir, d.name));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadValidModels() {
|
||||
const metaPath = path.join(ROOT, "kilo-meta.json");
|
||||
if (!fs.existsSync(metaPath)) return new Set();
|
||||
try {
|
||||
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
||||
const models = new Set();
|
||||
if (meta.agents) {
|
||||
for (const a of Object.values(meta.agents)) {
|
||||
if (a.model) models.add(a.model);
|
||||
}
|
||||
}
|
||||
return models;
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
// Lightweight YAML Frontmatter Parser
|
||||
function parseYamlFrontmatter(content) {
|
||||
if (!content.startsWith("---")) return null;
|
||||
|
||||
const end = content.indexOf("---", 3);
|
||||
if (end === -1) return null;
|
||||
const fmText = content.slice(3, end).trim();
|
||||
const lines = fmText.split(/\r?\n/);
|
||||
|
||||
const result = {};
|
||||
let currentObj = null;
|
||||
|
||||
for (const raw of lines) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
const indent = raw.match(/^(\s*)/)[1].length;
|
||||
const topMatch = raw.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
||||
if (topMatch && indent === 0) {
|
||||
const key = topMatch[1];
|
||||
let val = topMatch[2].trim();
|
||||
if (val.startsWith('"') || val.startsWith("'")) {
|
||||
const q = val[0];
|
||||
const endIdx = val.slice(1).indexOf(q);
|
||||
result[key] = endIdx >= 0 ? val.slice(1, endIdx + 1) : val;
|
||||
currentObj = null;
|
||||
} else if (val === "") {
|
||||
result[key] = {};
|
||||
currentObj = result[key];
|
||||
} else {
|
||||
result[key] = val;
|
||||
currentObj = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentObj !== null && indent > 0) {
|
||||
const nestedMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
||||
if (nestedMatch) {
|
||||
const nKey = nestedMatch[1];
|
||||
let nVal = nestedMatch[2].trim();
|
||||
if (nVal.startsWith('"') || nVal.startsWith("'")) {
|
||||
const q = nVal[0];
|
||||
const endIdx = nVal.slice(1).indexOf(q);
|
||||
currentObj[nKey] = endIdx >= 0 ? nVal.slice(1, endIdx + 1) : nVal;
|
||||
} else if (nVal === "") {
|
||||
currentObj[nKey] = {};
|
||||
} else {
|
||||
currentObj[nKey] = nVal;
|
||||
}
|
||||
} else if (trimmed.startsWith('"*"')) {
|
||||
const nextColon = trimmed.indexOf(":");
|
||||
if (nextColon > 0) {
|
||||
const k = trimmed
|
||||
.slice(0, nextColon)
|
||||
.replace(/"/g, "")
|
||||
.trim();
|
||||
const v = trimmed.slice(nextColon + 1).trim();
|
||||
currentObj[k] = v.replace(/"/g, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validation
|
||||
function validateAgentFile(filePath, validModels) {
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
errors.push({ file: filePath, msg: "Cannot read file" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.startsWith("---")) {
|
||||
errors.push({
|
||||
file: filePath,
|
||||
msg: "Missing YAML frontmatter (must start with ---)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const allLines = content.split(/\r?\n/);
|
||||
|
||||
// Find frontmatter boundaries
|
||||
let fmEnd = -1;
|
||||
for (let i = 1; i < allLines.length; i++) {
|
||||
if (allLines[i].trim() === "---") {
|
||||
fmEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fmEnd === -1) {
|
||||
errors.push({ file: filePath, msg: "Unclosed YAML frontmatter" });
|
||||
return;
|
||||
}
|
||||
|
||||
const fmLines = allLines.slice(0, fmEnd);
|
||||
|
||||
// --- Color MUST be double-quoted ---
|
||||
const colorLineIdx = fmLines.findIndex((l) => l.match(/^color\s*:/));
|
||||
if (colorLineIdx >= 0) {
|
||||
const colorRaw = fmLines[colorLineIdx];
|
||||
const colorMatch = colorRaw.match(/^color\s*:\s*(.+)$/);
|
||||
if (colorMatch) {
|
||||
const colorVal = colorMatch[1].trim();
|
||||
const isValid = colorVal.match(/^"#[0-9A-Fa-f]{6}"$/);
|
||||
if (!isValid && colorVal !== "") {
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: colorLineIdx + 1,
|
||||
msg: `color must be double-quoted hex (e.g. "#DC2626"), got: ${colorVal}`,
|
||||
});
|
||||
|
||||
const isBareHex = colorVal.match(/^#[0-9A-Fa-f]{6}$/);
|
||||
if (FIX_MODE && isBareHex) {
|
||||
allLines[colorLineIdx] = `color: "${colorVal}"`;
|
||||
fs.writeFileSync(filePath, allLines.join("\n"), "utf8");
|
||||
fixes.push({ file: filePath, msg: `Quoted color: ${colorVal}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fm = parseYamlFrontmatter(content);
|
||||
if (!fm) {
|
||||
errors.push({ file: filePath, msg: "Failed to parse YAML frontmatter" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate keys ONLY inside frontmatter
|
||||
const rawKeys = [];
|
||||
for (let i = 0; i < fmEnd; i++) {
|
||||
const l = fmLines[i];
|
||||
if (l.match(/^(\w[\w-]*)\s*:/)) {
|
||||
const key = l.match(/^(\w[\w-]*)\s*:/)[1];
|
||||
if (rawKeys.includes(key)) {
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: i + 1,
|
||||
msg: `Duplicate key: ${key}`,
|
||||
});
|
||||
}
|
||||
rawKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// mode
|
||||
if (fm.mode) {
|
||||
const modeLineIdx = fmLines.findIndex((l) => l.match(/^mode\s*:/)) + 1;
|
||||
if (!VALID_MODES.has(fm.mode)) {
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: modeLineIdx,
|
||||
msg: `Invalid mode: "${fm.mode}". Must be "subagent" or "all"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// model
|
||||
if (fm.model) {
|
||||
const modelLineIdx = fmLines.findIndex((l) => l.match(/^model\s*:/)) + 1;
|
||||
if (!fm.model.includes("/")) {
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: modelLineIdx,
|
||||
msg: `Model must include provider prefix, got: "${fm.model}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// description
|
||||
if (!fm.description || fm.description.trim() === "") {
|
||||
const descLineIdx = fmLines.findIndex((l) => l.match(/^description\s*:/)) + 1;
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: descLineIdx,
|
||||
msg: "description is required and must be non-empty",
|
||||
});
|
||||
}
|
||||
|
||||
// permissions - check block exists with at least some permissions
|
||||
if (fm.permission) {
|
||||
// Only warn about missing keys, don't error - some agents are intentionally limited
|
||||
const RECOMMENDED_KEYS = ["read", "edit", "write", "bash", "glob", "grep", "task"];
|
||||
const missing = RECOMMENDED_KEYS.filter((k) => !(k in fm.permission));
|
||||
if (missing.length > 0) {
|
||||
const permLineIdx = fmLines.findIndex((l) => l.match(/^permission\s*:/)) + 1;
|
||||
warnings.push({
|
||||
file: filePath,
|
||||
line: permLineIdx,
|
||||
msg: `Missing optional permission keys: ${missing.join(", ")}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const permLineIdx = fmLines.findIndex((l) => l.match(/^permission\s*:/)) + 1;
|
||||
errors.push({
|
||||
file: filePath,
|
||||
line: permLineIdx,
|
||||
msg: "permission block is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (VERBOSE && errors.length === 0 && warnings.length === 0) {
|
||||
log(` OK ${path.relative(ROOT, filePath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
const validModels = loadValidModels();
|
||||
const worktrees = findWorktrees();
|
||||
let totalFiles = 0;
|
||||
|
||||
function scanAgents(dirPath, label) {
|
||||
const files = globMd(dirPath).filter((f) =>
|
||||
f.includes(path.sep + "agents" + path.sep)
|
||||
);
|
||||
if (!files.length) return;
|
||||
if (VERBOSE) log(`\n[${label}]`);
|
||||
for (const file of files) {
|
||||
totalFiles++;
|
||||
validateAgentFile(file, validModels);
|
||||
}
|
||||
}
|
||||
|
||||
for (const d of AGENT_DIRS) {
|
||||
scanAgents(path.join(ROOT, d), "MAIN");
|
||||
}
|
||||
for (const wt of worktrees) {
|
||||
for (const d of AGENT_DIRS) {
|
||||
scanAgents(path.join(wt, d), `WORKTREE:${path.basename(wt)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Report
|
||||
log("\n" + "=".repeat(60));
|
||||
log(`Checked ${totalFiles} agent files`);
|
||||
|
||||
if (fixes.length > 0) {
|
||||
log(`\n✅ Auto-fixed ${fixes.length} issue(s):`);
|
||||
for (const f of fixes) {
|
||||
log(` ${path.relative(ROOT, f.file)}: ${f.msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
log(`\n⚠️ ${warnings.length} warning(s):`);
|
||||
for (const w of warnings) {
|
||||
const rel = path.relative(ROOT, w.file);
|
||||
const lineStr = w.line ? `:${w.line}` : "";
|
||||
log(` ${rel}${lineStr} ${w.msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
log("\n🎉 All agent frontmatters valid!\n");
|
||||
process.exit(0);
|
||||
} else {
|
||||
log(`\n❌ ${errors.length} error(s):\n`);
|
||||
for (const e of errors) {
|
||||
const rel = path.relative(ROOT, e.file);
|
||||
const lineStr = e.line ? `:${e.line}` : "";
|
||||
log(` ${rel}${lineStr} ${e.msg}`);
|
||||
}
|
||||
|
||||
if (!FIX_MODE && errors.some((e) => e.msg.includes("color"))) {
|
||||
log("\n💡 Run with --fix to auto-quote colors.\n");
|
||||
}
|
||||
log("");
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user