Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ por trás, assinaturas existentes e lançamento conjunto. Isso amplia as etapas
`workspace --claude` hospeda o binário Claude Code original em PTY oculto,
usa hooks para conversa e permissões na Bee e retoma o ID nativo. Mensagem real
curta comprovada; prompts nativos sem hook são sinalizados e concluídos por
`Ctrl+O` (#29); ferramentas/perfis reais ainda exigem ensaio humano.
`Ctrl+O` (#29); a origem das ações é explícita (#30); histórico e exportação
vêm da transcrição nativa (#31). Ferramentas/perfis reais exigem ensaio humano.
3. Experiência integrada aos dois harnesses, histórico e exportação: pendente.
4. Troca real entre agentes/contas com revisão e preservação da origem: pendente.
5. Lançamento conjunto após todos os aceites, sem substituir o requisito por
Expand Down
31 changes: 30 additions & 1 deletion docs/EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ e a Bee distingue ações aprovadas por ela das liberadas pelo próprio Claude.
Próximo: ensaio humano de ferramentas/permissões reais
([#30](https://github.com/lippdev/memory-bee/issues/30)) e histórico/exportação
revisável ([#31](https://github.com/lippdev/memory-bee/issues/31)).
O modo ainda guarda só IDs; perfis reais e Codex integrado seguem pendentes.
O estado ainda guarda só IDs; histórico e exportação vêm da transcrição nativa
(#31, PR em andamento). Perfis reais e Codex integrado seguem pendentes.
O lançamento da experiência completa ainda exige os dois agentes (ADR 0018).

**Refinamento visual.** Referência HTML e [plano visual](specs/workspace-visual.md)
Expand Down Expand Up @@ -816,3 +817,31 @@ escopo da entrega atual nem a próxima tarefa aprovada.
redesenha antes de encerrar.
- Próximo: ensaio humano do item 52 para fechar #29/#30 e, em código, #31
(histórico e exportação da sessão Claude).

## Claude oculto — histórico e exportação da sessão nativa (#31, código)

- Data: 2026-09-28. PR #51 (#29/#30) integrado por squash em `6b7fe2b`; este
recorte recomeçou o branch da sessão a partir da `main`.
- A transcrição é a informada pelo próprio Claude em `transcript_path` dos
hooks, validada como `<id>.jsonl` regular sem symlink; o estado continua sem
caminho nem conteúdo. Na retomada, a Bee relê o ramo único pelo leitor Claude
existente e mostra até 200 linhas marcadas `↺`, com omissões, leitura parcial
ou ramos múltiplos informados; nada é reenviado. `Ctrl+E` prepara um pacote
v1 somente contexto com referência Git do projeto, prévia com linhas da
transcrição, avisos, omissões e segredos possíveis (texto oculto), exclusões
por linha, rolagem, e `Ctrl+S` grava em `<estado>/exports/` (0700) e roda
`verify`. Ao sair, a CLI imprime o comando de exportação posterior.
- Evidências: testes de biblioteca (validação do caminho, histórico básico,
limite com omissão, transcrição truncada, ramos, exportação verificável,
destino não reutilizado, exclusão); teste unitário da TUI bloqueando possível
segredo até a exclusão da linha e sem vazar o token na prévia (o teste pegou um
vazamento na primeira versão da lista de registros, corrigido); teste PTY com
CLI falsa reportando `transcript_path`: exportação por `Ctrl+E`/`Ctrl+S`,
`memory-bee verify` do pacote, retomada com histórico e nenhum texto reenviado
ao Claude. Telas conferidas em emulador (`pyte`). Checks canônicos, scripts PTY
e `check_bundle.py` passaram localmente (Linux). Autorrevisão.
- Limitações: sem Claude autenticado neste ambiente, a reidratação e a exportação
não foram ensaiadas com transcrição real (item 54; depende também de #25).
Exportação após o Claude encerrar é feita pela CLI `export`, não pela TUI.
Código selecionado (v2) não é oferecido na TUI.
- Próximo: ensaios humanos 52 e 54; depois Codex real no workspace (ADR 0018).
20 changes: 20 additions & 0 deletions docs/MANUAL_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,23 @@ não contar esta seção como executada pelo resultado de CI deste ajuste.
| Item novo | Estado | Evidência manual |
|---|---|---|
| 53: revisão inicial, incremental e rascunho | Pendente | — |

## 54. Histórico e exportação do Claude oculto (pendente)

Em um projeto descartável já confiado, com Git, conversar com
`workspace --claude` (inclua uma leitura e uma execução), sair com `Ctrl+Q` e
retomar com `--resume`. Esperado: bloco "Histórico da transcrição do Claude (não
reenviado)" com as mensagens e ações anteriores marcadas com `↺`, sem nova
resposta do Claude nem prompt reenviado. Pressionar `Ctrl+E`: conferir destino
em `<estado>/exports/`, registros com linha, avisos e omissões; digitar uma linha
em "Excluir linhas", `Enter`, e conferir que ela sai da lista; `Ctrl+S` deve
informar "Pacote salvo e verificado". Rodar `memory-bee verify <pacote>` e ler
`HANDOFF.md`/`history.jsonl` sem a Memory Bee. Repetir após interromper um turno
com `Ctrl+C` e após um erro. Colar um token falso no formato `sk-` seguido de 24
letras numa mensagem: a prévia deve ocultar o texto daquela linha e `Ctrl+S`
recusar até a linha ser excluída. Ao sair, conferir o comando de exportação
impresso. Não versionar pacotes com dados reais.

| Item novo | Estado | Evidência manual |
|---|---|---|
| 54: reidratação na retomada, exportação com exclusões, segredos e verify | Pendente | — |
19 changes: 17 additions & 2 deletions docs/specs/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,23 @@ sessões, 64 KiB), sem transcrição. A pasta é 0700 e arquivos são 0600 em Un
`claude-native.lock` impede segundo escritor. Após crash, verificar que não há
processo ativo antes de remover a trava antiga. `--resume` usa o último ID desta
pasta; não reenvia prompt. A sessão nativa pode não existir quando se sai antes
de enviar uma mensagem. Exportação/revisão de memória, isolamento de perfis e
integração Codex estão pendentes. O modo não comprova
de enviar uma mensagem. Isolamento de perfis e integração Codex estão pendentes.

**Histórico e exportação (#31).** A transcrição vem do campo `transcript_path`
dos hooks oficiais; a Bee aceita só arquivo comum, sem symlink, chamado
`<id-da-sessão>.jsonl`, e não guarda o caminho no estado. Na retomada, ao receber
`SessionStart`, o leitor Claude existente relê o ramo único da transcrição e
mostra até 200 linhas marcadas com `↺` (mensagens, nomes de ações e resumos de
compactação), informando quantas ficaram só na transcrição, leitura parcial ou
ramos múltiplos; nada é reenviado nem inventado. `Ctrl+E` abre a prévia da
exportação somente contexto (pacote v1 com referência Git do projeto): destino
novo em `<estado>/exports/` (0700), registros com a linha da transcrição, avisos,
omissões e possíveis segredos, cujo texto fica oculto na prévia. O campo de
exclusões aceita linhas separadas por vírgula (`Enter` aplica, setas/PgUp/PgDn
rolam); `Ctrl+S` grava o pacote preparado e executa `verify`, recusando gravar
enquanto houver possível segredo. Funciona durante a sessão, inclusive após
erro, interrupção ou limite; ao sair, a CLI imprime o comando `memory-bee export
<transcrição> --preview` para exportar depois. O modo não comprova
conversa/permissões reais por si só; ver [roteiro manual](../MANUAL_TESTS.md).

**Direção atual:** o mantenedor não quer ver a TUI original do Claude. O modo
Expand Down
68 changes: 66 additions & 2 deletions scripts/check_claude_hidden_pty.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,45 @@ def hook(name, **fields):
sys.exit(0)
'''

# Resumable session: reports its transcript through the official hook field.
FAKE_HISTORY = r'''#!/usr/bin/env python3
import json, os, shutil, subprocess, sys
args = sys.argv[1:]
resume = '--resume' in args
session_id = args[args.index('--resume') + 1] if resume else args[args.index('--session-id') + 1]
transcript = os.path.join(os.environ['BEE_TRANSCRIPTS'], session_id + '.jsonl')
shutil.copy(os.environ['BEE_FIXTURE'], transcript)
payload = json.dumps(dict(hook_event_name='SessionStart', session_id=session_id, transcript_path=transcript, source='resume' if resume else 'startup'))
subprocess.run([os.environ['BEE_BINARY'], '__claude-hook'], input=payload, text=True, capture_output=True, check=True)
for line in sys.stdin:
with open(os.environ['BEE_FAKE_STDIN'], 'a') as f:
f.write(line.strip() + '\n')
if line.strip() == '/exit':
sys.exit(0)
'''


def history_and_export(binary, project, state, env, resume):
master, slave, before, proc, output, until = spawn(binary, project, state, env, resume)
try:
if resume:
until(b'decrescente') # Previous user message read back from the transcript.
else:
until(b'pronto')
os.write(master, b'\x05') # Ctrl+E opens the export preview.
until(b'Destino')
os.write(master, b'\x13') # Ctrl+S writes and verifies.
until(b'verificado')
os.write(master, b'\x11')
assert drain_until_exit(master, proc, output, 10) == 0
assert termios.tcgetattr(slave) == before
finally:
if proc.poll() is None:
proc.kill()
proc.wait()
os.close(master)
os.close(slave)


def drain_until_exit(master, proc, output, seconds):
"""Keep reading while waiting: a full PTY buffer (small on macOS) would
Expand All @@ -79,11 +118,13 @@ def drain_until_exit(master, proc, output, seconds):
return proc.returncode


def spawn(binary, project, state, env):
def spawn(binary, project, state, env, resume=False):
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0))
before = termios.tcgetattr(slave)
argv = [str(binary), 'workspace', '--claude', '--project', str(project), '--state', str(state), '--no-color']
if resume:
argv.append('--resume')
proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave, env=env)
output = bytearray()

Expand Down Expand Up @@ -227,7 +268,30 @@ def main():
assert not stdin_log.exists(), 'Ctrl+Q typed into the native dialog'
untrusted(binary, project, root / 'state-setup', env, False)
assert stdin_log.read_text().splitlines() == ['1', '/exit']
print('PASS: Bee-only UI, hidden original output, hooks, deny/allow, Ctrl+Q denial, resume, blocked native setup via Ctrl+O, safe Ctrl+Q and terminal restoration')
with tempfile.TemporaryDirectory(prefix='bee-history-') as temp:
root = Path(temp)
fake_dir = root / 'bin'
fake_dir.mkdir()
fake = fake_dir / 'claude'
fake.write_text(FAKE_HISTORY)
fake.chmod(0o700)
project = root / 'project'
project.mkdir()
transcripts = root / 'transcripts'
transcripts.mkdir()
state = root / 'state'
stdin_log = root / 'stdin'
fixture = Path(__file__).resolve().parent.parent / 'testdata/claude/basic.jsonl'
env = dict(os.environ, PATH=f'{fake_dir}:/usr/bin:/bin', BEE_BINARY=str(binary), BEE_FAKE_STDIN=str(stdin_log), BEE_TRANSCRIPTS=str(transcripts), BEE_FIXTURE=str(fixture))
history_and_export(binary, project, state, env, False)
bundles = list((state / 'exports').iterdir())
assert len(bundles) == 1, bundles
verified = subprocess.run([str(binary), 'verify', str(bundles[0])], capture_output=True, text=True)
assert verified.returncode == 0, verified.stdout + verified.stderr
history_and_export(binary, project, state, env, True)
# History is shown from the transcript, never typed back into Claude.
assert stdin_log.read_text().splitlines() == ['/exit', '/exit']
print('PASS: Bee-only UI, hidden original output, hooks, deny/allow, Ctrl+Q denial, resume, blocked native setup via Ctrl+O, safe Ctrl+Q, history rehydration, verified export and terminal restoration')


if __name__ == '__main__':
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ fn workspace(args: &[std::ffi::OsString]) -> Result<u8, (u8, String)> {
}
let state_path =
state.ok_or((64, "workspace --claude exige --state <private-dir>".into()))?;
let (id, code) = if claude_terminal {
let (id, code, transcript) = if claude_terminal {
tui::claude_native::run(Path::new(&project), &state_path, resume, mode, no_color)
} else {
#[cfg(unix)]
Expand All @@ -642,6 +642,12 @@ fn workspace(args: &[std::ffi::OsString]) -> Result<u8, (u8, String)> {
}
.map_err(|e| (1, e))?;
println!("Claude Code encerrou. Sessão nativa: {id}");
if let Some(transcript) = transcript {
println!(
"Para exportar depois: memory-bee export {} --preview",
transcript.display()
);
}
return Ok(if code == 0 { 0 } else { 4 });
}
if once {
Expand Down
Loading
Loading