#!/usr/bin/env node
// git-decision-gate — refuse to hand the owner a source-control decision.
// Two entry points, one brief:
//   Stop           — the closing message asks the owner to choose a git action.
//   PreToolUse     — an AskUserQuestion whose question or options are git mechanics.
// Code spans, fenced blocks and quoted text are stripped first, so quoting the
// rule does not trip it. Fail-open on any error.
import fs from 'node:fs';

const GIT_VERB =
  '(?:rebase|merge|squash|cherry-?pick|revert|reset|force[- ]?push|push|pull|fetch|' +
  'stash|amend|branch off|cut a branch|open a PR|raise a PR|land|ship|tag|release|' +
  'bump the version|resolve (?:the )?conflicts?|cherry pick)';

const ASK = [
  new RegExp(`\\b(?:should|shall|do you want|would you like|want) (?:I|we|me to) ${GIT_VERB}\\b`, 'i'),
  new RegExp(`\\b(?:do you want|would you like|which) (?:me to )?(?:${GIT_VERB})\\b.*\\bor\\b`, 'i'),
  new RegExp(`\\b(?:rebase|merge) or (?:merge|rebase)\\b`, 'i'),
  new RegExp(`\\bPR or (?:direct )?(?:push|merge|land)\\b`, 'i'),
  new RegExp(`\\b(?:direct )?(?:push|merge|land) or (?:a )?PR\\b`, 'i'),
  new RegExp(`\\blet me know (?:which|whether) .{0,40}\\b(?:branch|merge|rebase|PR|push|land)\\b`, 'i'),
  new RegExp(`\\bhow (?:do you want|would you like) (?:me to |this to )?(?:be )?(?:${GIT_VERB})\\b`, 'i'),
  new RegExp(`\\bwhich branch (?:should|do you want)\\b`, 'i'),
  new RegExp(`\\bwaiting (?:for|on) you to .{0,30}\\b(?:merge|push|land|approve the PR|review the PR)\\b`, 'i'),
  new RegExp(`\\bit(?:'s| is) (?:on|landed on) main\\b.{0,60}\\b(?:you|owner) (?:can|should|will) (?:now )?(?:deploy|release|pull|arm)\\b`, 'i'),
];

const BRIEF = [
  'git-decision-gate: you are handing the owner a source-control decision. He does not make them, ever.',
  '',
  'GIT_FATIGUE.md, short version — the part that binds you:',
  '1. The owner reports a problem and sees ONE result: it works, here is where to look. He never chooses or',
  '   even sees branches, merge vs rebase, PR vs direct push, CI jobs, gate failures, runner failures,',
  '   conflict resolution, cleanup, or reconciliation. Surfacing any of those IS the defect this gate exists for.',
  '2. Reversible mechanics are YOURS: which branch, which lander, merge vs rebase, cutting a release,',
  '   restarting a daemon, resolving a conflict. Decide by the rubric, execute, report in one line.',
  '   A conflict is resolved by judgment (integrate both sides by default; delete a side only after proving it',
  '   carries zero unique behavior) — never escalated, never blanket -X theirs.',
  '3. Land the way the project already establishes — read its CLAUDE.md/memory FIRST. Never invent ceremony,',
  '   never --force, never --no-verify. Drive the tested wrapper (ship.sh land), never hand-type the cascade.',
  '4. "On main" is NOT delivered. This is the owner\'s laptop and fires burn here. If the runtime does not yet',
  '   read the fix — a versioned bundle, a deploy clone, a unit\'s own copy — put the file where it belongs NOW,',
  '   back it up, print the rollback, prove the behaviour changed. Release ritual catches up afterwards.',
  '5. The ONLY thing worth surfacing is a product or design choice, or a genuinely irreversible destruction.',
  '   Not a git one.',
  '',
  'Now: make the call yourself, do it, and close with what shipped and where he can see it.',
].join('\n');

function stripCitations(text) {
  return text
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/`[^`\n]*`/g, ' ')
    .replace(/^\s*>.*$/gm, ' ')
    .replace(/"[^"\n]*"/g, ' ')
    .replace(/“[^”\n]*”/g, ' ')
    .replace(/'[^'\n]{0,80}'/g, ' ');
}

function lastAssistantText(rows, from) {
  const parts = [];
  for (let i = rows.length - 1; i > from; i--) {
    const o = rows[i];
    if (o?.type !== 'assistant') continue;
    const content = o.message?.content;
    if (!Array.isArray(content)) continue;
    if (content.some((b) => b?.type === 'tool_use')) break;
    for (const b of content) {
      if (b?.type === 'text' && typeof b.text === 'string') parts.unshift(b.text);
    }
    break;
  }
  return parts.join('\n');
}

function hitOf(text) {
  const re = ASK.find((r) => r.test(text));
  return re ? text.match(re)[0] : null;
}

function askUserQuestionText(inp) {
  const qs = inp?.tool_input?.questions;
  if (!Array.isArray(qs)) return '';
  const out = [];
  for (const q of qs) {
    if (typeof q?.question === 'string') out.push(q.question);
    if (typeof q?.header === 'string') out.push(q.header);
    if (Array.isArray(q?.options)) {
      for (const o of q.options) {
        if (typeof o?.label === 'string') out.push(o.label);
        if (typeof o?.description === 'string') out.push(o.description);
      }
    }
  }
  return out.join('\n');
}

function main() {
  let inp = {};
  try {
    inp = JSON.parse(fs.readFileSync(0, 'utf8'));
  } catch {
    return;
  }

  if (inp.hook_event_name === 'PreToolUse') {
    if (inp.tool_name !== 'AskUserQuestion') return;
    const hit = hitOf(stripCitations(askUserQuestionText(inp)));
    if (!hit) return;
    console.log(
      JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          permissionDecision: 'deny',
          permissionDecisionReason: `${BRIEF}\n\n(matched in your question: ${hit})`,
        },
      })
    );
    return;
  }

  if (inp.stop_hook_active) return;
  const tp = inp.transcript_path;
  if (!tp || !fs.existsSync(tp)) return;

  const rows = [];
  let lastPrompt = -1;
  try {
    for (const line of fs.readFileSync(tp, 'utf8').split('\n')) {
      if (!line) continue;
      let o;
      try {
        o = JSON.parse(line);
      } catch {
        continue;
      }
      rows.push(o);
      if (o?.type === 'user' && !o.isMeta && !o.toolUseResult) lastPrompt = rows.length - 1;
    }
  } catch {
    return;
  }
  if (lastPrompt < 0) return;

  const text = stripCitations(lastAssistantText(rows, lastPrompt));
  if (!text.trim()) return;
  const hit = hitOf(text);
  if (!hit) return;

  console.log(JSON.stringify({ decision: 'block', reason: `${BRIEF}\n\n(matched: ${hit})` }));
}

main();
process.exit(0);
