#!/usr/bin/env node
/**
 * agent-discovery-gate — Claude Code PreToolUse hook (tool: Agent).
 *
 * Intercepts `Agent` tool calls and denies the call when the requested
 * `subagent_type` / `agent_type` matches the name of a BALDART agent but
 * the corresponding `.claude/agents/<name>.md` file is missing or its
 * symlink is broken on the consumer's filesystem.
 *
 * Background: Claude Code has two known bugs that combine into a silent
 * failure mode for sub-agent invocations —
 *   #56869 — sub-agent returning internal error silently hangs parent
 *            (no error propagation, parent sees `0 tool uses · Done`)
 *   #20931 — file-based discovery of `.claude/agents/*.md` is documented
 *            as BROKEN in some scenarios (files ignored at session start)
 *
 * This hook covers the filesystem-detectable subset of the problem
 * (missing/broken symlinks). The model-side defense (the MUST rule in
 * `framework/AGENTS.md`) covers the rest. The two are inseparable:
 * without the MUST rule, a `permissionDecision: deny` from this hook
 * would likely be worked around by the model trying a different
 * `subagent_type` — the very failure mode this hook exists to prevent.
 *
 * Source of truth for the list of BALDART agents: the filesystem
 * directory next to this file (`.framework/framework/.claude/agents/`).
 * Every `<name>.md` (except `REGISTRY.md`) is a BALDART agent. No
 * separate manifest is shipped — the directory IS the manifest.
 *
 * Fail-safe contract: this hook MUST NOT block legitimate work. If
 * anything unexpected happens (missing directory, parse error, IO
 * error), it exits 0 and lets the tool call through. We optimise for
 * false negatives over false blocks — the gate is a safety net, not a
 * tribunal.
 *
 * Registration: see src/utils/hooks.js — `baldart add` and `baldart
 * update` auto-register this hook in the consumer's
 * `.claude/settings.json`.
 */

'use strict';

const fs = require('fs');
const path = require('path');

function safeExit() {
  process.exit(0);
}

function readStdin() {
  try {
    return fs.readFileSync(0, 'utf8');
  } catch (_) {
    return '';
  }
}

function parseInput(raw) {
  if (!raw) return null;
  try { return JSON.parse(raw); } catch (_) { return null; }
}

function loadBaldartAgents() {
  // `__dirname` resolves to .framework/framework/.claude/hooks/.
  // The agent files live alongside, in ../agents/.
  const agentsDir = path.join(__dirname, '..', 'agents');
  try {
    if (!fs.existsSync(agentsDir)) return null;
    const names = fs.readdirSync(agentsDir)
      .filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md')
      .map((f) => f.replace(/\.md$/, ''));
    return new Set(names);
  } catch (_) {
    return null;
  }
}

function isConsumerSymlinkHealthy(consumerCwd, agentName) {
  // fs.statSync follows symlinks; ENOENT/ELOOP/EACCES → broken / missing.
  const target = path.join(consumerCwd, '.claude', 'agents', `${agentName}.md`);
  try {
    fs.statSync(target);
    return true;
  } catch (_) {
    return false;
  }
}

function denyReason(agentName, consumerCwd) {
  const targetPath = path.join('.claude', 'agents', `${agentName}.md`);
  return [
    `agent-discovery-gate: BALDART agent \`${agentName}\` is not available on this`,
    `consumer's filesystem (\`${targetPath}\` is missing or its symlink is broken).`,
    ``,
    `This is a recoverable state, not a transient one — calling this agent`,
    `again will keep failing until the symlink is restored.`,
    ``,
    `Fix:`,
    `  1. Run \`npx baldart doctor\` — it will detect and repair the broken`,
    `     agent symlink(s) using SymlinkUtils.mergeAgents.`,
    `  2. Restart this Claude Code session so the discovery index is rebuilt.`,
    ``,
    `IMPORTANT — for the agent that received this denial:`,
    `Do NOT silently fall back to a different \`subagent_type\` (e.g.`,
    `\`feature-dev:*\`, \`general-purpose\`, or another BALDART agent). The`,
    `AGENTS.md MUST rule explicitly forbids this — substitution masks the`,
    `failure that this gate exists to surface. Instead, stop and report this`,
    `message verbatim to the user, then wait for direction.`,
    ``,
    `(Consumer cwd: ${consumerCwd})`,
  ].join('\n');
}

function emitDeny(reason) {
  try {
    process.stdout.write(JSON.stringify({
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'deny',
        permissionDecisionReason: reason,
      },
    }));
  } catch (_) {
    // Fallback to legacy mode: exit code 2 + stderr.
    process.stderr.write(reason + '\n');
    process.exit(2);
  }
  process.exit(0);
}

function main() {
  const raw = readStdin();
  const input = parseInput(raw);
  if (!input) safeExit();

  // Defensive: hook is registered with matcher 'Agent', but in case Claude
  // Code routes other tools through us, only proceed for the Agent tool.
  if (input.tool_name !== 'Agent') safeExit();

  const toolInput = input.tool_input || {};
  // Claude Code's hook docs name the field `agent_type`; the public SDK
  // exposes it as `subagent_type`. Accept either for forward/backward
  // robustness — whichever is present wins.
  const requested = String(
    toolInput.agent_type || toolInput.subagent_type || ''
  ).trim();
  if (!requested) safeExit();

  const baldartAgents = loadBaldartAgents();
  if (!baldartAgents || baldartAgents.size === 0) safeExit();

  // Only gate names that BALDART claims as its own. Built-in agents
  // (Explore, Plan, general-purpose), marketplace agents (feature-dev:*,
  // vercel:*, sentry:*), and plugin-provided agents are unaffected.
  if (!baldartAgents.has(requested)) safeExit();

  // Consumer cwd comes from Claude Code's input envelope; fall back to
  // process.cwd() for robustness.
  const consumerCwd = (typeof input.cwd === 'string' && input.cwd)
    ? input.cwd
    : process.cwd();

  if (isConsumerSymlinkHealthy(consumerCwd, requested)) safeExit();

  emitDeny(denyReason(requested, consumerCwd));
}

try { main(); } catch (_) { safeExit(); }
