Skip to content

[BUG] Hook do Iris nunca executa em repos com Husky v9 — commits perdem atribuição de IA #164

Description

@codermarcos

Summary

Em repositórios que usam Husky v9 (core.hooksPath=.husky/_), o Iris instala os hooks dentro do stub interno do Husky — um arquivo que sempre encerra antes de alcançar o código injetado. O iris hook install reporta sucesso, o iris hook status diz installed: true, mas nenhum trailer de atribuição é escrito e nenhum push automático acontece. O resultado é que commits assistidos por IA entram na base como HUMAN, de forma permanente e não recuperável.

Origem: investigação do @alexbuenocb compartilhada em #eng-ai-produtividade (documento completo).


Três defeitos independentes compõem a falha:

Bug A — código do hook nunca executa (crítico).
O Husky v9 usa delegação em duas camadas. core.hooksPath aponta para .husky/_, cujos arquivos são stubs de 39 bytes:

#!/usr/bin/env sh
. "$(dirname "$0")/h"

O h é sourced (.), roda sh -e .husky/<hook> e termina em exit $c. Como exit encerra o shell que fez o source, tudo que o Iris anexa depois dessa linha é código morto. get_hooks_dir() (iris/hooks/manager.py:46-62) devolve core.hooksPath sem detectar essa indireção.

Agravante: .husky/_/.gitignore contém * — o diretório é regenerado a cada npm install, então mesmo o código morto é apagado periodicamente. É o que explica design-system-react estar hoje sem hook nenhum, apesar de já ter sido instalado.

Bug B — flag inexistente no hook de push.
iris/hooks/post_commit_push.sh:49 chama "$IRIS_BIN" "$REPO_ROOT" --push --quiet. O flag --quiet não existe no argparse do CLI (v1.5.0 aceita --verbose, não --quiet). O argparse rejeita o comando inteiro com exit 2, e o 2>/dev/null engole o usage:, então a falha é totalmente silenciosa e o stamp file nunca é escrito — o hook tenta de novo a cada commit, sempre falhando.

Bug C — stdout não é redirecionado.
Ainda na linha 49, só o stderr vai para /dev/null. O bloco roda em background (&) e leva ~10-15s; quando o descritor de stdout do commit fecha antes disso, o processo iris recebe erro de pipe e morre antes de gravar o stamp.

Bug D — chaves de trailer: já resolvido, não refazer.
O documento original pedia expandir a regex para aceitar Assisted-by:. Isso já está em produção desde que _ATTRIBUTION_TRAILER_RE (iris/ingestion/git_reader.py:40-43) passou a ler Co-authored-by|Assisted-by|Made-with. Verificado nos repos afetados: 100% dos trailers reais são capturados, incluindo variações de modelo e de caixa. Evidência na seção de logs.

Reproduction steps

  1. Criar um repo com layout Husky v9:
    git init repro && cd repro && mkdir -p .husky/_
    cp <qualquer-repo-husky9>/.husky/_/h .husky/_/h
    printf '#!/usr/bin/env sh\n. "$(dirname "$0")/h"' > .husky/_/prepare-commit-msg
    chmod +x .husky/_/h .husky/_/prepare-commit-msg
    git config core.hooksPath .husky/_
  2. iris hook install e iris hook status.
  3. Commitar com o agente sinalizado: echo hi > a.txt && git add a.txt && AI_AGENT="Claude Code" git commit -m "test: repro".
  4. Inspecionar o corpo do commit: git log -1 --format='%B'.

Expected behaviour

O hook deveria ser instalado onde o Husky realmente executa código (.husky/<hook>, o hook público), o commit deveria receber Co-Authored-By: Claude Code <noreply@anthropic.com>, e o status deveria refletir alcançabilidade real de execução — não apenas presença do marcador.

Quando a instalação não for alcançável, status deve reportar não instalado (ou degradado), nunca installed: true.

Actual behaviour

Repro executado localmente contra o h real do design-system-react:

get_hooks_dir ->  <repo>/.husky/_
installed at  ->  <repo>/.husky/_/prepare-commit-msg
status        ->  {'installed': True, 'hook_type': 'prepare-commit-msg', ...}

--- corpo do commit resultante ---
test: repro$
$

Nenhum trailer. O commit é ingerido como HUMAN e não há como reatribuí-lo depois sem reescrever histórico.

Estado atual dos repos locais (core.hooksPath × onde o Iris gravou):

Repo core.hooksPath Hook do Iris está em Executa? % AI (200d)
clickbus-platform-bff .husky/_ .husky/_/ ❌ código morto 15%
design-system-react .husky/_ (nenhum — apagado) ❌ ausente 52%
clickbus-frontend-responsive (vazio) .git/hooks/ 11%
voucher, pricing (vazio) .git/hooks/ 60% / 88%

Os 52% do design-system-react vêm de trailers escritos manualmente pela instrução global do agente, não pelo hook — ou seja, a cobertura real do hook nesses repos é zero.

Surface

Hooks (prepare-commit-msg)

Versão do CLI

v1.5.0

Repo analisado

RocketBus/design-system-react, RocketBus/clickbus-platform-bff, RocketBus/clickbus-web-kit

Logs / screenshots / trace-id

Bug B, reproduzido:

$ iris . --push --quiet
usage: iris [-h] [--org ORG] [--repos REPOS] [--days DAYS] [--windows WINDOWS]
            [--churn-days CHURN_DAYS] [--out OUT] [--lang {en,pt-br}]
            [--trend] [--recent-days RECENT_DAYS] [--push] [--no-push]
            [--verbose]
            [repo_path]

Bug D — evidência de que as chaves de trailer já funcionam. Varredura com _ATTRIBUTION_TRAILER_RE + classify_origin sobre o histórico real (200 dias, 7 repos): nenhum trailer de atribuição escapou. As formas abaixo, todas presentes no histórico, classificam como AI_ASSISTED / tool Claude:

Assisted-by: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: claude-code_2-1-183_agent <claude-code_2-1-183_agent@iris.invalid>

A coluna "mention-no-trailer" da varredura (commits cujo corpo cita uma ferramenta de IA mas não tem trailer) só retornou prosa — docs: add CLAUDE.md for Claude Code e similares — corretamente não atribuídos.

Impacto nos dados. Commits com trailer já contam corretamente hoje; nada a recuperar ali. Commits sem trailer nenhum (onde o hook morto deveria ter escrito) são permanentemente não atribuíveis — a correção só vale dali para frente. Quanto mais tempo o bug ficar aberto, maior o buraco no histórico dos repos com Husky.

Notas para a correção

  • get_hooks_dir() precisa detectar a indireção do Husky (presença de _/h ou _/husky.sh dentro de core.hooksPath) e instalar no diretório pai.
  • O h do Husky roda o hook público com sh -e — qualquer comando de topo que devolva não-zero aborta o commit. O prepare_commit_msg.sh usa grep -q no guard de já-atribuído; sob -e isso derruba o commit quando não há match. Os trechos injetados precisam de guarda (|| true) antes de irem para .husky/.
  • .husky/<hook> é versionado no git, diferente de .git/hooks/. Instalar ali deixa a árvore suja / exige commit do hook. Decidir entre (a) instalar e documentar que o hook é versionado, ou (b) desfazer o core.hooksPath — precisa de decisão antes do PR.
  • Migração: install deve limpar marcadores mortos deixados em .husky/_/.

Metadata

Metadata

Assignees

Labels

type: bugAlgo funciona incorretamente, quebra, ou regrediu

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions