spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/phases/gate.ts4 * Description: The pure phase gate — per-phase capability policy over capability requests (v2 design §1).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { CapabilityRequest } from "../permissions/index.js";11import type { Phase } from "../session/index.js";12import { PHASE_GATE_BLOCKED } from "./types.js";1314/**15 * Read-only command prefixes usable in understand/design (collapsed-whitespace16 * prefix match). Deliberately conservative — anything else waits for implement.17 */18const READONLY_COMMAND_PREFIXES: readonly string[] = [19 "git status",20 "git diff",21 "git log",22 "git show",23 "git branch",24 "git blame",25 "ls",26 "cat ",27 "pwd",28 "which ",29 "wc ",30 "head ",31 "tail ",32 "grep ",33 "rg ",34 "find ",35 "file ",36 "du ",37 "tree",38 "node --version",39 "npm ls",40 "npm view",41];4243function isReadonlyCommand(subject: string): boolean {44 const collapsed = subject.replace(/\s+/g, " ").trim();45 return READONLY_COMMAND_PREFIXES.some(46 (prefix) => collapsed === prefix.trim() || collapsed.startsWith(prefix),47 );48}4950/** Design-phase writable area: docs/design/*.md under the project root. */51function isDesignDocPath(subject: string, projectRoot: string): boolean {52 const normalizedRoot = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;53 return (54 subject.startsWith(`${normalizedRoot}docs/design/`) &&55 subject.endsWith(".md") &&56 !subject.includes("..")57 );58}5960/** Project memory is writable in every phase — remembering IS understanding (v2 §5). */61function isMemoryPath(subject: string): boolean {62 return subject.endsWith("/.khaelor/MEMORY.md") || subject.endsWith(".khaelor/MEMORY.md");63}6465export type GateCheck = { allowed: true } | { allowed: false; feedback: string };6667function blocked(capability: string, subject: string, phase: Phase): GateCheck {68 return {69 allowed: false,70 feedback:71 `${PHASE_GATE_BLOCKED}: ${capability} for "${subject}" is not available in the "${phase}" phase. ` +72 `KHAELOR works in three phases: understand → design → implement. ` +73 `Finalize your design first: call the "design" tool with your goal, technical approach, ` +74 `the files you plan to touch, the risks, and how you will verify the result. ` +75 `Once the design is approved, implementation capabilities unlock.`,76 };77}7879/**80 * Evaluate one tool call's capability requests against the current phase.81 * The gate sits BEFORE the permission evaluation: a blocked call never82 * reaches the permission service (v2 design §1 — the Tool Runtime checks83 * the current phase before each dispatch).84 */85export function checkPhaseGate(86 phase: Phase,87 requests: readonly CapabilityRequest[],88 projectRoot: string,89): GateCheck {90 if (phase === "implement") return { allowed: true };9192 for (const request of requests) {93 switch (request.capability) {94 case "file.read":95 continue;96 case "file.write.project": {97 if (isMemoryPath(request.subject)) continue;98 if (phase === "design" && isDesignDocPath(request.subject, projectRoot)) continue;99 return blocked(request.capability, request.subject, phase);100 }101 case "file.write.outsideProject":102 return blocked(request.capability, request.subject, phase);103 case "process.execute":104 case "process.background": {105 if (isReadonlyCommand(request.subject)) continue;106 return blocked(request.capability, request.subject, phase);107 }108 case "network.access":109 case "git.modify":110 return blocked(request.capability, request.subject, phase);111 default:112 return blocked(request.capability, request.subject, phase);113 }114 }115 return { allowed: true };116}117