Ctrl K
ComunidadGuías…

docker/gateguard-effectiveness/arms.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use strict';

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

// see docs/gateguard/question-effectiveness.md#arms
const ARMS = Object.freeze({
  off: Object.freeze({ gate: false }),
  gate: Object.freeze({ gate: true, tree: 'candidate' }),
  'minus-target': Object.freeze({ gate: true, tree: 'candidate', dropTarget: true }),
  placebo: Object.freeze({ gate: true, tree: 'candidate', placebo: true }),
  main: Object.freeze({ gate: true, tree: 'main' })
});

const DEFAULT_ARMS = Object.freeze(['off', 'gate', 'minus-target', 'placebo']);
const TRIAL_FILE = 'hook-run.json';
const ARM_HOOK = 'scripts/hooks/gateguard-arm.js';
const WRAPPER = `'use strict';
const fs = require('fs');
const path = require('path');

function trialConfig(rawInput) {
  const data = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
  for (const start of [process.env.CLAUDE_PROJECT_DIR, data && data.cwd]) {
    let dir = typeof start === 'string' && start ? path.resolve(start) : null;
    for (let depth = 0; dir && depth < 8; depth++) {
      const file = path.join(dir, '${TRIAL_FILE}');
      if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
      const parent = path.dirname(dir);
      dir = parent === dir ? null : parent;
    }
  }
  throw new Error('${TRIAL_FILE} not found');
}

function run(rawInput) {
  const config = trialConfig(rawInput);
  process.env.GATEGUARD_STATE_DIR = config.stateDir;
  process.env.GATEGUARD_METRICS = '1';
  if (config.patch) require('../../arm-patch').applyArm(config.patch, require('../lib/gateguard-target-class'));
  return require('./gateguard-fact-force').run(rawInput);
}

module.exports = { run };
`;

function git(repoRoot, args, spawn = spawnSync) {
  const result = spawn('git', ['-C', repoRoot, ...args], { encoding: 'utf8', shell: false, timeout: 60000 });
  if (result.status !== 0 || result.error) throw new Error(`git ${args[0]} failed: ${String(result.stderr || result.error).trim()}`);
  return result.stdout;
}

/** Resolves a ref to a full commit sha. */
function resolveRef(repoRoot, ref) {
  return git(repoRoot, ['rev-parse', '--verify', `${ref}^{commit}`]).trim();
}

/** Extracts scripts/ at a commit into dest and adds the arm wrapper; returns dest. */
function materializeTree(repoRoot, sha, dest, spawn = spawnSync) {
  fs.mkdirSync(dest, { recursive: true });
  const tarName = 'scripts.tar';
  git(repoRoot, ['archive', '--format=tar', '-o', path.join(dest, tarName), sha, 'scripts'], spawn);
  // Name the archive relatively and run tar with dest as cwd, so no argument carries a
  // "C:\..." drive letter. GNU tar (Git Bash / MSYS on Windows) reads an archive argument
  // containing a colon as a "host:path" remote spec and dies with "Cannot connect to C:
  // resolve failed"; bsdtar (C:\Windows\system32\tar.exe) does not, which is why the
  // absolute path only broke under GNU tar. A relative -f plus cwd extracts identically
  // under both and makes -C unnecessary.
  const extract = spawn('tar', ['-xf', tarName], { cwd: dest, encoding: 'utf8', shell: false, timeout: 60000 });
  fs.rmSync(path.join(dest, tarName), { force: true });
  if (extract.status !== 0 || extract.error) throw new Error(`tar failed: ${String(extract.stderr || extract.error).trim()}`);
  fs.copyFileSync(path.join(__dirname, 'arm-patch.js'), path.join(dest, 'arm-patch.js'));
  fs.writeFileSync(path.join(dest, ...ARM_HOOK.split('/')), WRAPPER);
  return dest;
}

const forwardSlashes = file => file.split(path.sep).join('/');

/** Claude Code settings registering a tree's gate on the same tools as hooks/hooks.json. */
function armSettings(root) {
  if (!root) return { hooks: {} };
  const runner = forwardSlashes(path.join(root, 'scripts', 'hooks', 'run-with-flags.js'));
  const command = id => `node "${runner}" ${id} ${ARM_HOOK} standard,strict`;
  return {
    hooks: {
      PreToolUse: [
        { matcher: 'Edit|Write|MultiEdit|NotebookEdit', hooks: [{ type: 'command', command: command('pre:edit-write:gateguard-fact-force') }] },
        { matcher: 'Bash|PowerShell', hooks: [{ type: 'command', command: command('pre:bash:gateguard-fact-force') }] }
      ]
    }
  };
}

/** Question-table patch for one arm and scenario, or null when the gate runs unmodified. */
function trialPatch(armName, scenario) {
  const arm = ARMS[armName];
  if (!arm) throw new Error(`unknown arm: ${armName}`);
  if (arm.placebo) return { placebo: true };
  if (arm.dropTarget) return { drop: scenario.targetQuestions.slice() };
  return null;
}

module.exports = { ARMS, DEFAULT_ARMS, TRIAL_FILE, ARM_HOOK, resolveRef, materializeTree, armSettings, trialPatch };