From 80dca09ae0a74bd9b1b846e7c91d8b398d80ce9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8NW=C2=A8?= <¨neroworld@mail.ru¨> Date: Mon, 4 May 2026 22:01:45 +0100 Subject: [PATCH] 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 --- .kilo/agents/agent-architect.md | 11 +- .kilo/agents/orchestrator.md | 1 - .kilo/agents/security-auditor.md | 2 +- .kilo/agents/workflow-architect.md | 1 + .kilo/kilo.jsonc | 6 +- .kilo/rules/agent-frontmatter-validation.md | 51 ++- .kilo/rules/global.md | 25 +- scripts/validate-agents.cjs | 347 ++++++++++++++++++++ 8 files changed, 415 insertions(+), 29 deletions(-) create mode 100644 scripts/validate-agents.cjs diff --git a/.kilo/agents/agent-architect.md b/.kilo/agents/agent-architect.md index ba64277..ca4e456 100755 --- a/.kilo/agents/agent-architect.md +++ b/.kilo/agents/agent-architect.md @@ -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 diff --git a/.kilo/agents/orchestrator.md b/.kilo/agents/orchestrator.md index 0f047e7..3ef8793 100755 --- a/.kilo/agents/orchestrator.md +++ b/.kilo/agents/orchestrator.md @@ -40,7 +40,6 @@ permission: "planner": allow "reflector": allow "memory-manager": allow - "devops-engineer": allow --- # Kilo Code: Orchestrator diff --git a/.kilo/agents/security-auditor.md b/.kilo/agents/security-auditor.md index 9de9794..b44857f 100755 --- a/.kilo/agents/security-auditor.md +++ b/.kilo/agents/security-auditor.md @@ -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 diff --git a/.kilo/agents/workflow-architect.md b/.kilo/agents/workflow-architect.md index 19d61da..7326932 100755 --- a/.kilo/agents/workflow-architect.md +++ b/.kilo/agents/workflow-architect.md @@ -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) diff --git a/.kilo/kilo.jsonc b/.kilo/kilo.jsonc index 65f5c99..07be968 100644 --- a/.kilo/kilo.jsonc +++ b/.kilo/kilo.jsonc @@ -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", diff --git a/.kilo/rules/agent-frontmatter-validation.md b/.kilo/rules/agent-frontmatter-validation.md index 94d7dff..47dad63 100644 --- a/.kilo/rules/agent-frontmatter-validation.md +++ b/.kilo/rules/agent-frontmatter-validation.md @@ -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 diff --git a/.kilo/rules/global.md b/.kilo/rules/global.md index 653c8a9..2379177 100644 --- a/.kilo/rules/global.md +++ b/.kilo/rules/global.md @@ -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` diff --git a/scripts/validate-agents.cjs b/scripts/validate-agents.cjs new file mode 100644 index 0000000..6c7e742 --- /dev/null +++ b/scripts/validate-agents.cjs @@ -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); +}