# Objective Guard en Codex

Cómo correr Objective Guard dentro de Codex: qué hooks instalar, cómo probar que se dispararon y qué no observa la integración. Para la puesta en marcha inicial y tu primera ronda, leé antes [../getting-started.es.md](../getting-started.es.md). Versión en inglés: [codex.md](codex.es.md).

## Qué aporta esta integración

- **Un gate de preflight pre-edición.** Un hook `PreToolUse` ejecuta `src/pre-edit-gate.mjs` — el mismo binario que usa Claude Code. Si el repositorio tiene contrato tipado pero no una autorización vigente, el hook escribe `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", ...}}` y sale con código 2. Esto prueba que el agente leyó el contrato antes de mutar; no prueba nada sobre si el parche es correcto.
- **Detectores por parche.** Un hook `PostToolUse` ejecuta `src/codex-hook.mjs`, que parsea el sobre de `apply_patch`, extrae cada objetivo `*** Add File:` y `*** Update File:` (las eliminaciones se ignoran) y corre `oguard check --file <ruta> --json` sobre cada uno. El conjunto de detectores son sólo cuatro checks: `weak-test`, `stub-prod`, `new-dep` (únicamente sobre `package.json`) y sintaxis JS (únicamente `.js`, `.mjs`, `.cjs`). Cuando aparece un finding, el hook devuelve `additionalContext` nombrando las reglas; el silencio significa que esos cuatro no encontraron nada.
- **Inyección de prompt de la fase actual.** `scripts/codex-awareness-oguard-prompt.sh` en `UserPromptSubmit` sube desde el directorio de trabajo buscando `oguard.contract.json` y le dice al agente en qué estado está el repositorio — contrato placeholder, contrato activo o sin contrato — y el próximo comando a correr (`intent`, `start --from-intent` o `task-flow`). Para un contrato activo también indica no reintentar una denegación de preflight: hay que correr `task-flow` desde la raíz del contrato y continuar sólo tras obtener `ready`. Es un consejo inyectado como contexto; no fuerza nada.
- **Evidencia de ronda.** `oguard align` mide la superficie modificada contra el scope y los key results declarados; `oguard round-end` corre los checks declarados, evalúa los criterios de aceptación y conserva un registro append-only. Un `pass` significa que los checks en alcance no encontraron violación.
- **Telemetría sanitizada.** Cada decisión agrega un evento al inventario global: harness, hash del repositorio, nombre base del repositorio, resultado, código de razón, nombres de reglas y una duración. Sin paths, sin prompt, sin contenido de archivos.

## Requisitos

- Node 20 o superior (`package.json#engines` declara `"node": ">=20"`), y `node` resoluble en el `PATH` — los comandos del hook invocan `node` directamente.
- Un repositorio Git. Todo comando resuelve su raíz con `git rev-parse --show-toplevel`; fuera de un repositorio la CLI falla y el hook saltea el archivo.
- Una versión de Codex con hooks habilitados (`hooks = true` en `~/.codex/config.toml`) que soporte hooks de comando `PreToolUse`, `PostToolUse` y `UserPromptSubmit` y respete `permissionDecision: "deny"`. El código lee sólo `input.cwd`, `input.tool_name` e `input.tool_input.command` para el hook de parche (más `tool_input.file_path` o `tool_input.file` para el gate compartido) y no fija ninguna versión de harness.

## Instalación

Instalá el paquete público y usá la CLI publicada desde PATH:

```bash
npm install -g @fyso/objective-guard
oguard-hooks install --harness codex
```

El enlace de npm se verifica ejecutándolo en una regresión de CLI. Para desarrollo podés usar `node src/cli.mjs <cmd>`. El instalador reemplaza sus hooks de prefijos anteriores, preserva integraciones independientes y acepta `--home PATH` para probar una configuración aislada.

Awareness es opcional: el helper compartido consulta `awareness status` una sola vez, desde el directorio actual y con timeout de 1,5 segundos. Respeta el routing por worktree/sesión del CLI, omite focos divergentes y no exige mantenimiento por cada respuesta. Un error de Awareness no suprime el preflight del guard. Si ya existe un hook de prompt de Awareness, el instalador configura `OGUARD_AWARENESS_CONTEXT=off` para evitar inyección duplicada. El JSON se serializa de forma segura.

Las instrucciones de preflight, alineación y cierre siguen vigentes: esto no relaja la autorización de ediciones.

La configuración de hooks es **a nivel usuario**, en `~/.codex/hooks.json`, con hooks habilitados en `~/.codex/config.toml`. No es a nivel proyecto: una sola instalación protege todo repositorio que contenga un `oguard.contract.json`. Las rutas en `command` deben ser absolutas.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "OGUARD_HARNESS=codex node /absolute/path/to/objective-guard/src/pre-edit-gate.mjs",
            "timeout": 10,
            "statusMessage": "Validating Objective Guard task flow"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "OGUARD_HOOK_EXECUTE=1 node /absolute/path/to/objective-guard/src/codex-hook.mjs",
            "statusMessage": "Checking Objective Guard",
            "timeout": 10,
            "additionalContextLimit": 400
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash /absolute/path/to/objective-guard/scripts/codex-awareness-oguard-prompt.sh",
            "statusMessage": "Loading Objective Guard context",
            "timeout": 10,
            "additionalContextLimit": 800
          }
        ]
      }
    ]
  }
}
```

La entrada `PostToolUse` deliberadamente no declara `matcher`. `src/codex-hook.mjs` filtra por su cuenta: su primera sentencia devuelve un resultado vacío salvo que `input.tool_name === "apply_patch"`. Un matcher más angosto que excluya `apply_patch` desactiva el detector en silencio — el hook igual corre y igual no encuentra nada. Si agregás un matcher, verificalo con la prueba de payload de abajo en lugar de asumir el nombre de la herramienta.

`OGUARD_HARNESS=codex` es lo que etiqueta la telemetría; sin eso el evento pre-edición registra `tool: "unknown"`. `OGUARD_HOOK_EXECUTE=1` fuerza a `src/codex-hook.mjs` a ejecutar su `main()` cuando el harness lo invoca a través de un wrapper de shell.

Después, por repositorio:

```bash
oguard init   # crea solamente oguard.contract.json
```

`oguard init` no instala hooks ni toca la configuración del harness — escribe un contrato placeholder con `objective.id = "O-UNSPECIFIED"` y se niega a sobreescribir uno existente (`test/init-cli.test.mjs`). La instalación del hook es el paso manual de arriba.

## Verificar la integración

Corré esto desde la raíz de un repositorio con contrato tipado.

```bash
# 1. Preflight. Imprime status ready o action-required; action-required sale con código 1.
node src/cli.mjs task-flow --json

# 2. Pasale al gate el payload que envía Codex, con telemetría aislada.
export OGUARD_INVENTORY_DIR=/tmp/oguard-verify
printf '{"cwd":"%s","tool_name":"apply_patch","tool_input":{"file_path":"%s/README.md"}}' "$PWD" "$PWD" \
  | OGUARD_HARNESS=codex node src/pre-edit-gate.mjs
echo "exit=$?"

# 3. Leé la decisión que quedó registrada.
tail -1 "$OGUARD_INVENTORY_DIR/events.jsonl"
```

Un repositorio autorizado no produce stdout y devuelve `exit=0`; el evento agregado lleva `"preflight_decision":"allow","preflight_reason":"fresh-preflight"`. Uno no autorizado imprime el JSON con `permissionDecision: deny`, sale con código 2 y registra `"preflight_decision":"deny","preflight_reason":"missing-or-expired-preflight"`. Ambas transiciones están aseveradas en `test/pre-edit-gate.test.mjs`.

Para confirmar que el parser de parches reconoce tu sobre y que el detector dispara, corré los tests que ejercitan el mismo camino de código contra un fixture descartable:

```bash
node --test test/pre-edit-gate.test.mjs test/hooks-e2e.test.mjs test/claude-hook.test.mjs
```

`test/hooks-e2e.test.mjs` siembra un `test.skip`, llama a `runCodexHook` con un sobre real `*** Begin Patch / *** Update File: / *** End Patch` desde un directorio anidado y asevera tanto el finding `weak-test` como el evento sanitizado del inventario con `tool: "codex"`. `test/claude-hook.test.mjs` asevera que `editedFilesFromPatch` extrae archivos agregados y actualizados ignorando los eliminados. El hook `PostToolUse` de Codex no tiene un switch de debug propio; cuando no produce nada, revisá el archivo por separado con `node src/cli.mjs check --file <ruta> --json`.

## El loop en la práctica

```bash
oguard init
oguard intent --objective-id O-1 --objective "Protect checkout" --task-type bugfix --scope "src/**,test/**"
oguard intent-scan
oguard start --from-intent --round-id R-1 --round "Add regression" --kr-id KR-1 --kr "Checkout has a regression test"
git add oguard.contract.json && git commit -m "open round R-1"
oguard task-flow --json
# ... llamadas apply_patch, cada una frenada por PreToolUse e inspeccionada por PostToolUse
oguard check --file src/checkout.mjs
oguard align --json
oguard round-end --mode final
```

`init` escribe el contrato placeholder una vez por repositorio. `intent` registra un borrador local y revisable de objetivo, tipo de tarea y scope candidato en `.oguard/intent.json`; todavía no hay nada congelado. `intent-scan` resuelve sólo archivos observables por Git que coinciden con esos globs y lista candidatos de test — evidencia estructural, nunca prueba semántica, así que corregí la intención y volvé a escanear si la superficie está mal. `start --from-intent` copia el borrador revisado al contrato congelado y ancla el baseline; después de esto, cambiar de dirección exige una ronda sucesora, no otro `intent`. Commiteá `oguard.contract.json` para que el contrato congelado quede versionado con el trabajo. `task-flow --json` debe devolver `ready` antes del primer parche material; es lo que concede la autorización que busca el gate. Cada `apply_patch` queda entonces frenado e inspeccionado automáticamente. `check --file` corre los mismos cuatro detectores a demanda. `align` compara la superficie modificada con el scope y los key results declarados. `round-end --mode final` corre los checks declarados y bloquea si un criterio requerido sigue en `unknown`; `--mode fast` conserva esa incertidumbre en lugar de bloquear. Para el próximo trabajo, abrí una sucesora con `oguard next-round ...`, que exige árbol limpio y archiva el contrato y el baseline anteriores.

## Qué no puede ver el harness

- El gate sólo inspecciona payloads estructurados de herramientas. Lee `tool_input.file_path` o `tool_input.file`; no parsea el cuerpo del parche, así que en un `apply_patch` multiarchivo la decisión de autorización se toma una vez por llamada, no por archivo. Una escritura hecha dentro de un comando de shell — `sed -i`, `tee`, una redirección, `git apply`, un script que reescribe archivos — no queda frenada ni clasificada.
- El alcance del detector es exactamente lo que declara el sobre del parche. `editedFilesFromPatch` sólo matchea líneas que empiezan con `*** Add File:` o `*** Update File:`. Una eliminación, un rename expresado de otra forma, o un formato de parche que tu versión de Codex emita con otros marcadores no produce archivos y por lo tanto no produce findings.
- Un repositorio sin `oguard.contract.json` nunca queda bloqueado. El gate devuelve `allow` con razón `no-contract`, y un archivo resuelto fuera de la raíz del contrato devuelve `allow` con razón `file-outside-contract` (`test/pre-edit-gate.test.mjs`). La ausencia de una denegación no es evidencia de gobernanza.
- La autorización `ready` es cacheada, no continua. Se guarda en `.oguard/state.json` contra el hash del contrato y el hash de la política de KPIs versionada, y expira una hora después de otorgada. Editar el contrato u `oguard.kpis.json` la invalida de inmediato y el próximo parche queda denegado hasta que corras `task-flow` otra vez.
- El detector post-parche es informativo. Corre después de aplicado el parche y devuelve contexto, nunca un rechazo. Su alcance son cuatro checks por archivo, y `new-dep` sólo mira `package.json`.
- La inyección de prompt no es enforcement. `UserPromptSubmit` agrega contexto que el modelo puede ignorar. Sólo el gate `PreToolUse` produce una denegación.
- La telemetría prueba uso, no beneficio. El inventario registra que hubo una decisión, no que evitó un desvío; `oguard inventory dashboard --json` reporta `causal_effect: not-evaluable` justamente por eso.

## Resolución de problemas

| Síntoma | Causa probable | Comando que muestra la verdad |
| --- | --- | --- |
| Todo `apply_patch` queda denegado | No hay autorización vigente, o el contrato o la política de KPIs cambió desde la última | `node src/cli.mjs task-flow --json` |
| La denegación menciona una raíz de contrato inesperada | Un directorio padre tiene `oguard.contract.json`; el gate sube desde el archivo y desde el cwd | `printf '{"cwd":"%s","tool_input":{"file_path":"%s/file.mjs"}}' "$PWD" "$PWD" \| node src/pre-edit-gate.mjs` |
| `task-flow` devuelve `action-required` | Falta contrato tipado, hay mismatch de baseline, un finding de gobernanza o una violación bloqueante de la política de KPIs — el array `actions` dice cuál | `node src/cli.mjs task-flow --json` |
| El gate nunca deniega nada | No hay contrato en ese repositorio, o los hooks están deshabilitados en Codex | `ls oguard.contract.json` y revisar `hooks = true` en `~/.codex/config.toml` |
| Los parches se aplican pero nunca aparece un finding | El matcher de `PostToolUse` excluye la herramienta de parche, o el sobre no expuso líneas `Add/Update File` | `node --test test/hooks-e2e.test.mjs`, luego `node src/cli.mjs check --file <ruta> --json` |
| El inventario muestra `tool: "unknown"` | Falta `OGUARD_HARNESS=codex` en el comando de `PreToolUse` | `tail -3 ~/.oguard/inventory/events.jsonl` |
| `round-end` bloquea por criterios `unknown` | La ronda no declaró un check reproducible para un criterio requerido | `node src/cli.mjs round-end --mode fast --json` |
