gates
List, fetch, or execute reusable workflow gates for regression, confidence checks, and stage-specific actions across projects.
Instructions
List, fetch, or execute reusable workflow gates (regression, confidence, etc).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | ||
| id | No | For action=get or action=run | |
| stage | No | For action=for-stage | |
| projectRoot | No | For action=run | |
| autoSpawn | No | On action=run: auto-start the sidecar UI (kit ui) if not running and stream progress to it. |
Implementation Reference
- src/mcp-server/index.js:76-90 (schema)Input schema for the 'gates' MCP tool, defining actions: list, get, for-stage, run, with corresponding parameters.
{ name: 'gates', description: 'List, fetch, or execute reusable workflow gates (regression, confidence, etc).', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'get', 'for-stage', 'run'] }, id: { type: 'string', description: 'For action=get or action=run' }, stage: { type: 'string', enum: ['pre-plan', 'pre-execute', 'pre-verify', 'post-verify', 'any'], description: 'For action=for-stage' }, projectRoot: { type: 'string', description: 'For action=run' }, autoSpawn: { type: 'boolean', description: 'On action=run: auto-start the sidecar UI (kit ui) if not running and stream progress to it.' }, }, required: ['action'], }, }, - src/mcp-server/index.js:246-253 (registration)Registration of the 'gates' tool handler in the HANDLERS map, which is dispatched by the MCP CallToolRequestSchema handler.
const HANDLERS = { kit: handleKit, sync: handleSync, 'reverse-sync':handleReverseSync, gates: handleGates, forensics: handleForensics, install: handleInstall, }; - src/mcp-server/index.js:197-211 (handler)handleGates — the MCP tool handler that dispatches actions: list (listGates), get (getGate), for-stage (gatesForStage), run (runGate).
async function handleGates(args) { switch (args.action) { case 'list': return listGates(); case 'get': return getGate(args.id); case 'for-stage': return gatesForStage(args.stage); case 'run': return withAutoSpawn(args, 'gates.run', () => runGate(args.id, { projectRoot: args.projectRoot, yes: true, // MCP context: never prompt interactive: false, // MCP never prompts })); default: return { error: `Unknown action: ${args.action}` }; } } - src/core/gates.js:25-68 (handler)Core gate functions: listGates (reads gates/*.md files with frontmatter), getGate (fetches a single gate by ID), gatesForStage (filters gates by stage).
export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) { let entries; try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); } catch { return []; } const out = []; for (const e of entries) { if (!e.isFile() || !e.name.endsWith('.md')) continue; const abs = path.join(gatesRoot, e.name); const raw = await fs.readFile(abs, 'utf8'); const meta = parseFrontmatter(raw); out.push({ id: meta.id ?? e.name.replace(/\.md$/, ''), stage: meta.stage ?? 'any', blocking: meta.blocking !== false && meta.blocking !== 'false', description: meta.description ?? '', absPath: abs, }); } return out.sort((a, b) => a.id.localeCompare(b.id)); } export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) { const all = await listGates(gatesRoot); const g = all.find(x => x.id === id); if (!g) throw new Error(`Unknown gate: ${id}. Available: ${all.map(x => x.id).join(', ')}`); const raw = await fs.readFile(g.absPath, 'utf8'); return { ...g, content: raw }; } export async function gatesForStage(stage, gatesRoot = DEFAULT_GATES_ROOT) { const all = await listGates(gatesRoot); return all.filter(g => g.stage === stage || g.stage === 'any'); } function parseFrontmatter(raw) { const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!m) return {}; const out = {}; for (const line of m[1].split(/\r?\n/)) { const mm = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); if (mm) out[mm[1]] = mm[2].trim(); } return out; } - src/core/gate-runner.js:25-193 (helper)runGate — executes a gate either as a shell script (extracts ## Check section code blocks) or manual mode, returning a structured verdict.
export async function runGate(id, opts = {}) { const projectRoot = path.resolve(opts.projectRoot ?? process.cwd()); const yes = !!opts.yes; const onLog = opts.onLog ?? ((s) => stderr.write(s + '\n')); const interactive = opts.interactive !== false && !yes; const gate = await getGate(id, opts.gatesRoot); const parsed = parseGateBody(gate.content); onLog(''); onLog(`Gate: ${gate.id} [stage=${gate.stage}, blocking=${gate.blocking}]`); if (gate.description) onLog(`Description: ${gate.description}`); onLog(''); if (parsed.shellBlocks.length > 0) { return runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog }); } return runManualGate(gate, parsed, { projectRoot, interactive, onLog }); } // --- shell-mode gates --- async function runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog }) { const script = parsed.shellBlocks.join('\n\n'); onLog(`Will execute (cwd=${projectRoot}):`); onLog('─────'); onLog(script); onLog('─────'); let proceed = yes; if (interactive && !yes) { proceed = await ask('execute? [y/N] '); } if (!proceed) { return { id: gate.id, verdict: 'skipped', blocking: gate.blocking, reason: 'user declined or non-interactive without --yes' }; } const { exitCode, stdout, stderr: errOut } = await execScript(script, projectRoot); const verdict = mapVerdict(exitCode, gate); onLog(`exit=${exitCode} → verdict=${verdict}`); return { id: gate.id, verdict, blocking: gate.blocking, exitCode, stdout: trim(stdout), stderr: trim(errOut), }; } // --- manual-mode gates --- async function runManualGate(gate, parsed, { projectRoot, interactive, onLog }) { onLog('This gate has no executable check. Body:'); onLog('─────'); onLog(parsed.body.trim()); onLog('─────'); if (!interactive) { return { id: gate.id, verdict: 'manual', blocking: gate.blocking, reason: 'manual gate; no auto-decision in non-interactive mode' }; } const choice = await askChoice('verdict? [p]assed / [w]arn / [b]lock / [s]kip: ', { p: 'passed', w: 'warn', b: 'block', s: 'skipped', }); return { id: gate.id, verdict: choice, blocking: gate.blocking }; } // --- parsing --- function parseGateBody(content) { const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ''); const checkSection = extractSection(body, 'Check'); const shellBlocks = extractCodeBlocks(checkSection || ''); return { body, checkSection, shellBlocks }; } function extractSection(body, heading) { // Line-by-line: find `## Heading`, capture everything until the next `## ` or EOF. // Plain regex with `\Z` doesn't exist in JS, and `(?=^##|$)` is awkward — easier this way. const lines = body.split(/\r?\n/); const startRe = new RegExp(`^##\\s+${heading}\\s*$`, 'i'); let start = -1, end = lines.length; for (let i = 0; i < lines.length; i++) { if (startRe.test(lines[i])) { start = i + 1; break; } } if (start === -1) return null; for (let i = start; i < lines.length; i++) { if (/^##\s+/.test(lines[i])) { end = i; break; } } return lines.slice(start, end).join('\n').trim(); } function extractCodeBlocks(text) { const out = []; const re = /```(?:bash|sh|shell)?\s*\n([\s\S]*?)```/g; let m; while ((m = re.exec(text)) !== null) { const code = m[1].trim(); if (code) out.push(code); } return out; } // --- exec --- async function execScript(script, cwd) { // Write to a temp file and run with bash. We don't try to inline -c because // the scripts can be multiline and contain quoting we'd have to escape. const tmp = path.join(os.tmpdir(), `kit-gate-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`); await fs.writeFile(tmp, script, { encoding: 'utf8', mode: 0o700 }); try { const child = spawn('bash', [tmp], { cwd, env: process.env }); const stdout = [], stderrOut = []; child.stdout.on('data', (b) => stdout.push(b)); child.stderr.on('data', (b) => stderrOut.push(b)); const exitCode = await new Promise((resolve, reject) => { child.on('error', (e) => reject(new Error(`failed to spawn bash: ${e.message}. Install Git Bash or WSL on Windows.`))); child.on('close', resolve); }); return { exitCode: exitCode ?? -1, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderrOut).toString('utf8'), }; } finally { await fs.unlink(tmp).catch(() => {}); } } // --- verdict mapping --- function mapVerdict(exitCode, gate) { if (exitCode === 0) return 'passed'; return gate.blocking ? 'block' : 'warn'; } // --- prompts --- async function ask(question) { const rl = createInterface({ input, output }); try { const a = (await rl.question(question)).trim().toLowerCase(); return a === 'y' || a === 'yes'; } finally { rl.close(); } } async function askChoice(question, mapping) { const rl = createInterface({ input, output }); try { while (true) { const a = (await rl.question(question)).trim().toLowerCase(); if (mapping[a]) return mapping[a]; output.write(`unknown choice "${a}". try one of: ${Object.keys(mapping).join(', ')}\n`); } } finally { rl.close(); } } function trim(s) { if (!s) return s; return s.length > 4000 ? s.slice(0, 4000) + `\n…(truncated, ${s.length} bytes total)` : s; }