Skip to main content

Tentacruel

Multiplexador de worktrees para o Claude Code.

Cada sessão de chat = um git worktree novo + uma branch nova + um agente Claude Code com cwd fixado naquele worktree. Três conversas no mesmo repositório nunca enxergam os arquivos umas das outras, porque cada uma está fisicamente em um diretório diferente.

App web local, single-user, sem auth. Roda em localhost.

┌──────┬─────────────────────────────┬──────────────┐
│ rail │        chat da sessão       │ Arqs | Diff  │
│ de   │  (streaming + input fixo)   │  + commit /  │
│ sess.│                             │    push      │
└──────┴─────────────────────────────┴──────────────┘

Instalação

uv tool install tentacruel
tentacruel

O Tentacruel é uma aplicação Python que serve a própria interface, então o "executável" é um pacote e não um .app.

Não há assinatura nem notarização da Apple envolvidas, e isso é escolha e não omissão. Um .app baixado de um site precisa de conta de desenvolvedor paga e de notarização; sem elas, o Gatekeeper avisa sobre malware em toda instalação. Um pacote instalado por uma ferramenta que a pessoa já usa não passa por esse caminho — e o público deste app já instalou o CLI do Claude Code pelo terminal.

Como app do macOS, com ícone

tentacruel install-app

Cria o Tentacruel.app em ~/Applications — ele aparece no Launchpad e no Spotlight, e pode ir para o Dock. Clicar nele sobe o servidor e abre uma janela própria, sem barra de endereço: o WebKit é o do próprio macOS, então nada vem embutido e nenhum navegador precisa estar instalado.

Isso continua sem envolver a Apple, e não por descuido. O Gatekeeper age sobre o atributo de quarentena, que o navegador põe no que você baixa de um site. Um bundle gerado na sua máquina pelo pacote já instalado nunca recebe esse atributo: não há o que notarizar, nem conta de desenvolvedor a pagar.

~/Applications e não /Applications de propósito — a pasta do sistema exige autenticação de administrador, e instalação que pede senha é instalação que parte das pessoas abandona.

Para remover, apague a pasta:

rm -rf ~/Applications/Tentacruel.app

E tentacruel --window abre a mesma janela sem criar app nenhum, se você quiser só experimentar.

Pré-requisitos

O quê Por quê Como conferir
macOS único alvo do MVP
Claude Code instalado e logado o agente usa a sua assinatura; nãoANTHROPIC_API_KEY claude --version e, se preciso, claude/login
git 2.30+ worktrees são o produto inteiro git --version
uv é quem instala, e traz o Python junto uv --version

Node não está na lista de propósito: a interface já vem compilada dentro do pacote. Ele só é necessário para trabalhar no código, e está em Desenvolvimento.

O agente autentica pelo login existente do Claude Code. Tentacruel nunca lê, pede ou configura ANTHROPIC_API_KEY.


Aviso de atualização

O app consulta o índice de pacotes no máximo a cada 6 horas e, quando há versão mais nova, ela aparece em Configurações → Sobre. Toda falha é silenciosa: não saber se há atualização não é problema que mereça ir para a tela de alguém.

TENTACRUEL_CHECK_FOR_UPDATES=false desliga — e desligado, este app não fala com nada além do serviço de coordenação do próprio usuário.

Uso em 60 segundos

  1. Registrar um repo — ícone de pasta no rodapé do rail → Escolher pasta no Finder (abre o diálogo nativo do macOS) ou cole o caminho absoluto. Qualquer subdiretório serve; o Tentacruel resolve a raiz.
  2. Criar uma sessão — botão + (ou ⌘N): nome, branch base e modo de permissão. O nome vira o slug: Refatorar Auth → branch wt/refatorar-auth, worktree .tentacruel/worktrees/refatorar-auth.
  3. Conversar — cada painel é um agente independente. Vários podem trabalhar em paralelo; a bolinha no rail pulsa enquanto o agente executa.
  4. Revisar — aba Diff à direita mostra o que ainda não foi commitado e o que já está commitado na branch da sessão. Commit e push manuais ficam no rodapé do painel.
  5. Encerrar — três passos, cada um um pouco mais longe que o anterior: Arquivar (para o agente, preserva tudo — e Reabrir traz de volta), Apagar o worktree (some com o diretório e, por consequência, arquiva a sessão) e Excluir (apaga a sessão do Tentacruel). A branch sempre permanece no git.

Atalhos

Atalho Ação
⌘1⌘9 troca para a n-ésima sessão ativa
⌘N nova sessão
/ ⇧⏎ envia / quebra linha no composer

⌘N é reservado pelo Chrome e pelo Safari para "nova janela" e pode não chegar à página. O botão + do rail faz o mesmo.


Como o isolamento funciona

Ao criar uma sessão o backend executa:

git -C <repo> worktree add <repo>/.tentacruel/worktrees/<slug> -b wt/<slug> <base>

e instancia o agente com cwd = <worktree>. Três camadas garantem o isolamento:

  1. Filesystem — worktrees são checkouts separados. O agente da sessão A literalmente não tem os arquivos da sessão B no seu diretório.
  2. cwd do SDK — o processo do Claude Code nasce dentro do worktree; caminhos relativos resolvem lá.
  3. Permissões — em acceptEdits, o CLI recusa escritas fora do diretório de trabalho (verificado: uma tentativa de escrever em ~/ foi negada).

.tentacruel/ é adicionado automaticamente ao .git/info/exclude do repositório — o seu .gitignore nunca é tocado.

Atenção: .git/info/exclude resolve o git, mas não ferramentas que varrem o diretório por conta própria. Rodar pytest, ruff, eslint ou um watcher na raiz do repo passa a enxergar cópias dos seus arquivos em .tentacruel/worktrees/*. Se isso incomodar, exclua o diretório na config da ferramenta — por exemplo norecursedirs = [".tentacruel"] no pytest, extend-exclude = [".tentacruel"] no ruff, ignores: [".tentacruel"] no ESLint. Se preferir os worktrees fora do repo, aponte TENTACRUEL_WORKSPACE_DIRNAME para outro nome ou ajuste worktrees_root() em services/worktree.py — o caminho é calculado num lugar só.

Seletor de pasta

O botão Escolher pasta no Finder existe porque o browser não consegue entregar um caminho absoluto — nem webkitdirectory nem a File System Access API expõem isso. Como o backend roda na sua própria máquina, ele abre o diálogo nativo via osascript (choose folder) e devolve o POSIX path.

Consequências:

  • Só funciona no macOS. Fora dele o endpoint devolve 501 e sobra o campo de texto.
  • Uma janela por vez: um segundo pedido recebe 409 enquanto a primeira estiver aberta.
  • Se você fechar a aba com o diálogo aberto, ele fica órfão até o timeout de 3 minutos. Basta responder à janela do Finder que ainda está na tela.

Configurações

Ícone de ajustes no rodapé do rail. Tudo fica no banco, então sobrevive a trocas de browser.

Seção O que faz
Repositórios registra e remove repos, e define provedor/modelo/esforço padrão por repositório — herdados por sessões novas daquele repo
Provedores de IA lista os CLIs: instalado ou não, versão, caminho, como logar. Botão Instalar roda o comando com a saída transmitida ao vivo
Aparência tema, densidade e layout (posição do rail e do painel)
Dependências git, Node, uv e npm com versão, caminho e o porquê de cada um
Idioma português, inglês e espanhol
Sobre versão, caminhos, atalhos e a notificação de fim de tarefa

Temas

Cinco temas — Escuro, Carvão puro (AMOLED), Brasa, Claro e Papel/Sépia — são conjuntos de tokens, não folhas de estilo paralelas. Cada um é um bloco [data-theme="…"] em frontend/src/styles/tokens.css que sobrescreve as variáveis; nenhum componente sabe que temas existem.

O seletor prova isso: cada miniatura carrega data-theme próprio, então é o tema que anuncia — desenhada com os mesmos tokens que o app usa. Não há uma segunda definição de cor para manter em dia.

Adicionar um tema = adicionar um bloco em tokens.css e uma linha na lista de AppearanceSettings.

Densidade

Um multiplicador (--wt-density) alimenta espaçamento, tipografia e as dimensões de layout. De 70% a 140%, em passos de 5%.

Idioma

Português (Brasil), inglês e espanhol. O catálogo de pt-BR é a fonte: os outros idiomas são tipados contra ele, então uma chave faltando quebra o build em vez de virar texto vazio em produção.

Adicionar um idioma são dois passos: copiar frontend/src/i18n/locales/en.ts, traduzir os valores, e somar uma entrada em LOCALES (frontend/src/i18n/index.ts). Nenhuma tela muda.

O que ficou de fora: o Carcará oferece 18 idiomas. Eu parei em três — os que consigo revisar. Escrever mais 15 traduções que não sei avaliar geraria texto de qualidade desconhecida na sua interface; a infraestrutura está pronta e eu adiciono qualquer idioma que você pedir, sinalizando quais não posso revisar.

Instalação de provedores

O botão Instalar abre /ws/install/{provider} e transmite a saída do comando. O comando vem do registry no backend — o cliente só diz qual provedor, nunca o que rodar.

Instaladores rodam sem terminal interativo (stdin fechado, CI=1, npm_config_yes=true). Um instalador que insista em perguntar falha rápido em vez de travar para sempre; nesse caso o comando aparece na tela para você rodar no seu terminal. Timeout de 15 minutos, e o processo morre por process group.

Provedor e modelo por worktree

Cada sessão escolhe seu próprio provedor de IA, modelo e nível de esforço — no modal de criação e no header, trocável a qualquer momento.

Controle O que faz Como aplica
Provedor qual backend de IA roda neste worktree trocar reinicia o agente (históricos não são portáveis entre provedores)
Modelo Padrão, opus, sonnet, haiku, fable ao vivo, sem reiniciar nem perder a conversa (set_model() do SDK)
Esforço auto, lowmax reinicia o agente e retoma via resume, então o contexto sobrevive

São aliases, não ids fixos: opus sempre aponta para o Opus mais recente, então a lista não envelhece. Um id completo (claude-opus-4-5-20251101) também é aceito. Padrão = o que o seu CLI já estiver configurado para usar.

Rodar haiku numa sessão de documentação e opus numa refatoração, em paralelo no mesmo repo, é o caso de uso.

Autenticação: sempre a sua assinatura

Nenhum provedor usa API key. Cada backend dirige o CLI oficial do provedor, que já carrega o seu login — hoje é o claude (assinatura do Claude Code). Um provedor futuro seguirá a mesma regra: o CLI dele, o login dele, a sua conta. O Tentacruel nunca lê, pede ou repassa ANTHROPIC_API_KEY ou equivalente.

O catálogo em GET /api/providers diz, por provedor, se o CLI está instalado (available) e como logar (auth_hint, install_hint). O que não está instalado aparece desabilitado na UI, com a instrução de instalação no tooltip.

Adicionando um provedor (o que falta)

A arquitetura está pronta, mas só o Claude Code está implementado — nenhum outro CLI de agente estava instalado nesta máquina para eu construir e testar contra. Para somar um:

  1. Escreva services/agents/<provider>.py implementando o protocolo AgentBackend (start, send, interrupt, set_permission_mode, set_model, stop) e emitindo os dataclasses de agents/base.py.
  2. Adicione um ProviderSpec em services/agents/registry.py.

Só isso. O manager, as rotas, o WebSocket, o banco e o frontend inteiro leem o catálogo — nenhum deles precisa mudar. O ponto crítico de qualquer adapter é fixar o cwd no worktree: é o que sustenta o isolamento.

Navegador de arquivos

Aba Arquivos no painel lateral — a primeira, e a que o painel abre. É o que se procura primeiro; o diff só interessa depois que o agente escreveu alguma coisa. Árvore preguiçosa — um diretório por chamada — então um repositório grande custa o mesmo que um pequeno.

Ditado por voz

O microfone na caixa de mensagem grava, transcreve e põe o texto no campo — não envia. Transcrição erra, e ler antes de mandar é o ponto.

Quem transcreve é um CLI da sua máquina, pelo mesmo princípio dos provedores de IA: o Tentacruel dirige o que já está instalado em vez de embutir motor próprio. Embutir o openai-whisper arrastaria o torch junto; o faster-whisper arrastaria o CTranslate2 — e os dois baixam modelo no primeiro uso. Um multiplexador de worktrees não tem por que pesar um giga por causa de um microfone.

services/transcribe.py conhece dois:

CLI instalar observação
whisper-cli (whisper.cpp) brew install whisper-cpp o mais rápido em Apple Silicon, mas precisa de um modelo ggml — procurado em ~/.cache/whisper.cpp e nos diretórios do brew
whisper (OpenAI) brew install openai-whisper mais lento, porém baixa o próprio modelo: funciona assim que instala

Sem nenhum deles pronto, o microfone não aparece — botão que não funciona é pior que botão ausente. Configurações → Transcrição de voz mostra o estado de cada um e o comando para instalar.

A caixa ganha borda de destaque quando o campo está em foco — cor primária com um anel fino, em vez de só um cinza mais forte.

O áudio é gravado como WAV mono de 16 kHz no próprio navegador (lib/recorder.ts), que é exatamente o que o whisper.cpp exige e o que o whisper aceita — por isso nem o app nem você precisam de ffmpeg. No servidor ele vai para um arquivo temporário, passa pelo CLI e some: recado de voz não é coisa para deixar no disco.

Anexos

A caixa de mensagem é uma pilha, e as medidas fecham:

  4px  padding do cartão   (--wt-composer-padding)
 54px  campo de texto      (--wt-composer-field-height, com 8px de padding)
 32px  faixa de controles  (--wt-composer-bottom-height, um botão de ícone)
  4px  padding do cartão
 ────
 94px  (--wt-composer-height)

O campo tem caixa própria e mais folgada que o resto — 8px contra 4px — porque é a parte em que se escreve. Entre ele e os controles não há régua: os dois são a mesma caixa. A régua de 0.5px existe só acima da faixa de anexos, que separa coisas de naturezas diferentes; essa faixa tem 28px (--wt-strip-height), a mesma da linha de stats acima do cartão, e rola para o lado em vez de empurrar o texto para baixo.

Todo botão de ícone do app é 32×32 (--wt-icon-button-size) com um glifo de 18px (--wt-icon-glyph-size) centralizado nele. O respiro é o que sobra — 7px de cada lado — então não existe padding para manter em sincronia com nada. A medida vive na variante icon do botão, não nos pontos de uso: IconButton não tem prop de tamanho, e o iconLg que existia foi eliminado.

Cole (⌘V), arraste ou escolha pelo clipe. Tudo vai para <worktree>/.tentacruel/attachments/, e essa escolha responde três perguntas de uma vez: o agente alcança o arquivo, porque está dentro do próprio cwd — a mesma fronteira de todo o resto, sem exceção aberta para upload; o git nunca vê, porque .tentacruel/ já está no .git/info/exclude; e some junto com o worktree, porque mora dentro dele.

Imagem viaja inline, como bloco base64 na mensagem do usuário: o agente enxerga a figura sem gastar uma chamada de ferramenta nela. Qualquer outro arquivo entra como caminho, para o agente ler com a mesma ferramenta que usa para o resto do worktree.

As dimensões saem do cabeçalho do próprio arquivo (PNG, JPEG, GIF, WebP) — o app não carrega biblioteca de imagem para dizer 234×49. O caminho é relativo ao worktree, então a miniatura no chip é servida pelo endpoint de arquivo bruto que já existia: nenhum endpoint novo para desenhar o anexo.

O cliente manda caminhos; quem manda é o disco. Um caminho inventado, ou que aponte para fora do worktree, é descartado antes de virar mensagem.

O deslize

Todo elemento que muda de tamanho em passo discreto desliza em 400ms (--wt-slide-duration), por dois primitivos com papéis distintos:

  • Collapse — para o que aparece e some: uma faixa de grid de 0fr a 1fr, linhas para altura e colunas para largura. É o único jeito de transicionar até um tamanho vindo do conteúdo sem medir em JS antes.
  • ResizeSlide — para o que fica e muda de tamanho. O CSS não dá conta aqui: transição dispara em mudança de valor computado, e conteúdo crescendo sob width: auto nunca muda o valor — continua auto. Então ele fixa a largura antiga, solta até a nova e devolve para auto.

A diferença importa: um elemento que já está na tela e muda de tamanho deve mover só a diferença. Fazê-lo sumir e renascer com a transição é um efeito diferente, e pior.

Fechar uma aba também desliza. O React remove um elemento no instante em que ele deixa de ser renderizado, então sair com deslize é pedir para sair, animar, e só então ir de fato: o botão de fechar marca a aba como saindo, ela encolhe até zero, e o onClose — que apaga o chat ou fecha o arquivo — dispara no fim.

Desliza: a faixa de anexos, a régua abaixo dela, as pastas da árvore, o raciocínio e a saída de ferramenta, o botão de "commits atrás", o lápis da aba, e a aba de chat ao entrar e sair da renomeação.

Não desliza, de propósito: o balão de mensagem enquanto o agente escreve e o campo de texto enquanto você digita. Os dois mudam de tamanho de forma contínua, token a token e tecla a tecla; uma transição de 400ms ali faria o texto ficar permanentemente atrás do cursor. Animar mudança discreta dá a sensação de acabamento que você quer; animar mudança contínua faz o app parecer lento.

Pastas abrem e fecham deslizando em 400ms. O diretório é carregado antes de expandir: as linhas precisam já estar dentro do invólucro fechado para o slide ter o que mover — expandir primeiro faria a primeira abertura saltar enquanto todas as seguintes deslizam.

Clicar abre uma aba, e o que aparece depende do arquivo:

tipo o que abre
texto editor, com contador de linhas e ⌘S
imagem (.png, .jpg, .gif, .webp, .avif, .svg, .ico…) prévia sobre xadrez de transparência, com tamanho e dimensões
.pdf o leitor nativo do navegador
áudio (.mp3, .wav, .ogg, .m4a…) e vídeo (.mp4, .mov, .webm…) player com controles
resto tipo e tamanho, sem fingir que dá para exibir

O texto viaja dentro do JSON; mídia não. A página recebe só tipo, tamanho e uma URL, e o navegador busca os bytes em /file/raw — por isso um vídeo de 20 MB abre sem inchar resposta nenhuma. O limite de 1 MB é dos arquivos que entram na página; mídia vai até 64 MB.

Um .svg é desenho e o código que o desenha, então essa aba oferece os dois: um botão alterna entre prévia e fonte, e na fonte o ⌘S volta. Um PDF pequeno também decodifica como ASCII, mas "editar" ali é um jeito de corromper o arquivo — quem decide é o tipo declarado, nunca a aparência dos bytes.

Só o que a tabela declara sai com o próprio Content-Type. Qualquer outra coisa vai como application/octet-stream e attachment, então um .html perdido no worktree nunca vira página rodando na origem do app.

  • O git decide o que é ruído. A listagem passa por git check-ignore, então node_modules, dist e afins não aparecem. .git nunca é listado.
  • Arquivos alterados vêm marcados com o código do git status (M, ??, A), coloridos na árvore — o mesmo estado que a aba Diff mostra.
  • Clicar num arquivo abre ele como uma aba, ao lado das conversas, e o arquivo é editável: ⌘S (ou Ctrl+S) salva, Tab insere dois espaços, e a aba mostra um ponto enquanto houver alteração não salva. O rascunho vive no estado da tela, então trocar de aba e voltar não perde o que você escreveu.
  • Ícones por tipo de arquivo vêm do Material Icon Theme (o mesmo do VS Code). Só os SVGs mapeados em ui/fileIcons.ts entram no bundle, e o Vite os transforma em data URI — nenhuma requisição extra em tempo de execução.

O editor é sem coloração de sintaxe — destacar sintaxe significaria embarcar um pacote de gramáticas. Ele tem numeração de linha e edição de texto puro.

Salvar tem as mesmas guardas de ler: o caminho é resolvido contra a raiz do worktree, arquivos binários são recusados, e não é possível criar arquivo novo por aqui — o editor abre sobre um arquivo que já existe.

O mesmo limite do agente

Todo caminho vindo do cliente é resolvido e conferido contra a raiz do worktree antes de ser tocado. ../, caminho absoluto e symlink que aponta para fora são recusados — inclusive o worktree da sessão vizinha. A sessão só enxerga o que o agente dela enxerga, e há teste para cada uma dessas rotas de fuga.

Campo de mensagem

No estilo do Claude: um cartão arredondado único que segura o texto e os controles. Nada em volta pinta fundo ou borda — o composer flutua sobre o chat em vez de ficar numa barra preenchida.

Dentro do cartão, à direita, ficam o seletor de modelo e o de esforço, mais o botão de enviar. O header da sessão guarda o que é escolha de sessão (provedor e modo de permissão); o modelo mora onde você escreve, como no Claude. Um lugar por conceito — o modelo não aparece duas vezes.

Abrir um worktree que já existe

Nem todo worktree nasce aqui. Um pode ter vindo do git worktree add na mão, de outra ferramenta (o --worktree do próprio Claude Code cria em .claude/worktrees/), ou ter ficado para trás quando uma sessão foi excluída sem apagar o diretório. A caixa de nova sessão oferece adotar esses.

O seletor "Como começar" só aparece quando há algo a adotar. Ao escolher Abrir um existente, a branch base some — um worktree que já existe já está numa branch, não há de onde cortá-lo — e o nome vira opcional: sem nome, usa o do diretório.

Adotar não provisiona nada: o diretório, a branch e o que está dentro já existem. Só ganham uma sessão para um agente ser apontado a eles. Ficam de fora da lista os worktrees que já têm sessão e o checkout principal do repositório — apontar um agente para a cópia de trabalho do usuário é exatamente o que este app existe para evitar.

Voltar para o fim

Quando a conversa não está no fim, um botão redondo aparece no rodapé dela e leva de volta à última mensagem. Some sozinho quando você chega lá, e enquanto está escondido não recebe clique — senão engoliria o toque na mensagem embaixo dele.

A mesma medida — "estou a menos de 80px do fim?" — governa duas coisas: esse botão e o auto-scroll que segue o agente enquanto ele escreve. A diferença é que o auto-scroll lê de um ref, para não re-renderizar a cada mensagem, e o botão lê de um estado, porque precisa re-renderizar.

Manter o worktree em dia

Um worktree cortado de develop fica para trás enquanto o agente trabalha, e quanto mais tempo passa, pior o merge no fim. Isso acontece sozinho: quando a base ganha commits, o worktree os recebe — a branch e os arquivos no disco ao mesmo tempo, porque o merge roda dentro do worktree.

Merge pequeno quase nunca conflita, e é essa a aposta: o que a sincronização contínua elimina é a divergência que envelheceu. A divergência concorrente — duas pessoas na mesma função ao mesmo tempo — nenhuma ferramenta resolve; para essa existe o alerta de colisão.

O header mostra o estado, e são quatro:

estado o que significa
(nada) em dia com a base
↓3 atrás há commits a trazer; o botão ainda faz na mão, parando os agentes antes
aguardando há trabalho não commitado ou um agente no meio de um turno
em conflito o merge foi tentado e desfeito inteiro — e há um botão para pedir ao agente daquele worktree que resolva

Ele espera, não interrompe. O caminho manual para os agentes antes de mesclar, que é certo para algo que você apertou. Para algo que dispara sozinho, matar um turno no meio por um merge de rotina não é — então a sessão fica aguardando e sincroniza quando ficar quieta.

Três coisas disparam a verificação, e duas delas já eram eventos que existiam: o fim de um turno, um commit (que também avisa quem foi cortado daquela branch — é isso que faz empilhamento funcionar sem remote nenhum), e um relógio a cada TENTACRUEL_SYNC_INTERVAL segundos (60 por padrão, 0 desliga). O relógio existe porque um git pull na main no seu terminal não produz evento nenhum que este app consiga ouvir — e esse é o caso comum, não o exótico.

O número é rev-list --left-right --count <base>...HEAD — o mesmo cálculo que alimenta o do painel. O botão só aparece quando há algo a trazer.

Merge, não rebase. Rebase reescreveria commits que o agente talvez já tenha enviado, e um rebase interrompido no meio deixa um worktree — em que um agente está prestes a escrever — num estado que ninguém pediu. Merge ou acontece inteiro, ou é desfeito inteiro.

Conflito desfaz o merge. Se der conflito, o merge --abort roda e os arquivos que bateram são nomeados. Deixar marcadores de conflito num worktree que um agente vai ler é entregar a ele um arquivo que parece código e não é.

Mudanças não commitadas bloqueiam. O merge reescreve arquivos; trabalho em voo seria pego no meio. O botão fica desabilitado e o tooltip diz o porquê.

No caminho manual, os agentes param antes — pela mesma razão da troca de branch: os arquivos vão mudar debaixo do cwd deles, e quem apertou o botão pediu por isso. O daemon faz o contrário: nunca para ninguém, e por isso só age quando já não há ninguém trabalhando.

Quem mais está neste arquivo

Sincronizar não resolve duas pessoas dentro do mesmo arquivo ao mesmo tempo — só duas pessoas resolvem. O que dá para fazer é dizer cedo, enquanto ainda é uma conversa e não um merge.

O header mostra um chip quando outra sessão do mesmo repositório está mexendo nos mesmos arquivos, e o tooltip nomeia a sessão e os arquivos. Conta o que foi commitado na branch e o que ainda está solto no worktree: um arquivo já commitado colide tão forte quanto um que está no meio da edição.

Os dois lados são avisados. Aviso que só uma das duas pessoas vê começa discussão, não conversa.

A conta é barata porque todos os worktrees estão no mesmo disco sob um único banco de objetos: dois comandos git por sessão viva, refeitos nos mesmos momentos em que a sincronização é verificada.

Sessões e chats

Uma sessão é um worktree. Um chat é uma conversa com um agente rodando dentro dele. Várias conversas podem dividir o mesmo worktree — como várias abas de terminal no mesmo diretório — cada uma com seu próprio agente, histórico, modelo e modo de permissão.

O isolamento continua onde sempre esteve: entre sessões, no filesystem. Dentro de uma sessão, os chats compartilham os arquivos de propósito.

Chat nasce com nome automático (Chat 2, Chat 3…), que não diz nada quando há quatro deles. O lápis na aba aberta renomeia no lugar — e duplo clique também, para quem já sabe. Enter ou sair do campo salva, Escape descarta, e nome em branco mantém o antigo em vez de deixar uma aba sem nome. O campo é do tamanho do que está escrito, então a aba não pula nem abre buraco enquanto se digita.

O lápis aparece só na aba aberta: em todas seriam quatro elementos por chat disputando espaço com os nomes, e ali ele fica onde o olho já está.

O header da sessão

A coluna do chat abre com duas faixas:

faixa altura conteúdo
1 64px fixos ● nome da sessão · repositório · branch · caminho do worktree
2 a das abas as abas desta sessão — conversas (duplo clique renomeia) e arquivos abertos — com + para abrir outra conversa

A segunda faixa acompanha as abas em vez de ocupar outros 64px, então não sobra espaço morto acima delas. Só a faixa 1 tem altura fixa.

As opções da sessão ficam no canto superior direito da conversa, posicionadas de forma absoluta: não reservam espaço, o thread ocupa a altura inteira da coluna e elas ficam por cima.

À esquerda da barra fica o seletor de branch: qual branch este worktree está segurando. Trocar move o checkout — os agentes são parados antes, porque eles têm um cwd cujo conteúdo está prestes a mudar, e um turno que começa numa branch e termina em outra é um bug que ninguém consegue ler.

Uma branch que outro worktree já segura não aparece: o git recusa a mesma branch em dois worktrees, e linha que não dá para escolher é ruído num seletor. O backend ainda a reporta, com taken_by, para quem precise saber. Quem decide se a troca é segura é o próprio git switch — ele carrega mudanças limpas junto e recusa quando elas seriam perdidas. Reimplementar esse julgamento aqui seria só uma segunda cópia, pior, dele.

Não ocupar espaço é o que criava o encavalamento — o grupo tem 355px e a coluna de leitura tem 848px centralizados, então em qualquer largura ele cobria parte do texto. Duas coisas resolvem, sem devolver espaço ao layout: a conversa reserva --wt-floating-clearance (52px) no topo, para nenhuma mensagem começar debaixo dos botões, e a barra desenha um fade sob si — sólido na altura dos botões, esvaindo no fim — para o que rola por baixo ler como passando sob vidro em vez de bater neles. Sem cartão nem moldura própria — os dois dropdowns (provedor e modo de permissão) são bare, do mesmo jeito.

Os três ícones à direita são uma escada, e o ícone diz qual degrau:

ícone ação o que acontece
caixa de arquivo Arquivar / Reabrir para o agente; worktree e branch ficam. Reabrir é o caminho de volta que o arquivamento promete — só possível enquanto o diretório existir
pasta com X Apagar o worktree do disco git worktree remove --force. A sessão passa a arquivada junto: sem diretório não há onde o agente rodar, e a caixa de confirmação diz isso
lixeira Excluir sessão tira sessão e histórico do Tentacruel, e apaga o worktree

Nenhum deles é um menu de reticências — ··· promete "mais opções", e usá-lo para apagar diretório era mentira de ícone.

Só o chat tem as duas. O rail e o painel mantêm uma faixa de 64px — o rail com o botão de nova sessão, o painel com as abas Arquivos / Diff / Setup.

O rodapé segue a mesma régua: o rail fecha com o botão de Configurações e o painel com a linha de commit/push, ambos em 64px, então a borda inferior atravessa as colunas sem degrau. O estado da branch (↑1, worktree limpo) subiu para o topo do painel, ao lado do diff que ele descreve.

A altura é fixa em 64px, sem escalar com a densidade, e vive só em --wt-header-height. O rail tem 64px de largura, também fixa (--wt-rail-width), então o canto superior esquerdo é um quadrado de 64×64.

Cada entrada do rail é um quadrado de 46px (--wt-rail-item-size) com o tile do repositório de 38px (--wt-rail-avatar-size) centralizado nele — 4px de folga nos quatro lados. Os dois são pixels fixos, não passos de densidade: é um ícone num tamanho escolhido, não texto que acompanha a escala.

Barra de status da sessão

Duas linhas abraçam o campo de mensagem, ambas sem fundo e na mesma coluna dele, com dados reais do agente em execução. A de cima lê como uma frase — Sessão: 13% - Contexto: 5% - Semana: 7% - Fable: 1% — e a porcentagem só ganha cor quando a janela começa a encher:

Campo Onde De onde vem
% de contexto acima client.get_context_usage(), medido contra o limiar de auto-compact — é ele que decide quando a conversa é cortada
cota da sessão (5h), da semana e por modelo acima a leitura de /usage que o próprio Claude Code guarda em ~/.claude.json
usuário e plano abaixo oauthAccount do ~/.claude.json — nome, email, organização e plano (default_claude_max_20xMax 20x)
worktree e branch abaixo a própria sessão; clique copia

Contexto e cota falam do turno que está prestes a ser enviado, então ficam encostados na caixa de texto. Usuário e caminho descrevem o ambiente, e ficam embaixo.

De onde saem as porcentagens de cota. O RateLimitEvent do SDK entrega utilization como null para a maioria das contas — só status e resetsAt. Por esse canal, porcentagem não existe.

Mas o /usage do Claude Code tem os números, e guarda a resposta em ~/.claude.json, sob cachedUsageUtilization — o mesmo arquivo de onde o Tentacruel já lê o perfil. Então services/agents/limits.py lê esse cache e, quando a leitura envelhece (5 min), roda claude --print /usage para o próprio CLI atualizá-lo. O comando é resolvido localmente: não gasta token de modelo. O RateLimitEvent deixou de ser fonte e virou gatilho — ele avisa que a cota mexeu, e aí o número é relido.

Nenhuma credencial passa pelo Tentacruel: quem fala com a Anthropic é o CLI, com o login que já estava lá. E toda leitura carrega limits_fetched_at, que aparece no tooltip — número velho nunca se passa por número vivo.

A atualização roda fora do caminho quente: uma tarefa em segundo plano que transmite pelo WebSocket quando termina, porque abrir o CLI leva ~4s e um turno não pode acabar 4 segundos mais tarde por causa de um enfeite.

A leitura da conta é só de exibição: nome, email, plano e organização. O Tentacruel não lê, não guarda e não transmite credencial nenhuma — é o ponto de dirigir o CLI do provedor em vez de usar API key.

Avatar da sessão

O rail é só ícone — sem legenda embaixo, que quebrava o layout com nomes longos. Cada item mostra:

  • a logo do repositório, se ele tiver uma. A busca não exige nome exato: qualquer imagem cujo nome contenha favicon, icon, apple-touch-icon ou logo conta — trakk_favicon.jpeg e logo_whitemode.png são encontrados. Diretórios varridos: raiz, public/, static/, app/, src/app/, assets/, src/assets/, .github/, docs/ e afins — mais public/, static/, app/, src/app/ dentro de cada pasta de primeiro nível, o que cobre monorepos (front-web/public/favicon.ico, front-admin/src/app/icon.png). Desempate: diretório mais próximo da raiz → favicon antes de logo (um wordmark a 32px é ilegível) → nome exato antes de decorado (logo.svg ganha de logo-dark.svg) → svg > png > ico > webp > jpg. Sem logo, cai na inicial do nome da sessão.
  • a inicial da sessão num chip no canto superior esquerdo, porque várias sessões do mesmo repo mostrariam a mesma logo.
  • uma borda colorida derivada do nome da sessão, e o anel azul na ativa.
  • o status na bolinha do canto inferior (pulsando quando o agente trabalha).

Passe o mouse para ver nome, branch, caminho do worktree, status, repo e o atalho ⌘n.

A busca é limitada de propósito: varrer a árvore inteira entraria em node_modules. node_modules, dist, build, .venv e afins ficam de fora, assim como arquivos acima de 2 MB e symlinks que apontam para fora do repo.

.tentacruelinclude

Worktree é checkout limpo: arquivos ignorados pelo git (.env e afins) não existem nele. Crie um .tentacruelinclude na raiz do repo — um glob por linha, # para comentário (um .wtmuxinclude de antes do rename continua sendo lido quando o novo não existe):

# segredos e config local
.env
.env.local
config/*.local.json
certs

Cada match é copiado para o worktree novo preservando o caminho relativo. Padrões absolutos (/etc/...), com .. ou apontando para .git são ignorados por segurança.

.tentacruel/setup.sh

Se existir, roda dentro do worktree novo logo após a criação — para npm install, uv sync, symlinks, o que for. A saída aparece ao vivo na aba Setup. Variáveis disponíveis: TENTACRUEL_REPO, TENTACRUEL_WORKTREE, TENTACRUEL_SLUG, TENTACRUEL_BRANCH.

#!/usr/bin/env bash
set -euo pipefail
echo "provisionando $TENTACRUEL_SLUG"
uv sync
(cd frontend && npm ci)

Se o script falhar, a sessão continua utilizável e o erro fica visível no log.


Arquitetura

src/tentacruel/
  config.py           Settings (uma fonte para portas, caminhos, timeouts)
  models.py           SQLModel: Repo, Session, Chat, Message
  db.py               engine + sessões do SQLite (WAL) + migração no boot
  schemas.py          TODO shape que cruza HTTP/WS — espelhado em types.ts
  main.py             app FastAPI + estáticos + fallback SPA
  cli.py              `tentacruel` (console script)
  api/                rotas finas: repos.py, sessions.py, ws.py, deps.py
  services/
    agents/
      base.py         contrato AgentBackend + emissões normalizadas
      claude.py       ÚNICO lugar que fala com o Claude Agent SDK
      account.py      perfil logado no CLI (só exibição, nunca credencial)
      limits.py       cotas da assinatura: lê o cache do `/usage` do CLI
      registry.py     catálogo de provedores (um provider novo = uma entrada)
    git.py            ÚNICO lugar que roda o binário `git`
    picker.py         ÚNICO lugar que roda `osascript` (seletor de pasta nativo)
    repo_icon.py      encontra a logo/favicon do repo para o avatar da sessão
    files.py          navegação e leitura de arquivos do worktree
    attachments.py    o que o usuário anexa: onde guarda e como chega ao modelo
    transcribe.py     ÚNICO lugar que chama um transcritor de voz
    settings_store.py preferências do usuário (uma chave, um schema)
    toolcheck.py      detecta git/node/uv/npm e versões
    installer.py      instala um CLI de provedor, com streaming
    proc.py           teardown de subprocesso por process group
    worktree.py       ciclo de vida do worktree (sem FastAPI/DB/SDK)
    agent.py          ÚNICO lugar que fala com o Claude Agent SDK
    tool_summary.py   "editou src/x.py" — uma definição só
    manager.py        orquestra DB + worktree + agente + broadcast
    repo.py           registro de repositórios
    hub.py            fan-out de WebSocket
frontend/src/
  styles/tokens.css   ÚNICA fonte de cor, raio, tipografia, densidade
  components/ui/      primitivos — o único lugar que desenha
  components/         compostos do domínio (só primitivos + layout)
  pages/              composição e estado
  lib/                cn, api, ws, types, diff, format
  state/sessions.tsx  estado global + sockets
tests/                pytest (worktree service + API + WebSocket)

Regras estruturais (as mesmas dos dois lados):

  • Nenhum comando git fora de services/git.py.
  • Todo subprocesso nasce com start_new_session=True e morre por process group (services/proc.py). Matar só o filho direto não basta: um neto (npm install disparado pelo setup.sh, um credential helper do git) herda o pipe de stdout e mantém o communicate() do pai bloqueado — o timeout não serviria para nada.
  • Nenhuma chamada a SDK de IA fora de services/agents/<provider>.py. O resto do app só conhece as emissões normalizadas de agents/base.py.
  • services/worktree.py não importa FastAPI, SQLModel nem o SDK — só recebe caminhos e devolve dataclasses. É o que permite que, na v1.1, uma tool custom do SDK (create_worktree_session) chame create_worktree() direto e deixe o próprio agente abrir uma sessão irmã. Não implementado no MVP, mas não bloqueado.
  • Todo shape do backend nasce em schemas.py e é espelhado em frontend/src/lib/types.ts. O OpenAPI fica em /docs.

Endpoints

Método Rota O quê
GET/POST /api/repos listar / registrar repositório
POST /api/repos/browse abre o Finder nativo e devolve a pasta escolhida
GET /api/repos/{id}/icon logo/favicon do repositório, usada no avatar da sessão
GET /api/providers catálogo de provedores de IA, modelos e níveis de esforço
GET/PATCH /api/settings preferências de interface (tema, densidade, layout, idioma)
GET /api/system versão do Tentacruel + dependências detectadas na máquina
PATCH /api/repos/{id} padrões do repositório herdados por sessões novas
WS /ws/install/{provider} instala o CLI de um provedor, com saída ao vivo
DELETE /api/repos/{id} remover do Tentacruel (nada é apagado do disco)
GET/POST /api/sessions listar / criar sessão
GET/PATCH/DELETE /api/sessions/{id} detalhe + chats / renomear + provedor / excluir
POST /api/sessions/adopt abrir um worktree que já existe
GET /api/repos/{id}/worktrees worktrees no disco que nenhuma sessão tem
GET/POST /api/sessions/{id}/sync quantos commits atrás da base / trazer esses commits (merge)
GET /api/sessions/{id}/branches branches locais, marcando a atual e as presas em outro worktree
POST /api/sessions/{id}/branch trocar a branch do worktree — 409 se o git recusar
GET/POST /api/sessions/{id}/chats listar / abrir outra conversa no mesmo worktree
GET/PATCH/DELETE /api/chats/{id} histórico / renomear, modelo, esforço, permissão / fechar
POST /api/chats/{id}/interrupt interromper o turno em execução
POST /api/sessions/{id}/archive arquivar (remove_worktree opcional)
POST /api/sessions/{id}/unarchive reabrir — 409 se o worktree não estiver mais no disco
GET /api/sessions/{id}/diff diff commitado + não commitado + status
GET /api/chats/{id}/usage uso da janela de contexto + cotas da assinatura
GET /api/sessions/{id}/files filhos de um diretório do worktree (listagem preguiçosa)
GET/PUT /api/sessions/{id}/file ler / salvar um arquivo do worktree
POST /api/sessions/{id}/attachments guardar o que o usuário colou, arrastou ou escolheu
POST /api/transcribe áudio → texto, pelo transcritor da máquina (nada é guardado)
GET /api/sessions/{id}/file/raw os bytes de um arquivo, para o navegador desenhar
POST /api/sessions/{id}/commit git add -A && git commit
POST /api/sessions/{id}/push git push -u origin wt/<slug>
WS /ws/sessions/{id} log de provisionamento e lista de chats de um worktree
WS /ws/chats/{id} uma conversa, com streaming
WS /ws/events ciclo de vida das sessões (mantém o rail vivo)

Estado

  • Banco: ~/.tentacruel/tentacruel.db (SQLite/WAL). make reset apaga. Um ~/.wtmux/ de antes do rename é adotado no primeiro init_db(), com o WAL consolidado antes do rename para não perder o histórico recente.
  • Migração: init_db() roda a cada boot e é idempotente. create_all só cria tabelas que faltam, então quatro passos cobrem o resto, nessa ordem: colunas novas (com backfill dos defaults), chats para sessões antigas (adotando as mensagens que ainda apontavam para a sessão), remoção de colunas que o modelo não declara mais e índices que faltam. A remoção só acontece quando a coluna órfã é NOT NULL sem default — a forma que trava todo INSERT, como message.session_id travou depois que a conversa saiu da sessão para o chat. Como o SQLite não faz DROP COLUMN em coluna indexada ou usada por uma foreign key, a tabela é reconstruída.
  • Worktrees: <repo>/.tentacruel/worktrees/<slug>.
  • Histórico do agente: gerenciado pelo próprio Claude Code; o Tentacruel guarda só o agent_session_id para o resume.

Notas do Agent SDK

Verificado contra claude-agent-sdk 0.2.142 e a documentação oficial em https://code.claude.com/docs/en/agent-sdk/python (o domínio platform.claude.com redireciona para lá). Diferenças em relação ao briefing original, com a doc/API real prevalecendo:

Ponto Realidade na 0.2.142
Classe de opções ClaudeAgentOptions (o nome antigo ClaudeCodeOptions não existe mais)
Sessão persistente ClaudeSDKClient.connect() sem prompt mantém o processo vivo; query() envia turnos e receive_messages() consome tudo
session_id vem em SystemMessage(subtype="init").data["session_id"] e também em ResultMessage.session_idSystemMessage tem subtype/data, não content: str
resume ClaudeAgentOptions(resume=<uuid>) mantém o mesmo session_id (com fork_session=False), então o valor guardado continua válido entre reinícios
Streaming incremental exige include_partial_messages=True; os deltas chegam como StreamEvent com event["type"] == "content_block_delta" e delta.type text_delta/thinking_delta
permission_mode o SDK aceita 6 modos; o Tentacruel expõe os 3 do briefing (default, acceptEdits, plan) e troca em tempo real via client.set_permission_mode()
Interrupção client.interrupt()

Outras decisões que divergem da letra do briefing:

  • Tailwind v4 não usa tailwind.config.js. O mapeamento token → utilitário vive em @theme inline dentro de frontend/src/styles/globals.css, que é o equivalente v4 de "mapeados no config do Tailwind". tokens.css continua sendo a fonte única.
  • O diff mostra também o não commitado. O briefing pedia git diff base...HEAD; como em acceptEdits o agente edita sem commitar, o painel renderia vazio. A aba mostra as duas seções (não commitado — incluindo arquivos novos — e commitado), além do git status --short.
  • O rail mostra avatar + nome; branch e status ficam no tooltip. Um rail de ícones não comporta um nome de branch.
  • ESLint foi adicionado manualmente. O template atual do Vite vem com oxlint, que não suporta regras custom; a regra "template first" exigia ESLint.

Desenvolvimento

Além dos pré-requisitos de instalação, trabalhar no código pede Python 3.12+ e Node 20+ — este último só para compilar a interface, que num pacote instalado já vem pronta.

Rodar a partir do código

git clone <este-repo> Tentacruel && cd Tentacruel
make setup        # uv sync + npm install
make dev          # backend :8787 + Vite :5173  → abra http://localhost:5173

Modo "produção local" (tudo servido pelo FastAPI, uma porta só):

make start        # build do frontend + servidor em http://localhost:8787

Outros alvos: make test, make lint, make build, make clean, make reset. make help lista todos.


Testes

make test          # ruff + pytest + tsc + eslint
uv run pytest -k worktree     # só o worktree service

Cobertura atual (427 testes): criação de worktree, .tentacruelinclude (incluindo padrões que tentam escapar do repo), .tentacruel/setup.sh, colisão de slug (nome repetido, branch preexistente, diretório órfão), remoção idempotente, isolamento entre worktrees, diff commitado vs não commitado (incluindo arquivos novos, apagados e grandes demais para inline), commit, push sem remote, resumos de ferramenta, e o ciclo completo da API + WebSocket com um agente de mentira (para não gastar tokens). O caminho com agente real é o teste manual acima.


Publicar uma versão

make dist        # compila o frontend e monta o wheel COM a interface dentro

O alvo falha de propósito se a interface não estiver no pacote. Ela é artefato de build e portanto está no .gitignore, e o hatchling monta a lista de arquivos a partir do controle de versão — sem o artifacts no pyproject.toml, o wheel instala sem UI e serve a página de "frontend não compilado". O erro só aparece depois de instalar, que é o pior lugar para descobri-lo.

A versão vive no pyproject.toml; o app a lê da metadata do pacote. Já esteve escrita em três lugares, o que funciona até alguém editar um e esquecer os outros dois.

Teste manual: os agentes estão mesmo isolados?

Critério de aceite nº 2. Leva ~3 minutos.

  1. make dev e registre um repo qualquer com pelo menos um commit.
  2. Crie três sessões: alfa, beta, gama.
  3. Mande instruções diferentes, uma em cada painel:
    • alfa: "Crie um arquivo ALFA.txt com a palavra alfa. Não commite."
    • beta: "Crie um arquivo BETA.txt com a palavra beta. Não commite."
    • gama: "Liste os arquivos na raiz do projeto e me diga se existe ALFA.txt ou BETA.txt."
  4. No terminal, confira:
cd <seu-repo>
git status --short                     # vazio: o checkout principal está intacto
ls .tentacruel/worktrees/alfa               # ALFA.txt presente
ls .tentacruel/worktrees/beta               # BETA.txt presente
ls .tentacruel/worktrees/alfa/BETA.txt      # No such file or directory  ✅
ls .tentacruel/worktrees/beta/ALFA.txt      # No such file or directory  ✅
git worktree list                      # main + 3 worktrees
  1. O agente gama deve responder que não existe nenhum dos dois arquivos.
  2. A aba Diff de cada sessão mostra apenas o arquivo daquela sessão.

Persistência e resume (critério 3): encerre o make dev (Ctrl+C), suba de novo e reabra o browser. As três sessões continuam no rail com o histórico completo. Pergunte a alfa: "que arquivo você criou no turno anterior?" — ela responde ALFA.txt, porque o Tentacruel guardou o agent_session_id e reconecta com resume.

Remoção (critério 5): nos controles de gama, pasta-com-X → Apagar o worktree do disco. ls .tentacruel/worktrees/ não lista mais gama, git worktree list também não, e git branch --list "wt/*" continua mostrando wt/gama.


Teste manual: o design system é mesmo fonte única?

Critério de aceite nº 7. Leva ~1 minuto. Detalhes em frontend/README.md.

  1. Abra frontend/src/styles/tokens.css e troque uma linha:
--wt-primary: #5b6bf5;   /* → #16a34a */
  1. Com o make dev rodando, o app inteiro fica verde no reload: botão +, anel do avatar ativo, botão Enviar, ícones do assistente, badges, cabeçalho do hunk no diff, anel de foco. Nenhum outro arquivo foi tocado.

  2. Mesma ideia para comportamento: em frontend/src/components/ui/Button.tsx, troque defaultVariants: { variant: "secondary" } para "primary" — todo <Button> sem variant explícito muda junto.

  3. Reverta e rode cd frontend && npm run lint: as regras tentacruel/no-raw-elements e tentacruel/no-visual-classnames falham se alguma tela fora de src/components/ui/ usar um <button> cru ou uma classe visual como bg-[#1a1a1a] / px-[13px].

  4. /dev/ui (http://localhost:5173/dev/ui) mostra o catálogo vivo de tokens e primitivos.


Vindo do wtmux

Sobre o nome. O produto é Tentacruel, e esse é também o identificador técnico: o pacote Python (src/tentacruel/), o comando (tentacruel), o diretório de dados (~/.tentacruel/) e a pasta de worktrees dentro de cada repo (.tentacruel/worktrees/).

Antes, os quatro se chamavam wtmuxworktree multiplexer, no molde do tmux, que multiplexa terminais. Instalações daquela época continuam funcionando, e as duas metades são tratadas de formas diferentes porque o risco não é o mesmo:

  • ~/.wtmux/ é movida no primeiro boot. É diretório nosso, e o WAL do SQLite é consolidado antes do rename — sem isso, o histórico mais recente seria descartado em silêncio, porque o SQLite acha o -wal pelo nome do .db.
  • <repo>/.wtmux/ fica onde está. Aqueles worktrees estão registrados em .git/worktrees/ e podem ter trabalho não commitado de agente dentro. O layout é detectado: repo que já tem .wtmux/ mantém, repo novo nasce .tentacruel/. .wtmuxinclude também continua valendo — é arquivo seu, no seu repo.

Duas abreviações de worktree sobreviveram de propósito, e não têm relação com o nome antigo: o prefixo de branch wt/ e os tokens CSS --wt-*.

Fora do escopo do MVP

Preview de dev server, multi-usuário/auth, empacotamento desktop, merge/PR pela UI (faça no terminal).

Provedores de IA além do Claude Code: a arquitetura está pronta e Codex, OpenCode e Gemini já aparecem em Configurações → Provedores (detectáveis e instaláveis). Falta escrever os adapters em services/agents/<id>.py — o que exige os CLIs instalados para testar de verdade.

Próximo passo previsto (v1.1): tool custom create_worktree_session para o próprio agente abrir uma sessão irmã — a arquitetura já está preparada.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tentacruel-0.1.3-py3-none-any.whl (1.6 MB view details)

Uploaded Python 3

File details

Details for the file tentacruel-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: tentacruel-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.12

File hashes

Hashes for tentacruel-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f201ee53650a15f1148de4ea352caf8b2d5b70acf8734f98afe44cfee41c1313
MD5 f8555f18978edc61f017b4121281ed3f
BLAKE2b-256 177be0c3c5686fcd67d6eb9bfff937aad414b7c8f6b96202d756fce2218bf3e2

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.3 This release

1 file

0.1.2

1 file

0.1.1

1 file

0.1.0

1 file

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page