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:
¨NW¨
2026-05-04 22:01:45 +01:00
parent fb552e0020
commit 80dca09ae0
8 changed files with 415 additions and 29 deletions

View File

@@ -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

View File

@@ -40,7 +40,6 @@ permission:
"planner": allow
"reflector": allow
"memory-manager": allow
"devops-engineer": allow
---
# Kilo Code: Orchestrator

View File

@@ -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

View File

@@ -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" />

View File

@@ -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",

View File

@@ -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

View File

@@ -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
View 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);
}