SPB Git

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%
3.7 KB · 117 lines typescript
Raw Blame History
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