Referência da linha de comando¶
Depois de instalado, flower é um único executável, 4 subcomandos, 23 flags. Esta página lista todas elas: o tipo de cada flag, o valor padrão, a semântica exata, mais como falar com a run no meio do caminho, o que ele pergunta na primeira execução, quais são os códigos de saída e quais arquivos ele coloca no seu diretório. Depois de ler esta página você não precisa abrir o código-fonte.
Código-fonte: flower/cli.py.
| Subcomando | O que faz | Argumento posicional | Flags próprias |
|---|---|---|---|
go | Tudo em um: esclarecer o pedido → definir o objetivo → delegar o trabalho → verdict a cada rodada. É o padrão quando você não escreve subcomando | ask (opcional) | 11 |
run | Executa um workflow escrito por você | target (obrigatório) | 0 |
once | Executa um único agent uma vez, sem workflow e sem verdict | prompt (obrigatório) | 6 |
setup | Configura credenciais, grava em ~/.config/flower/.env | nenhum | 0 |
Total de 23 flags = 5 globais + 11 próprias do go + 6 próprias do once + -h/--help. run e setup não têm flags próprias.
Formas de invocação¶
Todo o argv do flower passa primeiro por _with_default_cmd(), que completa o subcomando padrão, e só depois vai para o argparse (cli.py:1437-1439). É por isso que flower "帮我做一个 X" funciona — ele é reescrito como flower go "帮我做一个 X".
As regras para completar o subcomando padrão (cli.py:940-976):
- O conjunto de flags globais é derivado do próprio parser principal, não é uma lista fixa no código. As que têm
nargs == 0contam como flags puras; as demais contam como flags com valor. - Varre da esquerda para a direita, pulando as flags globais. As que levam valor são puladas junto com o valor, e a forma com
=, tipo--workspace=/tmp, também é reconhecida. - Para no primeiro token que não é uma flag global. Se ele for
go,runouonce, é entregue ao argparse como está; caso contrário, insere umgoantes dele, e ele vira o corpo do pedido dogo. - Se a varredura terminar sem encontrar argumento posicional (argv vazio, ou só com flags globais) → acrescenta
gono final e entra na entrada interativa. - Exceção: se o argv contiver
-hou--help, é devolvido como está, para o argparse imprimir a ajuda.
A constante usada nessa decisão é _CMDS = ("go", "run", "once") (cli.py:937) — setup não está lá dentro, veja as consequências em setup.
O que a reescrita realmente produz¶
| O que você digitou | Como é realmente interpretado | Efeito |
|---|---|---|
flower | ["go"] | Pergunta interativamente "要做什么?" |
flower -v | ["-v", "go"] | Idem, com verbose |
flower "帮我做一个 X" | ["go", "帮我做一个 X"] | Começa direto |
flower -w /tmp "做 X" | ["-w", "/tmp", "go", "做 X"] | Flags globais podem vir antes |
flower --workspace=/tmp "做 X" | ["--workspace=/tmp", "go", "做 X"] | A forma com = também é reconhecida |
flower "做 X" --timeout 0 | ["go", "做 X", "--timeout", "0"] | Flags do subcomando podem vir depois do pedido |
flower --timeout 0 "做 X" | ["go", "--timeout", "0", "做 X"] | Também podem vir antes |
flower --new | ["go", "--new"] | Só flag, sem pedido → entrada interativa |
flower once "hi" | ["once", "hi"] | Como está |
flower run flows:main | ["run", "flows:main"] | Como está |
flower run | ["run"] | O argparse reclama que falta target, não trata como pedido |
flower go run | ["go", "run"] | Desambiguação explícita: o corpo do pedido é run |
flower setup | ["go", "setup"] | Executa o go, com o pedido sendo a string setup, veja setup |
flower --help | Como está | O argparse imprime a ajuda |
As palavras run e once não podem ser usadas diretamente como corpo do pedido; essa ambiguidade foi deixada de propósito (cli.py:949-950). Para usá-las como pedido, escreva flower go run.
As seis formas que funcionam¶
flower # 1. Sem nada: pergunta "要做什么?" ou "接着上次?"
flower "帮我做一个 X" # 2. Pedido como argumento posicional
echo "帮我做一个 X" | flower --timeout 0 # 3. Alimenta o stdin por pipe
flower once "读一眼这个仓库" # 4. Agent único
flower run flows.py:main # 5. Executa um workflow próprio
flower go setup # 6. go explícito, com setup como corpo do pedido
A forma de módulo python -m flower.cli é equivalente a flower (cli.py:1451-1452). Os parâmetros do wrapper de container docker/flowerbox são exatamente iguais aos do flower.
Alimentar o stdin por pipe¶
Quando sys.stdin.isatty() é falso, ask_for_prompt() não imprime o cabeçalho do prompt e lê uma linha direto com input("> ") (cli.py:993-1001). Por isso echo "..." | flower funciona.
Mas logo em seguida ele imprime um aviso, e a thread de stdin encontra EOF imediatamente e sai:
Rodar por pipe deve vir com --timeout 0: as perguntas deixam de fingir que esperam 30 minutos, falham na hora, e o agent decide sozinho e escreve as suposições na seção "未知与假设" do brief.
Subcomandos¶
go¶
Texto de ajuda: 一键跑:问清需求 → 派人干活(不写子命令时的默认) (cli.py:1275-1307).
Argumento posicional ask, com nargs="?" — se não for dado, entra na entrada interativa. É a porta de entrada mais usada; flower "做 X" passa por aqui.
O que ele faz (cli.py:1190-1221):
ensure_credentials()— verifica as credenciais e realmente dispara uma sonda à API, veja Fluxo de configuração na primeira execução.- Sondagem de wake: apenas dá uma olhada de leitura para ver se este diretório já foi usado, sem gravar um único byte.
- Se
asknão foi dado, mostra o prompt e pergunta; digitar/newequivale a--new, e então pergunta o pedido de novo. - Se for continuity, imprime uma linha de banner de wake.
- Monta o workflow de três steps:
确认需求→设定目标→干活, com um干活·判定#Ndepois de cada rodada de trabalho.--clarify-onlydeixa só o primeiro step. - Começa a rodar.
O banner de wake é assim (o diretório home no caminho é trocado por ~):
需求已确认 aparece sempre; 目标 N 条 só aparece quando existe lista de verdict; 干活上下文 X exige que o contexto da última rodada daquela session possa ser consultado em sessions.db — se não for encontrado, não é exibido.
-W e -T são silenciosamente sobrescritas no caminho do go
Essas duas flags globais não fazem nada no go, sem erro e sem aviso:
-W/--workbench: o workflow montado pelogosempre já traz seu próprio workbench, e o código pegagetattr(wf, "workbench", None) or args.workbench(cli.py:1038) — o do workflow sempre tem prioridade. Ou seja, o workbench é sempre<workspace>/.flower/(com--isolateé<workspace>.parent/.flower-<nome>/), e-Wnão muda isso.-T/--trim: ogopassa por_drive(wf, args, trim=not args.no_trim)(cli.py:1221), usando diretamente o inverso de--no-trime sem nem olhar paraargs.trim. Isto é, no caminho dogoo trim já vem ligado por padrão, e a única forma de desligar é--no-trim.
Essas duas flags só têm efeito em run (quando o workflow não traz workbench próprio) e em once.
As 11 flags do go¶
| Flag | Tipo | Padrão | Descrição |
|---|---|---|---|
--asks N | int | -1 | Cota de perguntas. -1 ou qualquer negativo = sem limite; 0 = proibido perguntar, a primeira pergunta já dá over_budget; N = cota rígida. Ao estourar, a ferramenta recusa direto, sem bloquear a run |
--rounds N | int | 3 | Limite do número total de rodadas de trabalho, não de rodadas extras. Ao fim de cada rodada, um judge independente decide "está pronto ou não"; se não estiver, devolve e continua a mesma session |
--no-goal | flag | False | Desliga o goal guard: não gera 目标.md, não faz verdict, o trabalho termina quando o step termina |
--judge-can-run | flag | False | Deixa o judge executar comandos. O verdict fica mais duro, ao custo de ele também poder alterar o workspace |
--timeout segundos | float | 1800.0 | Quanto tempo esperar por uma resposta humana. 0 ou negativo = totalmente automático, todas as perguntas falham imediatamente, sem fingir espera. Semântica em timeout |
--isolate | flag | False | Dá a cada subagent um git worktree próprio, ou seja, isolation. Exige que o workspace seja um repositório git, senão código de saída 1. Também move o workbench para fora do repositório |
--window N | int | nenhum (inferido pelo nome do modelo) | Janela de contexto do modelo. Quando não dado: nome do modelo contém 1m ou não contém haiku → 1,000,000; contém haiku → 200,000. Ao chegar em janela − 50000 escreve o handoff document e faz o handoff |
--no-handoff | flag | False | Desliga o handoff e volta ao compact nativo do SDK |
--new | flag | False | Não continua de onde parou. Move (não apaga) o lineage.json + 需求.md + 目标.md do trecho anterior para notes/archive/<YYYYmmdd-HHMMSS>/ e recomeça do zero |
--clarify-only | flag | False | Faz só o clarify, sem seguir para o trabalho — sobra apenas o step 确认需求 no workflow |
--no-trim | flag | False | Desliga o trim. No caminho do go o trim vem ligado por padrão, e esta é a única forma de desligá-lo |
Cantos dos valores, todos silenciosos, sem erro e sem aviso:
--rounds 0e--rounds 1são equivalentes — internamente éretries = max(0, rounds - 1), ambos rodam 1 rodada.- Qualquer negativo em
--askssignifica sem limite, não só-1. - Qualquer negativo em
--timeoutequivale a0, ou seja, totalmente automático. --window 0é silenciosamente ignorado (0é falsy, nem chega a ser repassado) e volta ao valor padrão inferido pelo nome do modelo. Negativos são repassados e depois presos em10000.--clarify-onlyé uma operação vazia num diretório já esclarecido — o step确认需求vê um需求.mdcompleto e pula, e como esse é o único step do workflow, nada acontece (fora o contador de wake +1). Para reesclarecer, combine com--new.- O
--helpdogotermina com "全局开关(-v/-w/-r/-T)见flower --help"; essa linha esqueceu o-W.
run¶
Texto de ajuda: 运行一个 workflow (cli.py:1309-1312).
Argumento posicional target, escrito como módulo:atributo. As duas formas são suportadas (cli.py:1010-1031):
flower run mypkg.flows:build # import pelo nome do módulo
flower run flows.py:build # caminho de arquivo; coloca o diretório pai no sys.path e importa pelo nome do arquivo
Se o atributo obtido for chamável, ele é chamado uma vez e o retorno vira o workflow; se já for um objeto de workflow, é usado direto.
run não tem nenhuma flag própria, só as 5 globais. Então --window, --no-handoff e afins ficam sempre no padrão nesse caminho (o código usa getattr como fallback, cli.py:1041-1043). Para ajustá-los, escreva os parâmetros dentro do seu próprio workflow.
once¶
Texto de ajuda: 跑一次单 agent (cli.py:1314-1324). O argumento posicional prompt é obrigatório.
Ele monta um AgentSpec(name="ad-hoc", …) e executa direto, sem passar pelo _drive. Por isso o once não tem:
- Ctrl-C para interromper e falar (apertar gera um
KeyboardInterruptcomum) - Thread de resposta no stdin, nem o prompt de entrada fixo na parte de baixo
- Perguntas ao oracle
- Gravação de emergência da contabilidade em SIGHUP / SIGTERM
- A linha final
总花费 … · 清单 … - Guia de reconfiguração automática depois de falha de credencial
No run manifest, o nome deste step é sempre ad-hoc.
| Flag | Tipo | Padrão | Descrição |
|---|---|---|---|
-i, --instructions | str | vazio | Instruções de domínio, feitas em append depois do system prompt nativo do Claude Code, sem substituí-lo |
-t, --tools | str | Read,Glob,Grep | Lista branca de ferramentas separada por vírgula. Sem valor, são essas três ferramentas somente-leitura |
-p, --permission-mode | str | default | Só aceita default, acceptEdits, plan ou bypassPermissions; qualquer outro valor faz o argparse abortar com código de saída 2 |
-b, --budget | float | sem limite | Teto de budget em dólares; ao estourar, para |
--resume SESSION_ID | str | nenhum | Continua uma session existente |
--fork | flag | False | Bifurca em vez de continuar, usado junto com --resume |
O tempo e o custo acumulado mostrados pelo once são sempre 0
O once cria uma nova instância de renderizador a cada evento recebido (cli.py:688-690, cli.py:1239), e tanto o início da contagem de tempo quanto o custo acumulado ficam guardados na instância (cli.py:500-501). Portanto:
- O
用时da linha final é sempre0:00 - O
累计 $0.00da linha de status é sempre 0, e o上下文também nunca acumula
O custo real de cada step tem que ser lido no campo cost_usd de runs/manifest.json. Os caminhos go e run mantêm a mesma instância de renderizador e não têm esse problema.
setup¶
Texto de ajuda: 配置凭证(API key / 网关 / 模型),写到 ~/.config/flower/.env (cli.py:1326-1328). Sem nenhuma flag.
O que ele faz: lê o .env → decide se já foi configurado → inicia o fluxo interativo de configuração, com reason sendo 重新配置。 ou 还没配过凭证。. O conteúdo da tela está em Fluxo de configuração na primeira execução.
flower setup hoje não chega nesse subcomando
A constante de decisão do subcomando padrão, _CMDS = ("go", "run", "once") (cli.py:937), esqueceu "setup", mas o setup está de fato registrado no parser (cli.py:1326). Assim, flower setup é reescrito como flower go setup — o que roda é o workflow completo do go, com o corpo do pedido sendo a string setup: primeiro valida as credenciais, depois pergunta o pedido e então começa mesmo a delegar o trabalho. Com flags globais é igual: flower -v setup → ["-v", "go", "setup"].
Não existe nenhum argv capaz de chegar ao subcomando setup.
Para configurar credenciais, hoje só há estes dois caminhos, e ambos levam à mesma interface interativa:
- Rodar direto
flower "随便一句诉求"; se não houver credencial configurada, ele pergunta antes; - Ou escrever
~/.config/flower/.envà mão, com as chaves listadas em As chaves que ele grava.
Alguns textos também ficam afetados: o 跑 `flower setup` 重配。 impresso quando a credencial é recusada, e o comentário da primeira linha do .env, 由 `flower setup` 写, apontam todos para esse comando inalcançável.
Flags globais¶
As 5 flags globais ficam penduradas ao mesmo tempo no parser principal e em cada subcomando (cli.py:1250-1266). A cópia dos subcomandos usa argparse.SUPPRESS: se não for passada, não escreve o atributo, então tanto faz escrevê-las antes ou depois do subcomando, uma não sobrescreve a outra. O efeito colateral é que elas não aparecem no --help dos subcomandos — para vê-las, rode flower --help.
| Flag | Tipo | Padrão | Descrição |
|---|---|---|---|
-w, --workspace | str | . | Diretório de trabalho do agent. É passado por resolve() para caminho absoluto e criado com mkdir -p. O workbench .flower/ é criado aqui dentro |
-r, --run-dir | str | runs | Diretório do session store e do run manifest. Relativo ao CWD atual, não ao workspace |
-v, --verbose | flag | False | Imprime mais coisas, veja abaixo |
-W, --workbench | flag | False | Habilita o workbench. Não tem efeito no go, só vale para run (quando o workflow não traz workbench próprio) e once, e nesse caso o workbench fica em <run_dir>/workbench/ |
-T, --trim | flag | False | No resume, troca resultados grandes de ferramenta antigos por ponteiros de arquivo, ou seja, trim. Não tem efeito no go, que controla isso ao contrário com --no-trim |
-h, --help | flag | — | Existe em todo parser. Quando aparece no argv, a reescrita do subcomando padrão é pulada e a ajuda é impressa direto |
O detalhe de -r/--run-dir ser relativo ao CWD morde: flower -w /other/proj "做 X" cria runs/ no diretório onde você digitou o comando, enquanto .flower/ é criado dentro de /other/proj/ — os dois estados se separam. Para mantê-los juntos, passe explicitamente -r /other/proj/runs.
A ajuda do -v diz "显示思考与工具结果", mas o pensamento da main thread já é exibido por padrão. O que o -v realmente liga a mais é:
- O corpo do texto dos subagents (não exibido por padrão, só as chamadas de ferramenta deles)
- Resultados normais de ferramenta (por padrão só os que deram erro)
- Eventos
prompt - Antes de iniciar, imprime uma vez a configuração de credenciais em vigor, com o token mascarado deixando só os 4 primeiros caracteres
Esta última passa por um print() cru, sem sanitização de saída, sem quebra de linha e sem a proteção do lock de escrita do terminal; ao rodar vários flower em paralelo, essas linhas podem sair rasgadas.
Como falar com ele durante a run¶
Depois que a run começa, o terminal está o tempo todo lendo o que você digita. Não é preciso esperar uma pergunta nem apertar nada para entrar em modo de entrada — a última linha é sempre a linha onde você pode digitar.
O prompt de entrada fixo embaixo¶
Há uma thread daemon flower-stdin lendo o stdin o tempo todo (cli.py:764-934), usando select com polling a cada 0.2 segundos, não leitura bloqueante (assim o sinal de parada consegue acordá-la; em streams que não suportam select, como no Windows, ela degrada para leitura bloqueante).
Ela lê o tempo todo, não só quando há uma pergunta. A razão é: se só lesse durante as perguntas, o que você digitou nas horas de trabalho ficaria no buffer do terminal e seria engolido como resposta na pergunta seguinte — você seria respondido antes mesmo de ver a pergunta.
Na exibição, _say() é a única saída; antes de cada escrita ele apaga o prompt e redesenha depois (cli.py:309-315), então o prompt nunca é empurrado para cima pela saída de eventos. No redesenho, os caracteres que você digitou pela metade e ainda não deu Enter voltam junto — eles ficam em _PROMPT["buf"] (cli.py:183-192). Sem isso, o conteúdo na verdade não se perde (ainda está no buffer de linha do terminal, e o Enter o envia normalmente), mas você não o vê, fica inseguro e redigita.
O prompt tem dois textos, alternando conforme haja ou não pergunta pendente:
| Estado | Última linha da tela |
|---|---|
| Com pergunta pendente | 你的回答 (回车=跳过,让它自己判断) > |
| Sem pergunta pendente | (直接说 = 加需求,下个检查点送达;? 开头 = 顺便问一句,不打扰它干活) > |
Modo de entrada caractere a caractere e teclas¶
Para redesenhar "os caracteres digitados pela metade", o flower precisa assumir a entrada. Quando o stdin é um terminal e é possível import termios, o terminal é colocado em cbreak antes de iniciar a thread flower-stdin (cli.py:793-807) — usa cbreak e não raw para que Ctrl+C continue gerando SIGINT, mantendo todo o comportamento de Ctrl-C. Tem que ser antes de iniciar a thread: colocar isso dentro da thread cria uma corrida real, e os caracteres digitados no instante em que a thread ainda não pegou CPU são engolidos pelo modo de linha, aparecendo como "a entrada sumiu" (reproduzido de forma estável uma vez a cada três testes, cli.py:928-934).
Se não der para configurar, volta ao readline() de linha inteira (não é terminal, termios indisponível, tcgetattr falhou). Os dois caminhos funcionam, só que no modo de linha as teclas abaixo não existem (cli.py:883-899).
A lógica de edição está em LineEditor (cli.py:320-414), uma máquina de estados pura que não toca no terminal:
| Tecla | Efeito |
|---|---|
| Caractere imprimível | Inserido na posição do cursor. UTF-8 usa um decodificador incremental e só entra no buffer quando um caractere completo é formado |
| Backspace / Ctrl+H | Apaga um caractere antes do cursor. No modo de linha o terminal apaga por byte, um caractere chinês exige três toques e ainda sai lixo; aqui não |
| ← / → | Move o cursor de verdade. A sequência de escape inteira é consumida, sem inserir coisas como [A na entrada |
Home / End (ou [1~ / [4~) | Salta para o início / fim da linha |
Delete ([3~) | Apaga um caractere para frente |
| Ctrl+A / Ctrl+E | Início / fim da linha |
| Ctrl+U | Limpa a linha inteira |
| Ctrl+D | Só é EOF quando o buffer está vazio; com conteúdo, é ignorado |
| ↑ / ↓ | Não fazem nada. Não há histórico, e mexer só faria você achar que perdeu algo (cli.py:335) |
| Outros caracteres de controle | Ignorados |
O Enter entrega o buffer e o esvazia, além de pular uma linha na tela — o que você disse fica ali em cima (cli.py:811-827).
Para onde vai o que você digita¶
| O que você digita | Com pergunta pendente | Sem pergunta pendente |
|---|---|---|
| Linha vazia (só Enter) | Pula a pergunta, deixa ele decidir sozinho | Não faz nada |
Começando com ? | Pergunta ao oracle, veja abaixo | Idem |
| Só dígitos, dentro da faixa das opções | Substitui pela opção correspondente e responde | Tratado como texto comum |
| Outro texto | Enviado como resposta ao agent que perguntou | Vai para a caixa de entrada, como requisito adicional |
| EOF (Ctrl-D ou pipe fechado) | Recusa a pergunta, remove o prompt, a thread sai | Remove o prompt, a thread sai |
Ao entrar na caixa de entrada, ele imprime um recibo:
Quando não há brief onde fazer o spill, a segunda metade vira 没有确认书可落盘 —— 它可能活不过下一个步骤. A caixa de entrada não interrompe o worker que está trabalhando; ele só pega da próxima vez que consultar a caixa por iniciativa própria. A mesma frase também é anexada a notes/需求.md; sem spill, ela não sobrevive à fronteira do step — o próximo step é uma session nova, que só lê os artefatos congelados.
Começar com ? = pergunta ao oracle¶
Uma linha que começa com ? não é enviada ao agent em execução, e sim ao oracle:
Ele sobe um Runtime independente, com run_dir em <run_dir>/aside/, então o custo e a lineage de sessions dele não se misturam ao manifest.json principal. O papel é somente-leitura, as ferramentas são apenas Read, Glob, Grep, no máximo 12 rodadas, com teto de custo de $0.5. O contexto que ele vê são os 60 eventos mais recentes (eventos thinking e prompt não entram nessa janela), cada um truncado em 200 caracteres, mais a descrição do caminho do workbench.
Ele roda concorrentemente; a run em andamento não espera um segundo. A resposta é assim:
Em caso de falha, imprime uma linha em vermelho # 旁路问答失败:<类型>: <消息>, sem afetar o fluxo principal. Na saída, espera no máximo 120 segundos pelo encerramento do oracle, e antes de esperar imprime uma linha (等 N 条旁路问答收尾…).
O que ele diz não entra no contexto daquela run — perguntar não afeta a run, e a resposta é descartada depois.
O ? de largura total não aciona o oracle — usuários de IME chinês vão tropeçar
A linha de código que decide a pergunta ao oracle é (cli.py:907):
Os dois caracteres são o ? ASCII de meia largura (0x3f) — verificado byte a byte. Pela forma como foi escrito, a intenção era claramente aceitar tanto o ? de meia largura quanto o ? de largura total (U+FF1F) produzido por IMEs chineses, mas na prática ficou o mesmo caractere.
Consequência: uma linha que começa com o ? de largura total não é tratada como pergunta ao oracle, e sim enviada silenciosamente à caixa de entrada como "requisito adicional", sendo então anexada a notes/需求.md. O recibo que você vê é + 收到, não # 旁路.
Para perguntar ao oracle, é obrigatório usar o ? de meia largura — troque o IME para inglês antes de digitar, ou digite ao menos o primeiro caractere em meia largura.
O que aparece na tela¶
Os ícones são sempre ASCII, não emoji (cli.py:51-69). A razão está no comentário do código: emoji junto com caracteres de moldura, geométricos e setas dispara fallback de glifos no terminal, o que já causou dois travamentos de terminal.
| Ícone | Significado | Ícone | Significado |
|---|---|---|---|
= | Separador de step | + | Concluído / respondido / recebido |
~ | Pensamento, retentativa | x | Falha / erro |
> | Delegação | # | Handoff, oracle, tarefa |
* | Chamada de ferramenta | - | Linha de status, item de lista |
? | Pergunta | <- | Continuação, ponto de aterrissagem do handoff |
! | Aviso / interrupção | . | Pulado |
\| | Barra vertical de indentação do subagent |
O ❓ e o ↩ da documentação antiga não existem no terminal real
A documentação inicial usava ❓ para pergunta e ↩ para a linha de wake. No código nunca foram esses dois caracteres — o ícone de pergunta é o ? de meia largura, e o ícone de wake e do ponto de aterrissagem do handoff são os dois caracteres ASCII <-.
Então o que o terminal real imprime é:
Não é ❓ 这个工具……, nem ↩ 在 ~/proj 接上上次. Fazer grep nos logs seguindo a documentação antiga não encontra nada.
Os cinco estados de uma pergunta, na tela, são:
| Estado | Saída na tela |
|---|---|
| Perguntou | ? <问题>, seguido das opções uma a uma 1) 选项一, e havendo cota, mais (还能问 N 次) |
| Respondeu | + <答案> |
| Timeout | ! 无人应答 —— 它会自己判断,把假设记进「未知与假设」 |
| Cota esgotada | ! 提问额度用完 |
| Você pulou | . 已跳过 |
Quando --asks está sem limite (o padrão), a última linha "还能问 N 次" não é exibida.
Quando o handoff termina de escrever o handoff document, sai um bloco inteiro:
# 上下文 950.0K/1000K —— 写交接准备换代
- 现在在做 …
- 已定的事 …
- 走不通的 …
- 下一步 …
<- 交接写在 ~/proj/.flower/notes/交接-干活.md
<- 新会话接手,上下文从 950.0K 重新开始
Quando o handoff document é degradado, entra uma linha vermelha extra 交接没写成,用了降级版本 —— 接手的人会自己去现场看.
A saída também faz duas coisas que você não vê: todas as linhas passam por uma sanitização que só deixa passar os códigos SGR de cor do próprio flower, engolindo por inteiro sequências de limpar tela e mover cursor cuspidas pelo modelo ou pelas ferramentas; e a largura é max(40, min(colunas do terminal, 110)), então em terminais largos o texto não ocupa a linha inteira — isso é intencional.
O prompt na largada¶
Rodando flower puro (sem pedido), ele pergunta antes. Dois textos:
O segundo só aparece quando este diretório já foi usado e as quatro seções de 需求.md estão completas.
Esse prompt lê via input(), sem passar pelo parsing do shell. Aspas chinesas, espaços e pontos de exclamação podem ser digitados direto — essa é toda a razão de ele existir. O zsh, ao encontrar a aspa direita chinesa, entra em dquote> esperando continuação, o que parece travamento, mas na verdade nada chegou a iniciar.
- Entrada vazia + primeira vez → sai, imprimindo
诉求是空的。直接 `flower` 然后按提示输入,或者 flower "帮我做一个 X"。 - Entrada vazia + wake → é válido, significa "continuar"
- Digitar
/new→ equivale a--new, arquiva o trecho anterior e pergunta o pedido de novo - Ctrl-C / Ctrl-D → sai, imprimindo
已取消
Timeout¶
--timeout é float, em segundos, padrão 1800.0. Três faixas de valor:
| Valor | Comportamento |
|---|---|
> 0 | Espera esses segundos. Ao estourar, a pergunta é liquidada como timeout e o agent decide sozinho |
0 ou negativo | Totalmente automático. A pergunta não entra na fila de espera, não emite evento asked, não aparece na tela, e é liquidada como timeout na hora |
| Esperar para sempre | Impossível pela linha de comando. Internamente há suporte a "esperar para sempre", mas --timeout é float e tem valor padrão, e nenhuma forma de escrita o produz. O teto é passar um número muito grande de segundos |
--timeout 0 e --timeout -1 são completamente equivalentes. Rodar por pipe, em CI ou sem supervisão usa exatamente isso.
Quando a pergunta não obtém resposta, o resultado de ferramenta devolvido ao modelo é um texto fixo, em quatro variantes:
| Resultado | Texto devolvido ao modelo |
|---|---|
| Cota esgotada | 提问额度已用完。不要再问了 —— 把剩下的不确定项写进「未知与假设」那一段,按你自己的判断继续。 |
| Timeout | 无人应答。按你自己的判断继续,并把这个问题和你采用的假设写进「未知与假设」那一段。不要重复提问,也不要停在这里。 |
| Você pulou | 对方跳过了这个问题。按你自己的判断继续,并把假设写进「未知与假设」。 |
| A pergunta estava vazia | 问题是空的。把问题写清楚再问。 |
Ctrl-C¶
A semântica do Ctrl-C nos dois lugares é completamente diferente.
Apertado no prompt de largada > — sai do programa direto, imprimindo 已取消.
Apertado no meio da run — interrompe a rodada atual e te dá uma chance de falar:
Dar Enter direto aqui significa apenas interromper sem dizer nada e seguir. Se houver perguntas pendentes no momento, sai uma linha a mais: (有 N 个提问还等着,打断不影响它们).
Apertar Ctrl+C outra vez é sair de verdade, e é um KeyboardInterrupt não capturado — vai aparecer um traceback de Python na tela, não é uma saída limpa.
A interrupção é cooperativa: corta de forma limpa na fronteira de mensagem, sem cancelar tarefas à força. Ela não conta como tentativa falha e não consome retentativas. Ao continuar, é anexada uma explicação dizendo ao modelo que "chamadas de ferramenta em voo retornando interrupted é um efeito colateral normal da interrupção, não uma falha de ambiente".
Esse Ctrl-C customizado só é instalado quando sys.stdin.isatty() (cli.py:1097). Rodando por pipe, mantém o comportamento padrão do Python, ou seja, sai já na primeira vez. O caminho do once não passa por aqui, então o Ctrl-C no once também sai já na primeira vez.
SIGHUP / SIGTERM¶
Os caminhos go e run instalam tratamento para SIGHUP e SIGTERM: primeiro gravam também o step em voo no manifest.json, marcado como killed-by-signal, depois restauram a ação padrão e realmente saem.
A origem disso é que, quando o terminal trava, o kernel manda SIGHUP, cuja ação padrão encerra o processo direto: o finally não roda, o manifest não é gravado — e a contabilidade daquela run se perde. Em threads que não são a principal do sistema operacional, ou em plataformas sem suporte, isso é pulado silenciosamente.
Fluxo de configuração na primeira execução¶
As três portas de entrada go, run e once chamam ensure_credentials() logo no começo (cli.py:1392-1428), com duas barreiras.
Primeira barreira: existe credencial?¶
Procura credenciais por ordem de prioridade. Se não achar ANTHROPIC_API_KEY nem ANTHROPIC_AUTH_TOKEN, inicia a configuração interativa; em modo não interativo (stdin não é terminal), não bloqueia, imprime este bloco e sai:
缺少凭证:需要 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN。
最省事:跑一次 `flower setup`,把 token 存到 /Users/you/.config/flower/.env(装一次,处处生效)。
或者:在当前目录建 `.env`,或 export 进进程环境。
flower 不读 ~/.claude/settings.json —— 那是可移植性的代价。
Este texto tem dois pontos que não batem com a implementação: o flower setup da segunda linha hoje é inalcançável (veja setup); e a quarta linha contradiz o código — o flower de fato usa o bloco env de ~/.claude/settings.json e settings.local.json como último nível de fallback, pegando emprestado apenas 9 chaves de credencial dali, sem assumir nenhuma outra configuração. O lugar que imprime essa linha é env.py:192 (a função check_credentials() está definida em env.py:184), enquanto quem realmente lê esses dois arquivos é env.py:56-75 e :109-111; registrado na issue #13. Vale o código: ele lê. A ordem completa de busca e as 9 chaves estão na referência de configuração.
O que a configuração interativa pergunta¶
== 配置 flower ========================================
<为什么要配这一行>
凭证会存到 /Users/you/.config/flower/.env(只你可读)。装一次,处处生效。
1. 你的 API key 或网关 token (Anthropic 官方的 sk-ant-… 或第三方网关签发的)
>
2. 网关地址 (直接回车 = Anthropic 官方;第三方网关填它的 BASE_URL)
>
3. 模型名 (直接回车 = 默认;网关有自己的模型名就填,如 claude-opus-5[1m])
>
+ 存好了:/Users/you/.config/flower/.env
- A pergunta 1 é obrigatória. Deixar em branco imprime em vermelho
没给 token,取消。e a configuração é abandonada. - As perguntas 2 e 3 podem ficar em branco.
- Quando o stdin não é terminal, todo o fluxo é pulado, sem bloquear.
As chaves que ele grava¶
| O que você digitou | Chave gravada |
|---|---|
Token começando com sk-ant- | ANTHROPIC_API_KEY |
| Outros tokens | ANTHROPIC_AUTH_TOKEN |
| Endereço de gateway não vazio | ANTHROPIC_BASE_URL |
| Nome de modelo não vazio | ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL e ANTHROPIC_DEFAULT_SONNET_MODEL, os três de uma vez |
O caminho do arquivo é ${XDG_CONFIG_HOME:-~/.config}/flower/.env, e o diretório pai é criado automaticamente. A escrita é sobrescrita integral, chaves com valor vazio são puladas, e ao terminar faz chmod 0600 e carrega na hora — não precisa reabrir o shell. A primeira linha é sempre um comentário lembrando de não versionar o arquivo.
Segunda barreira: a credencial funciona?¶
Com a configuração completa, imprime uma linha - 验一下凭证… e então dispara uma chamada real à API.
Detalhes da sonda: POST {BASE_URL}/v1/messages, max_tokens=16, timeout padrão de 20 segundos, via urllib da stdlib, sem trazer dependências. O modelo é escolhido na ordem ANTHROPIC_DEFAULT_HAIKU_MODEL → ANTHROPIC_MODEL → claude-3-5-haiku-20241022. Havendo ANTHROPIC_API_KEY, usa o header x-api-key; caso contrário, authorization: Bearer <ANTHROPIC_AUTH_TOKEN>.
max_tokens é 16 de propósito, e não 1: em testes, modelos com cadeia de pensamento forçada não cabem nem o pensamento, e o servidor demora até 30 segundos para responder; com 16, leva só 3.6 segundos.
A conclusão da sonda é tratada em três classes, e a diferença importa:
| Conclusão | Condição de disparo | O que o flower faz |
|---|---|---|
auth | HTTP 401 / 403, ou simplesmente nenhuma credencial | Imprime ! 凭证被拒:<响应体前 160 字>, inicia a reconfiguração interativa e valida de novo ao fim. Em modo não interativo, código de saída 1 |
config | HTTP 404, ou 400 e o corpo da resposta dizendo explicitamente que não foi encontrado / não existe (not_found, not found, does not exist, unknown model, no such model, invalid model) | Imprime ! 网关地址或模型名不对:<…>, idem acima |
net | Sem conexão / timeout / falha de DNS / falha de TLS / 5xx | Imprime (探针没打通:<前 80 字> —— 当作网络问题,照常开跑), não pede reconfiguração e começa a rodar |
ok | Menor que 400, ou qualquer caso indeterminado, tudo é liberado | Continua em silêncio |
O critério de config foi apertado: em JSONs de erro no estilo da Anthropic, a palavra model aparece quase sempre, e usá-la como "nome de modelo errado" transformaria um 400 transitório em erro de configuração e forçaria uma reconfiguração — só vale quando a resposta diz explicitamente "não encontrado / não existe" (env.py:176-182).
O net é intencional: uma oscilação de rede não deve forçar você a redigitar o token, e o próprio flower tem mecanismo de suspender e reconectar em queda de rede. Ao ver "探针没打通", ignore e siga.
A chance de reconfiguração é dada no máximo uma vez. Se falhar de novo na segunda, sai.
A sonda só é disparada em terminal interativo. ensure_credentials() retorna direto, sem essa chamada à API, se qualquer uma destas condições valer (cli.py:1413): quem chamou passou probe=False, FLOWER_NO_PROBE está setado, ou o stdin não é terminal (pipe / CI / testes offline). O motivo é que, sem interatividade, descobrir o problema não permite consertá-lo, e o único efeito seria "falhar mais cedo" — e falhar mais cedo, em caso de falso positivo, é pior do que não sondar. Se a credencial for realmente ruim, a run vai explodir sozinha, e esse caminho é capturado pela reconfiguração automática depois de uma falha.
Reconfiguração automática depois de uma falha¶
Quando o workflow falha, o flower pega a mensagem de erro do step que falhou e a compara com uma regex (401, invalid api key, authentication, unauthorized, 无效…key/token/密钥). Se casar e o stdin for um terminal, ele imprime na hora ! 看起来是凭证不对:<前 120 字> e inicia a configuração interativa; ao terminar, imprime:
E depois sai com código de saída 1 de qualquer forma. O caminho do once não tem essa parte.
Códigos de saída¶
| Código | Quando |
|---|---|
0 | Terminou normalmente |
1 | Todas as saídas deliberadas. A mensagem vai para o stderr, sem traceback. Lista abaixo |
2 | Erro de argumento do argparse: flag desconhecida, argumento posicional faltando, -p com valor fora das choices |
130 | Dois Ctrl+C seguidos no meio da run. É um KeyboardInterrupt não capturado, com traceback de Python |
| Morto por sinal | SIGHUP / SIGTERM: grava primeiro o step em voo no manifest e depois segue a ação padrão |
Todas as mensagens do código de saída 1:
| Mensagem | Quando |
|---|---|
已取消 | Ctrl-C ou Ctrl-D no prompt de largada |
诉求是空的。直接 `flower` 然后按提示输入,或者 flower "帮我做一个 X"。 | Diretório novo + Enter direto |
缺少凭证:需要 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN。… (4 linhas no total) | Não interativo + sem credencial |
凭证被拒,且无法交互配置。跑 `flower setup` 重配。 | Não interativo + sonda concluiu auth |
网关地址或模型名不对,且无法交互配置。跑 `flower setup` 重配。 | Não interativo + sonda concluiu config |
--isolate 要求 <路径> 是 git 仓库(每个 subagent 要分一份 worktree)。先 git init,或者去掉 --isolate。 | --isolate usado em diretório não git |
要给一句诉求,例如 flower '帮我做一个 X' | Pedido vazio e diretório sem wake |
在步骤 '<步骤名>' 中止 | Algum step do workflow falhou e a política é parar |
需要 模块:属性 形式,例如 flows:main | flower run flows, faltou os dois-pontos |
找不到 <路径>(当前目录 <cwd>)。给的是文件路径就要能对上;要按模块名导入就别带 .py | flower run missing.py:main |
导入 '<模块>' 失败:<原始消息> | Falha no import do módulo alvo |
'<模块>' 里没有 '<属性>' | O atributo não existe no módulo |
Ao terminar (caminhos go / run), imprime uma última linha:
Esse valor conta apenas o gasto deste processo, sem incluir a run anterior — ainda que o próprio arquivo de manifest acumule entre processos.
O que ele cria no projeto¶
Duas árvores: <run_dir>/ (padrão ./runs/, relativo ao CWD) guarda a contabilidade e as sessions; <workspace>/.flower/ guarda o workbench.
runs/¶
| Caminho | O que guarda |
|---|---|
runs/sessions.db | SQLite, transcript completo. É a base material que permite a continuity |
runs/manifest.json | Run manifest. Array JSON, acumulado entre processos; todos os números das páginas de caso podem ser reconferidos aqui |
runs/lineage.json | Lineage: {"workspace": …, "woke": N, "steps": {"步骤名": "session_id"}}. Escrito com substituição atômica |
runs/aside/ | Runtime independente das perguntas ao oracle, com seu próprio sessions.db e manifest.json. Custo e lineage não se misturam ao manifest principal |
runs/workbench/ | Só aparece quando -W foi usado e o workflow não traz workbench próprio (caminhos run / once) |
Campos de cada registro do manifest.json:
step session_id ok cost_usd num_turns text error started_at ended_at
attempts errors[] resumed retired[] context duration_s run
run é a marca deste processo, no formato YYYYmmdd-HHMMSS-<6 dígitos hex>. A política de gravação é anexar sem sobrescrever: antes de cada escrita, relê o arquivo e deduplica por run — as linhas deste processo são substituídas pelas mais recentes, e as linhas de outros processos ficam como estão.
Os nomes de step têm quatro formatos:
| Formato | Quando |
|---|---|
<步骤名> | Primeira tentativa |
<步骤名>#retry<N> | Retentativa comum |
<步骤名>#round<N> | O verdict não passou e o step foi devolvido para continuar |
<步骤名>·判定#<N> | O step do judge |
Quando morre por sinal, o step em voo também é gravado, com o campo error valendo killed-by-signal.
Vários flower em paralelo no mesmo diretório: o manifest.json é seguro (relê e mescla por run), mas o lineage.json é sobrescrito por inteiro, e dois processos vão apagar a lineage um do outro para steps de mesmo nome. Para rodar em paralelo, use -r diferentes.
O lineage.json guarda o caminho absoluto do workspace. Se não bater, é tratado como inexistente e o sistema volta silenciosamente para uma session nova, sem erro — depois que o diretório é copiado para outro lugar, o session_id antigo não seria encontrado mesmo.
.flower/¶
| Caminho | O que guarda |
|---|---|
.flower/scripts/ | Scripts que serão rodados uma segunda vez. A primeira linha traz # desc: uma frase, e essa frase aparece no índice |
.flower/artifacts/ | Saídas longas, acima de 2000 caracteres: relatórios, dados, logs. Na conversa aparece só o caminho |
.flower/notes/ | Registros de decisão entre steps |
.flower/spill/ | Spill: resultados de ferramenta acima de 4000 caracteres caem aqui, e no contexto fica só uma linha de ponteiro mais os 400 primeiros caracteres. O nome do arquivo são os 16 primeiros dígitos do sha256 do conteúdo mais .txt |
.flower/INDEX.md | Índice dos diretórios acima, injetado no system prompt do coordinator (subagents não o herdam) |
O caminho do go gera sempre estes arquivos sob notes/:
| Arquivo | Conteúdo |
|---|---|
notes/需求.md | O brief congelado, em quatro seções: objetivo / critérios de aceitação / fronteiras / incógnitas e suposições |
notes/目标.md | O objetivo congelado, em duas seções: objetivo / lista de verdict |
notes/问答记录.md | Registro em append de todas as perguntas e respostas (com estado), incluindo o que você falou por iniciativa própria. Não entra no contexto, é só arquivo |
notes/交接-<步骤名>.md | O handoff document escrito no handoff; a geração anterior é recolhida em notes/archive/交接/<步骤名>-<时间戳>.md |
notes/archive/<YYYYmmdd-HHMMSS>/ | O lineage.json, 需求.md e 目标.md arquivados por --new ou /new. É movimentação, não remoção |
Com --isolate, o workbench é movido para fora do repositório: <workspace>.parent/.flower-<nome do workspace>/. O worktree é a cópia privada de cada agent, e o workbench é a camada compartilhada entre agents; o que é compartilhado não pode ficar dentro de uma cerca privada. Nesse caso, o caminho do workbench passado ao modelo é absoluto.