Skip to content
Development
Hook

Hooks

What beast-forge runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
beast-forge
253 skills21 agents2 commands3 hooks
Install
$ npx -y skills add malakhov-dmitrii/forge --agent claude-code

Ships with beast-forge. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • bun ${CLAUDE_PLUGIN_ROOT}/hooks/forge-hooks.mjs sessionstart

SessionEnd

  • bun ${CLAUDE_PLUGIN_ROOT}/hooks/forge-hooks.mjs sessionend

PreCompact

  • bun ${CLAUDE_PLUGIN_ROOT}/hooks/forge-hooks.mjs precompact
Read hooks/hooks.json

In the plugin's words

How beast-forge describes its own hook set.

Beast-forge plugin hooks. Stop hook removed in v3 — ralph provides persistence instead.

Where it lives

  • hooks/discover-skills.shGitHub
    Read the script
    #!/bin/bash
    # Dynamic skill discovery for beast-plan
    # Finds skills by matching keywords in YAML frontmatter descriptions
    # Returns JSON array of top 3 matches: [{name, path, score}]
    set -euo pipefail
    
    TASK_DESC="${1:-}"
    SEARCH_ROOT="${SKILL_SEARCH_ROOT:-$HOME/.claude}"
    MAX_RESULTS=3
    
    # Stop words to filter out (common words that don't indicate skill relevance)
    STOP_WORDS="a an the is are was were be been being have has had do does did will would shall should may might can could of in to for on with at by from as into through during before after above below between under again further then once here there when where why how all each every both few more most other some such no nor not only own same so than too very just"
    
    # Fail-safe: return empty array on any error
    trap 'echo "[]"; exit 0' ERR
    
    # Return empty array if no task description
    if [[ -z "$TASK_DESC" ]]; then
      echo "[]"
      exit 0
    fi
    
    # Extract keywords from task description
    # 1. Convert to lowercase
    # 2. Remove punctuation
    # 3. Split into words
    # 4. Filter out stop words
    # 5. Keep unique words
    extract_keywords() {
      local text="$1"
      local keywords=""
    
      # Lowercase and remove punctuation
      text=$(echo "$text" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' ' ')
    
      # Split into words and filter
      for word in $text; do
        # Skip short words (< 3 chars) and stop words
        if [[ ${#word} -lt 3 ]]; then
          continue
        fi
    
        # Check if word is a stop word
        local is_stop=0
        for stop in $STOP_WORDS; do
          if [[ "$word" == "$stop" ]]; then
            is_stop=1
            break
          fi
        done
    
        if [[ $is_stop -eq 0 ]]; then
          # Add to keywords if not already present
          if ! echo "$keywords" | grep -qw "$word"; then
            keywords="$keywords $word"
          fi
        fi
      done
    
      echo "$keywords"
    }
    
    # Find all SKILL.md files under ~/.claude
    find_skill_files() {
      # Find all SKILL.md files in skills directories
      # Pattern: ~/.claude/**/skills/*/SKILL.md
      find "$SEARCH_ROOT" -type f -path "*/skills/*/SKILL.md" 2>/dev/null || true
    }
    
    # Parse YAML frontmatter from SKILL.md and extract name/description
    parse_skill_metadata() {
      local file="$1"
      local in_frontmatter=0
      local name=""
      local desc=""
    
      while IFS= read -r line; do
        # Detect start of frontmatter
        if [[ "$line" == "---" ]]; then
          if [[ $in_frontmatter -eq 0 ]]; then
            in_frontmatter=1
            continue
          else
            # End of frontmatter
            break
          fi
        fi
    
        if [[ $in_frontmatter -eq 1 ]]; then
          # Extract name
          if [[ "$line" =~ ^name:\ *(.+)$ ]]; then
            name="${BASH_REMATCH[1]}"
          fi
          # Extract description
          if [[ "$line" =~ ^description:\ *(.+)$ ]]; then
            desc="${BASH_REMATCH[1]}"
          fi
        fi
      done < "$file"
    
      echo "$name|$desc"
    }
    
    # Score a skill based on keyword matches in description
    score_skill() {
      local desc="$1"
      local keywords="$2"
      local score=0
    
      # Convert description to lowercase for matching
      desc=$(echo "$desc" | tr '[:upper:]' '[:lower:]')
    
      # Count keyword matches
      for keyword in $keywords; do
        # Use grep -o to count occurrences
        local count=$(echo "$desc" | grep -o "$keyword" | wc -l | tr -d ' ')
        score=$((score + count))
      done
    
      echo "$score"
    }
    
    # Main execution
    KEYWORDS=$(extract_keywords "$TASK_DESC")
    
    # If no keywords extracted, return empty array
    if [[ -z "$KEYWORDS" ]]; then
      echo "[]"
      exit 0
    fi
    
    # Find and score all skills
    SCORED=""
    SEEN_NAMES=""
    
    # Use temp file for deduplication (bash 3.2 doesn't have associative arrays)
    TEMP_FILE=$(mktemp)
    trap 'rm -f "$TEMP_FILE"; echo "[]"; exit 0' ERR
    
    # Collect all skills first, then process
    while IFS= read -r skill_file; do
      # Parse metadata
      metadata=$(parse_skill_metadata "$skill_file")
      name=$(echo "$metadata" | cut -d'|' -f1)
      desc=$(echo "$metadata" | cut -d'|' -f2)
    
      # Skip if no name
      if [[ -z "$name" ]]; then
        continue
      fi
    
      # Store in temp file: name|desc|path
      echo "$name|$desc|$skill_file" >> "$TEMP_FILE"
    done < <(find_skill_files)
    
    # Deduplicate by name (keep first occurrence) and score
    # Sort by name, use uniq to keep first, then score
    while IFS='|' read -r name desc skill_file; do
      # Score this skill
      score=$(score_skill "$desc" "$KEYWORDS")
    
      # Only include skills with score > 0
      if [[ $score -gt 0 ]]; then
        SCORED="$SCORED$score	$name	$skill_file
    "
      fi
    done < <(sort -t'|' -k1,1 -u "$TEMP_FILE")
    
    # Clean up temp file
    rm -f "$TEMP_FILE"
    
    # If no matches, return empty array
    if [[ -z "$SCORED" ]]; then
      echo "[]"
      exit 0
    fi
    
    # Sort by score (descending), take top N, format as JSON
    # FIX (Skeptic Mirage 1): Filter out empty lines before JSON generation
    echo "$SCORED" | grep -v '^$' | sort -t$'	' -k1 -nr | head -n "$MAX_RESULTS" | awk -F'	' '
    BEGIN {
      printf "["
      first = 1
    }
    {
      if (!first) printf ","
      first = 0
      # Escape double quotes and backslashes in strings for JSON
      gsub(/"/, "\\\"", $2)
      gsub(/\\/, "\\\\", $2)
      gsub(/"/, "\\\"", $3)
      gsub(/\\/, "\\\\", $3)
      printf "\n  {\"name\": \"%s\", \"path\": \"%s\", \"score\": %d}", $2, $3, $1
    }
    END {
      if (!first) printf "\n"
      printf "]\n"
    }'
    
    exit 0
    
  • hooks/forge-crud.mjsGitHub
    Read the script
    /**
     * Forge Intelligence — CRUD Operations
     *
     * Create/park/resume/complete/abandon forges.
     * Record gates, spikes. Aggregate risk. Update co-failures.
     */
    
    import { openForgeDb } from "./forge-schema.mjs";
    
    // ── DB helper ──────────────────────────────────────────
    /** Open db, run fn(db), close unconditionally. Returns fn's result. */
    function withDb(cwd, fn) {
      const db = openForgeDb(cwd);
      try { return fn(db); } finally { db.close(); }
    }
    
    // ── Forge CRUD ──────────────────────────────────────────
    
    export function createForge(cwd, { slug, systems = [], parentId = null, priority = "medium" }) {
      const db = openForgeDb(cwd);
      try {
        const result = db.run(
          `INSERT INTO forges (slug, parent_id, systems, priority, status, phase)
           VALUES (?, ?, ?, ?, 'active', 'bf-precedent')`,
          [slug, parentId, JSON.stringify(systems), priority]
        );
        saveCurrentState(db, { forgeId: result.lastInsertRowid, slug, phase: "bf-precedent", iteration: 1 });
        return result.lastInsertRowid;
      } finally { db.close(); }
    }
    
    export function parkForge(cwd, forgeId, reason = null) {
      const db = openForgeDb(cwd);
      try {
        db.run(
          `UPDATE forges SET status = 'parked', parked_at = datetime('now'),
           updated_at = datetime('now'), blocking_reason = ?
           WHERE id = ? AND status = 'active'`,
          [reason, forgeId]
        );
        clearCurrentState(db);
      } finally { db.close(); }
    }
    
    export function resumeForge(cwd, slugOrId) {
      const db = openForgeDb(cwd);
      try {
        const forge = typeof slugOrId === "number"
          ? db.query("SELECT * FROM forges WHERE id = ?").get(slugOrId)
          : db.query("SELECT * FROM forges WHERE slug = ?").get(slugOrId);
        if (!forge) throw new Error(`Forge not found: ${slugOrId}`);
        if (forge.status !== "parked") throw new Error(`Forge "${forge.slug}" is ${forge.status}, not parked`);
    
        db.run(
          `UPDATE forges SET status = 'active', parked_at = NULL, updated_at = datetime('now')
           WHERE id = ?`, [forge.id]
        );
        saveCurrentState(db, {
          forgeId: forge.id, slug: forge.slug, phase: forge.phase,
          iteration: forge.iteration, context: forge.context
        });
        return forge;
      } finally { db.close(); }
    }
    
    export function completeForge(cwd, forgeId, lesson = null) {
      const db = openForgeDb(cwd);
      try {
        db.run(
          `UPDATE forges SET status = 'completed', completed_at = datetime('now'),
           updated_at = datetime('now'), context = json_set(COALESCE(context, '{}'), '$.lesson', ?)
           WHERE id = ?`,
          [lesson, forgeId]
        );
        updateCoFailures(db, forgeId);
        clearCurrentState(db);
      } finally { db.close(); }
    }
    
    export function abandonForge(cwd, forgeId, reason = null) {
      const db = openForgeDb(cwd);
      try {
        db.run(
          `UPDATE forges SET status = 'abandoned', completed_at = datetime('now'),
           updated_at = datetime('now'), blocking_reason = ?
           WHERE id = ?`,
          [reason, forgeId]
        );
        clearCurrentState(db);
      } finally { db.close(); }
    }
    
    export function updateForgePhase(cwd, forgeId, phase, iteration = null) {
      const db = openForgeDb(cwd);
      try {
        if (iteration !== null) {
          db.run(
            `UPDATE forges SET phase = ?, iteration = ?, updated_at = datetime('now') WHERE id = ?`,
            [phase, iteration, forgeId]
          );
        } else {
          db.run(
            `UPDATE forges SET phase = ?, updated_at = datetime('now') WHERE id = ?`,
            [phase, forgeId]
          );
        }
        const forge = db.query("SELECT slug, iteration FROM forges WHERE id = ?").get(forgeId);
        saveCurrentState(db, { forgeId, slug: forge?.slug, phase, iteration: iteration ?? forge?.iteration });
      } finally { db.close(); }
    }
    
    export function spawnForge(cwd, { slug, systems, parentId, blocksParent = false, priority = "medium" }) {
      const db = openForgeDb(cwd);
      try {
        const result = db.run(
          `INSERT INTO forges (slug, parent_id, systems, priority, status, phase)
           VALUES (?, ?, ?, ?, 'active', 'bf-precedent')`,
          [slug, parentId, JSON.stringify(systems), priority]
        );
        if (blocksParent) {
          db.run(
            `UPDATE forges SET status = 'blocked', blocked_by = ?, blocking_reason = 'waiting for: ' || ?
             WHERE id = ?`,
            [result.lastInsertRowid, slug, parentId]
          );
        }
        return result.lastInsertRowid;
      } finally { db.close(); }
    }
    
    // ── Gates ───────────────────────────────────────────────
    
    export function recordGate(cwd, forgeId, iteration, gate, result, findings = [], { blind = 1, inputsSeen = [], metaFindings = [] } = {}) {
      const db = openForgeDb(cwd);
      try {
        db.run(
          `INSERT OR REPLACE INTO gates (forge_id, iteration, gate, result, findings, blind, inputs_seen, meta_findings)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
          [forgeId, iteration, gate, result, JSON.stringify(findings), blind, JSON.stringify(inputsSeen), JSON.stringify(metaFindings)]
        );
      } finally { db.close(); }
    }
    
    // ── Spikes ──────────────────────────────────────────────
    
    export function recordSpike(cwd, forgeId, assumption, result, actual = null) {
      const db = openForgeDb(cwd);
      try {
        const permanent = result === "refuted" ? 1 : 0;
        db.run(
          `INSERT INTO spikes (forge_id, assumption, result, actual, permanent) VALUES (?, ?, ?, ?, ?)`,
          [forgeId, assumption, result, actual, permanent]
        );
      } finally { db.close(); }
    }
    
    export function searchSpikes(cwd, query) {
      const db = openForgeDb(cwd);
      try {
        return db.query(
          `SELECT s.*, sf.rank FROM spikes_fts sf
           JOIN spikes s ON s.id = sf.rowid
           WHERE spikes_fts MATCH ?
           AND (s.permanent = 1 OR s.tested_at > datetime('now', '-30 days'))
           ORDER BY sf.rank LIMIT 10`
        ).all(query);
      } finally { db.close(); }
    }
    
    // ── Visionary ──────────────────────────────────────────
    
    export function recordVisionaryPass(cwd, forgeId, iteration, { passNumber, angle, agent, content }) {
      const db = openForgeDb(cwd);
      try {
        db.run(
          `INSERT OR IGNORE INTO visionary_passes (forge
  • hooks/forge-global.mjsGitHub
    Read the script
    /**
     * Forge Intelligence — Global Knowledge DB
     *
     * ~/.forge/global.db — cross-project verified knowledge.
     * Spikes about tools/libraries, abstract patterns, process learnings.
     * Future: sync with Forge Knowledge Network (forge-beast.dev).
     */
    
    import { Database } from "bun:sqlite";
    import { existsSync, mkdirSync } from "node:fs";
    import { join } from "node:path";
    import { homedir } from "node:os";
    
    const GLOBAL_SCHEMA_VERSION = 1;
    
    /** Open (or create) global forge DB */
    export function openGlobalDb() {
      const forgeDir = join(homedir(), ".forge");
      if (!existsSync(forgeDir)) mkdirSync(forgeDir, { recursive: true });
    
      const dbPath = join(forgeDir, "global.db");
      const db = new Database(dbPath);
    
      db.run("PRAGMA journal_mode = WAL");
      db.run("PRAGMA busy_timeout = 5000");
    
      migrateGlobalIfNeeded(db);
      return db;
    }
    
    function migrateGlobalIfNeeded(db) {
      const currentVersion = db.query("PRAGMA user_version").get().user_version;
      if (currentVersion >= GLOBAL_SCHEMA_VERSION) return;
    
      if (currentVersion < 1) ensureGlobalSchemaV1(db);
    
      db.run(`PRAGMA user_version = ${GLOBAL_SCHEMA_VERSION}`);
    }
    
    function ensureGlobalSchemaV1(db) {
      db.run(`CREATE TABLE IF NOT EXISTS global_spikes (
        id INTEGER PRIMARY KEY,
        assumption TEXT NOT NULL,
        result TEXT CHECK(result IN ('confirmed','refuted')),
        actual TEXT,
        technology TEXT,
        tested_at TEXT DEFAULT (datetime('now')),
        source_project TEXT,
        permanent INTEGER DEFAULT 0,
        network_id TEXT,
        network_status TEXT DEFAULT 'local'
          CHECK(network_status IN ('local','pending','verified','rejected'))
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS global_patterns (
        id INTEGER PRIMARY KEY,
        pattern TEXT NOT NULL,
        category TEXT CHECK(category IN ('integration','architecture','process','tooling','security')),
        confidence REAL DEFAULT 0.5,
        evidence_count INTEGER DEFAULT 1,
        discovered_at TEXT DEFAULT (datetime('now')),
        last_confirmed TEXT,
        network_id TEXT,
        network_status TEXT DEFAULT 'local'
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS process_learnings (
        id INTEGER PRIMARY KEY,
        learning TEXT NOT NULL,
        context TEXT,
        evidence_count INTEGER DEFAULT 1,
        discovered_at TEXT DEFAULT (datetime('now')),
        network_id TEXT,
        network_status TEXT DEFAULT 'local'
      )`);
    
      db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS global_spikes_fts USING fts5(
        assumption, actual, technology, content=global_spikes, content_rowid=id
      )`);
    
      db.run(`CREATE TRIGGER IF NOT EXISTS global_spikes_ai AFTER INSERT ON global_spikes BEGIN
        INSERT INTO global_spikes_fts(rowid, assumption, actual, technology)
        VALUES (new.id, new.assumption, new.actual, new.technology);
      END`);
    
      db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS global_patterns_fts USING fts5(
        pattern, category, content=global_patterns, content_rowid=id
      )`);
    
      db.run(`CREATE TRIGGER IF NOT EXISTS global_patterns_ai AFTER INSERT ON global_patterns BEGIN
        INSERT INTO global_patterns_fts(rowid, pattern, category)
        VALUES (new.id, new.pattern, new.category);
      END`);
    }
    
    // ── CRUD ────────────────────────────────────────────────
    
    export function promoteSpike(spike, sourceProject = null) {
      const db = openGlobalDb();
      try {
        const existing = db.query(
          "SELECT id FROM global_spikes WHERE assumption = ? AND technology = ?"
        ).get(spike.assumption, spike.technology);
    
        if (existing) {
          db.run("UPDATE global_spikes SET tested_at = datetime('now') WHERE id = ?", [existing.id]);
          return existing.id;
        }
    
        const result = db.run(
          `INSERT INTO global_spikes (assumption, result, actual, technology, source_project, permanent)
           VALUES (?, ?, ?, ?, ?, ?)`,
          [spike.assumption, spike.result, spike.actual, spike.technology, sourceProject, spike.permanent ? 1 : 0]
        );
        return result.lastInsertRowid;
      } finally { db.close(); }
    }
    
    export function addPattern(pattern, category) {
      const db = openGlobalDb();
      try {
        const existing = db.query("SELECT id FROM global_patterns WHERE pattern = ?").get(pattern);
    
        if (existing) {
          db.run(
            `UPDATE global_patterns SET confidence = MIN(confidence + 0.1, 1.0),
             evidence_count = evidence_count + 1, last_confirmed = datetime('now')
             WHERE id = ?`, [existing.id]
          );
          return existing.id;
        }
    
        const result = db.run(
          "INSERT INTO global_patterns (pattern, category) VALUES (?, ?)", [pattern, category]
        );
        return result.lastInsertRowid;
      } finally { db.close(); }
    }
    
    export function addProcessLearning(learning, context = null) {
      const db = openGlobalDb();
      try {
        const existing = db.query("SELECT id FROM process_learnings WHERE learning = ?").get(learning);
    
        if (existing) {
          db.run("UPDATE process_learnings SET evidence_count = evidence_count + 1 WHERE id = ?", [existing.id]);
          return existing.id;
        }
    
        const result = db.run(
          "INSERT INTO process_learnings (learning, context) VALUES (?, ?)", [learning, context]
        );
        return result.lastInsertRowid;
      } finally { db.close(); }
    }
    
    export function searchGlobalSpikes(query, technology = null) {
      const db = openGlobalDb();
      try {
        if (technology) {
          return db.query(
            `SELECT * FROM global_spikes WHERE technology = ?
             AND (permanent = 1 OR tested_at > datetime('now', '-30 days'))
             ORDER BY tested_at DESC LIMIT 20`
          ).all(technology);
        }
        return db.query(
          `SELECT s.* FROM global_spikes_fts f
           JOIN global_spikes s ON s.id = f.rowid
           WHERE global_spikes_fts MATCH ?
           ORDER BY f.rank LIMIT 20`
        ).all(query);
      } finally { db.close(); }
    }
    
    export function searchGlobalPatterns(query) {
      const db = openGlobalDb();
      try {
        return db.query(
          `SELECT p.* FROM global_patterns_fts f
           JOIN global_patterns p ON p.id = f.rowid
           WHERE global_patterns_fts MATCH ?
           ORDER BY p.confidence DESC LIMIT 20`
        ).all(query);
      } finally { 
  • hooks/forge-hooks.mjsRunsGitHub
    Read the script
    #!/usr/bin/env bun
    /**
     * Forge Intelligence — Hook Handlers
     *
     * Dispatched by CLI arg: sessionstart | sessionend | precompact
     * Registered in ~/.claude/settings.json (user-level, not plugin).
     *
     * SessionStart: health check (stale, zombies, orphans, phantom blocks)
     * SessionEnd:   aggregate risk, reconcile with git, complete current_state bookkeeping
     * PreCompact:   save forge state, return recovery prompt as systemMessage
     */
    
    import { openForgeDb, resolveOmcRoot } from "./forge-schema.mjs";
    import { getCurrentState } from "./forge-crud.mjs";
    import { existsSync } from "node:fs";
    import { execSync } from "node:child_process";
    import { join } from "node:path";
    
    const cwd = process.env.PWD || process.cwd();
    const command = process.argv[2];
    
    // Read stdin (Claude Code passes JSON context)
    let stdin = {};
    try {
      const chunks = [];
      for await (const chunk of Bun.stdin.stream()) {
        chunks.push(chunk);
      }
      if (chunks.length > 0) {
        const text = Buffer.concat(chunks).toString();
        if (text.trim()) stdin = JSON.parse(text);
      }
    } catch { /* no stdin or invalid JSON — fine */ }
    
    // Resolve cwd from stdin or env
    const projectCwd = stdin.cwd || cwd;
    
    // Check if forge.db exists — skip silently if no forge activity in this project
    const omcRoot = resolveOmcRoot(projectCwd);
    const forgeDbPath = join(omcRoot, "forge.db");
    const forgeDbExists = existsSync(forgeDbPath);
    
    try {
      switch (command) {
        case "sessionstart":
          await handleSessionStart();
          break;
        case "sessionend":
          await handleSessionEnd();
          break;
        case "precompact":
          await handlePreCompact();
          break;
        default:
          break;
      }
    } catch (err) {
      console.error(`[forge-hooks] ${command} error:`, err.message);
    }
    
    // ── SessionStart: Health Check ──────────────────────────
    
    async function handleSessionStart() {
      if (!forgeDbExists) return;
    
      const db = openForgeDb(projectCwd);
      try {
        const issues = [];
    
        // 1. Auto-park zombie forges (active but not updated in >2 hours)
        const zombies = db.query(`
          SELECT id, slug, phase, updated_at FROM forges
          WHERE status = 'active' AND updated_at < datetime('now', '-2 hours')
        `).all();
    
        for (const z of zombies) {
          db.run(`UPDATE forges SET status = 'parked', parked_at = updated_at,
            blocking_reason = 'auto-parked: zombie (last active ' || updated_at || ')',
            updated_at = datetime('now') WHERE id = ?`, [z.id]);
          issues.push(`auto-parked zombie "${z.slug}" (${z.phase}, last active ${z.updated_at})`);
        }
    
        // 2. Phantom blocks (blocked by completed/abandoned forge)
        const phantoms = db.query(`
          SELECT f.id, f.slug, b.slug as blocker_slug, b.status as blocker_status
          FROM forges f JOIN forges b ON f.blocked_by = b.id
          WHERE f.status = 'blocked' AND b.status IN ('completed', 'abandoned')
        `).all();
    
        for (const p of phantoms) {
          db.run(`UPDATE forges SET status = 'parked', blocked_by = NULL,
            blocking_reason = 'auto-unblocked: blocker "' || ? || '" ' || ?,
            updated_at = datetime('now') WHERE id = ?`,
            [p.blocker_slug, p.blocker_status, p.id]);
          issues.push(`auto-unblocked "${p.slug}" (blocker "${p.blocker_slug}" ${p.blocker_status})`);
        }
    
        // 3. Stale parked forges
        const stale = db.query(`
          SELECT slug, phase,
            CAST(julianday('now') - julianday(COALESCE(parked_at, updated_at)) AS INTEGER) as days
          FROM forges WHERE status = 'parked'
          AND COALESCE(parked_at, updated_at) < datetime('now', '-7 days')
          ORDER BY days DESC
        `).all();
    
        for (const s of stale) {
          if (s.days > 30) {
            issues.push(`stale ${s.days}d "${s.slug}" at ${s.phase} — consider /forge --abandon`);
          } else {
            issues.push(`parked ${s.days}d "${s.slug}" at ${s.phase}`);
          }
        }
    
        // 4. Orphan children (parent done, children still open)
        const orphans = db.query(`
          SELECT c.slug, p.slug as parent_slug, p.status as parent_status
          FROM forges c JOIN forges p ON c.parent_id = p.id
          WHERE c.status IN ('parked', 'blocked')
          AND p.status IN ('completed', 'abandoned')
        `).all();
    
        for (const o of orphans) {
          issues.push(`orphan "${o.slug}" (parent "${o.parent_slug}" ${o.parent_status})`);
        }
    
        // 5. Summary counts
        const counts = db.query(`
          SELECT status, COUNT(*) as c FROM forges
          WHERE status IN ('active', 'parked', 'blocked')
          GROUP BY status
        `).all();
    
        if (issues.length > 0 || counts.length > 0) {
          const countStr = counts.map(c => `${c.c} ${c.status}`).join(", ");
          const header = countStr ? `FORGES: ${countStr}` : "FORGES: none active";
          const body = issues.length > 0
            ? issues.map(i => `  ${i}`).join("\n")
            : "";
    
          const message = body ? `${header}\n${body}` : header;
          console.log(JSON.stringify({ continue: true, systemMessage: message }));
        }
      } finally { db.close(); }
    }
    
    // ── SessionEnd: Reconcile + Aggregate ───────────────────
    
    async function handleSessionEnd() {
      if (!forgeDbExists) return;
    
      const db = openForgeDb(projectCwd);
      try {
        let changedFiles = [];
        try {
          const porcelain = execSync("git status --porcelain", { cwd: projectCwd, encoding: "utf-8" });
          changedFiles = porcelain.split("\n")
            .filter(Boolean)
            .map(line => line.slice(3).trim());
        } catch { /* not a git repo or git error */ }
    
        if (changedFiles.length === 0) return;
    
        const openForges = db.query(`
          SELECT id, slug, systems, status FROM forges
          WHERE status IN ('parked', 'blocked')
        `).all();
    
        for (const forge of openForges) {
          const systems = JSON.parse(forge.systems || "[]");
          const touched = systems.some(sys =>
            changedFiles.some(f => f.toLowerCase().includes(sys.toLowerCase()))
          );
    
          if (touched) {
            const touchedSystems = systems.filter(sys =>
              changedFiles.some(f => f.toLowerCase().includes(sys.toLowerCase()))
            )
  • hooks/forge-schema.mjsGitHub
    Read the script
    /**
     * Forge Intelligence — Schema & DB Init
     *
     * Creates and migrates forge.db (project-scoped).
     * Tables: forges, gates, spikes, co_failures, current_state
     * Views: system_risk
     * Triggers: cascade on status change
     */
    
    import { Database } from "bun:sqlite";
    import { existsSync, mkdirSync } from "node:fs";
    import { join } from "node:path";
    import { execSync } from "node:child_process";
    
    /**
     * Schema version. Bump this when adding tables/columns/triggers.
     * Migration functions handle upgrading from any previous version.
     */
    const SCHEMA_VERSION = 7;
    
    /** Resolve project .omc/ directory (git worktree aware) */
    export function resolveOmcRoot(cwd) {
      try {
        const root = execSync("git rev-parse --show-toplevel", {
          cwd,
          encoding: "utf-8",
          stdio: ["ignore", "pipe", "ignore"],
        }).trim();
        return join(root, ".omc");
      } catch {
        return join(cwd, ".omc");
      }
    }
    
    /** Open (or create) forge.db with WAL mode and busy timeout */
    export function openForgeDb(cwd) {
      const omcRoot = resolveOmcRoot(cwd);
      if (!existsSync(omcRoot)) mkdirSync(omcRoot, { recursive: true });
    
      const dbPath = join(omcRoot, "forge.db");
      const db = new Database(dbPath);
    
      db.run("PRAGMA journal_mode = WAL");
      db.run("PRAGMA busy_timeout = 5000");
      db.run("PRAGMA foreign_keys = ON");
    
      migrateIfNeeded(db);
      return db;
    }
    
    /** Check current version and run migrations */
    function migrateIfNeeded(db) {
      const currentVersion = db.query("PRAGMA user_version").get().user_version;
    
      if (currentVersion >= SCHEMA_VERSION) return;
    
      if (currentVersion < 1) {
        ensureSchemaV1(db);
      }
    
      if (currentVersion < 2) {
        migrateV1toV2(db);
      }
    
      if (currentVersion < 3) {
        migrateV2toV3(db);
      }
    
      if (currentVersion < 4) {
        migrateV3toV4(db);
      }
    
      if (currentVersion < 5) {
        migrateV4toV5(db);
      }
    
      if (currentVersion < 6) {
        migrateV5toV6(db);
      }
    
      if (currentVersion < 7) {
        migrateV6toV7(db);
      }
    
      db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
    }
    
    /** Get current schema version (useful for diagnostics) */
    export function getSchemaVersion(cwd) {
      const db = openForgeDb(cwd);
      try {
        return db.query("PRAGMA user_version").get().user_version;
      } finally { db.close(); }
    }
    
    function ensureSchemaV1(db) {
      db.run(`CREATE TABLE IF NOT EXISTS forges (
        id INTEGER PRIMARY KEY,
        slug TEXT UNIQUE NOT NULL,
        parent_id INTEGER REFERENCES forges(id),
        blocked_by INTEGER REFERENCES forges(id),
        status TEXT NOT NULL DEFAULT 'active'
          CHECK(status IN ('active','parked','blocked','completed','abandoned')),
        phase TEXT,
        iteration INTEGER DEFAULT 1,
        priority TEXT DEFAULT 'medium' CHECK(priority IN ('high','medium','low')),
        systems TEXT DEFAULT '[]',
        plan_path TEXT,
        context TEXT DEFAULT '{}',
        blocking_reason TEXT,
        created_at TEXT NOT NULL DEFAULT (datetime('now')),
        updated_at TEXT DEFAULT (datetime('now')),
        parked_at TEXT,
        completed_at TEXT
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS gates (
        id INTEGER PRIMARY KEY,
        forge_id INTEGER NOT NULL REFERENCES forges(id),
        iteration INTEGER NOT NULL,
        gate TEXT NOT NULL CHECK(gate IN ('skeptic','integration','second_opinion','static')),
        result TEXT NOT NULL CHECK(result IN ('PASS','FAIL')),
        findings TEXT DEFAULT '[]',
        created_at TEXT NOT NULL DEFAULT (datetime('now'))
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS spikes (
        id INTEGER PRIMARY KEY,
        forge_id INTEGER REFERENCES forges(id),
        assumption TEXT NOT NULL,
        result TEXT CHECK(result IN ('confirmed','refuted')),
        actual TEXT,
        tested_at TEXT DEFAULT (datetime('now')),
        permanent INTEGER DEFAULT 0
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS co_failures (
        system_a TEXT NOT NULL,
        system_b TEXT NOT NULL,
        gate TEXT NOT NULL,
        fail_count INTEGER DEFAULT 1,
        total_count INTEGER DEFAULT 1,
        PRIMARY KEY (system_a, system_b, gate)
      )`);
    
      db.run(`CREATE TABLE IF NOT EXISTS current_state (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL,
        updated_at TEXT DEFAULT (datetime('now'))
      )`);
    
      db.run(`CREATE VIEW IF NOT EXISTS system_risk AS
        SELECT
          je.value AS system,
          ROUND(1.0 * SUM(CASE WHEN g.result = 'FAIL' THEN 1 ELSE 0 END) / MAX(COUNT(*), 1), 2) AS fail_rate,
          ROUND(AVG(f.iteration), 1) AS avg_iterations,
          MAX(f.updated_at) AS last_touched,
          COUNT(DISTINCT f.id) AS sample_count
        FROM forges f, json_each(f.systems) je
        LEFT JOIN gates g ON g.forge_id = f.id
        WHERE f.status IN ('completed', 'abandoned')
        GROUP BY je.value
      `);
    
      db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS spikes_fts USING fts5(
        assumption, actual, content=spikes, content_rowid=id
      )`);
    
      db.run(`CREATE TRIGGER IF NOT EXISTS spikes_ai AFTER INSERT ON spikes BEGIN
        INSERT INTO spikes_fts(rowid, assumption, actual) VALUES (new.id, new.assumption, new.actual);
      END`);
    
      db.run(`CREATE TRIGGER IF NOT EXISTS cascade_on_complete
        AFTER UPDATE OF status ON forges
        WHEN NEW.status = 'completed'
      BEGIN
        UPDATE forges SET status = 'parked', blocked_by = NULL,
          blocking_reason = 'auto-unblocked: ' || NEW.slug || ' completed',
          updated_at = datetime('now')
        WHERE blocked_by = NEW.id AND status = 'blocked';
        UPDATE forges SET priority = 'high',
          blocking_reason = COALESCE(blocking_reason, '') || ' parent completed — still relevant?',
          updated_at = datetime('now')
        WHERE parent_id = NEW.id AND status IN ('parked', 'blocked');
      END`);
    
      db.run(`CREATE TRIGGER IF NOT EXISTS cascade_on_abandon
        AFTER UPDATE OF status ON forges
        WHEN NEW.status = 'abandoned'
      BEGIN
        UPDATE forges SET priority = 'high',
          blocking_reason = 'blocker abandoned — review scope',
          updated_at = datetime('now')
        WHERE blocked_by = NEW.id AND status = 'blocked';
      END`);
    
      db.run(`DELETE FROM gates WHERE forge_id IN (
        SELECT id FROM forges WHERE status IN ('completed','abandoned')
        AND completed_at < datetime('n
  • hooks/integration-runner.mjsGitHub
    Read the script
    /**
     * integration-runner.mjs — Task 3.2
     *
     * runIntegration(cwd, forgeId) — executes all integration contracts for a forge.
     *
     * Spawn note: uses Bun.spawnSync with stdout:"pipe", stderr:"pipe" for output capture.
     * This is safe because integration contracts are expected to produce <64KB output.
     * The 64KB deadlock gotcha (CLAUDE.md) applies only to long-lived processes with "pipe".
     * Short-lived contract scripts are bounded by design.
     */
    
    import {
      getForgeContext,
      listIntegrationContracts,
      updateContractStatus,
    } from "./forge-crud.mjs";
    
    /**
     * Run all integration contracts for the given forge.
     *
     * @param {string} cwd   - forge working directory (used for DB path)
     * @param {number} forgeId
     * @returns {Promise<{ allPass: boolean, contracts: Array, shortCircuited?: boolean }>}
     */
    export async function runIntegration(cwd, forgeId) {
      // iter-7 Phase-2/Phase-3 kill-switch: skip all execution when disabled
      const ctx = getForgeContext(cwd, forgeId);
      if (ctx?.integration_disabled === true) {
        return { allPass: true, contracts: [], shortCircuited: true };
      }
    
      const rows = listIntegrationContracts(cwd, forgeId);
    
      if (rows.length === 0) {
        return { allPass: true, contracts: [] };
      }
    
      let allPass = true;
      const contracts = [];
    
      for (const row of rows) {
        const proc = Bun.spawnSync(["sh", "-c", row.test_cmd], {
          stdout: "pipe",
          stderr: "pipe",
        });
    
        const exitCode = proc.exitCode;
        const result = exitCode === 0 ? "pass" : "fail";
    
        let failureOutput = null;
        if (result === "fail") {
          const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
          const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
          failureOutput = (stdout + stderr).trim() || null;
          allPass = false;
        }
    
        updateContractStatus(cwd, row.id, result, failureOutput);
    
        contracts.push({
          id: row.id,
          contractName: row.contract_name,
          result,
          failureOutput,
        });
      }
    
      return { allPass, contracts };
    }
    
  • hooks/nightly-maintenance-launchd.mjsGitHub
  • hooks/nightly-maintenance-policy.mjsGitHub
  • hooks/nightly-maintenance-runner.mjsGitHub
  • hooks/nightly-maintenance-state.mjsGitHub
  • hooks/stream-planner.mjsGitHub

All 11 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

Ships withbeast-forge

Ore in, steel out. A blacksmith doesn't blame the ore. It smelts, shapes, tempers, and quenches — until what comes out holds an edge.

Get the whole plugin
Stats
25
Stars
4
Forks
Maintained
Maintenance
JavaScript
Language
MIT
License
2mo ago
Last commit
7mo ago
Created

Repo: malakhov-dmitrii/forge