/** * KHAELOR * File: src/phases/gate.ts * Description: The pure phase gate — per-phase capability policy over capability requests (v2 design §1). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import type { CapabilityRequest } from "../permissions/index.js"; import type { Phase } from "../session/index.js"; import { PHASE_GATE_BLOCKED } from "./types.js"; /** * Read-only command prefixes usable in understand/design (collapsed-whitespace * prefix match). Deliberately conservative — anything else waits for implement. */ const READONLY_COMMAND_PREFIXES: readonly string[] = [ "git status", "git diff", "git log", "git show", "git branch", "git blame", "ls", "cat ", "pwd", "which ", "wc ", "head ", "tail ", "grep ", "rg ", "find ", "file ", "du ", "tree", "node --version", "npm ls", "npm view", ]; function isReadonlyCommand(subject: string): boolean { const collapsed = subject.replace(/\s+/g, " ").trim(); return READONLY_COMMAND_PREFIXES.some( (prefix) => collapsed === prefix.trim() || collapsed.startsWith(prefix), ); } /** Design-phase writable area: docs/design/*.md under the project root. */ function isDesignDocPath(subject: string, projectRoot: string): boolean { const normalizedRoot = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`; return ( subject.startsWith(`${normalizedRoot}docs/design/`) && subject.endsWith(".md") && !subject.includes("..") ); } /** Project memory is writable in every phase — remembering IS understanding (v2 §5). */ function isMemoryPath(subject: string): boolean { return subject.endsWith("/.khaelor/MEMORY.md") || subject.endsWith(".khaelor/MEMORY.md"); } export type GateCheck = { allowed: true } | { allowed: false; feedback: string }; function blocked(capability: string, subject: string, phase: Phase): GateCheck { return { allowed: false, feedback: `${PHASE_GATE_BLOCKED}: ${capability} for "${subject}" is not available in the "${phase}" phase. ` + `KHAELOR works in three phases: understand → design → implement. ` + `Finalize your design first: call the "design" tool with your goal, technical approach, ` + `the files you plan to touch, the risks, and how you will verify the result. ` + `Once the design is approved, implementation capabilities unlock.`, }; } /** * Evaluate one tool call's capability requests against the current phase. * The gate sits BEFORE the permission evaluation: a blocked call never * reaches the permission service (v2 design §1 — the Tool Runtime checks * the current phase before each dispatch). */ export function checkPhaseGate( phase: Phase, requests: readonly CapabilityRequest[], projectRoot: string, ): GateCheck { if (phase === "implement") return { allowed: true }; for (const request of requests) { switch (request.capability) { case "file.read": continue; case "file.write.project": { if (isMemoryPath(request.subject)) continue; if (phase === "design" && isDesignDocPath(request.subject, projectRoot)) continue; return blocked(request.capability, request.subject, phase); } case "file.write.outsideProject": return blocked(request.capability, request.subject, phase); case "process.execute": case "process.background": { if (isReadonlyCommand(request.subject)) continue; return blocked(request.capability, request.subject, phase); } case "network.access": case "git.modify": return blocked(request.capability, request.subject, phase); default: return blocked(request.capability, request.subject, phase); } } return { allowed: true }; }