跳转至

命令行参考

flower 装完只有一个可执行文件,4 个子命令,23 个开关。这一页把它们全列出来:每一个开关的类型、 默认值、精确语义,加上运行途中怎么插话、第一次跑会问什么、退出码是几、它在你的目录里放了哪些文件。 读完这一页不需要再打开源码。

源码:flower/cli.py

子命令 干什么 位置参数 专属开关
go 一条龙:问清需求 → 设目标 → 派人干活 → 每轮判定。不写子命令时的默认 ask(可选) 11 个
run 跑一个你自己写的流程 target(必需) 0 个
once 跑一次单 agent,不套流程、不做判定 prompt(必需) 6 个
setup 配置凭证,写到 ~/.config/flower/.env 0 个

开关总数 23 = 5 个全局 + 11 个 go 专属 + 6 个 once 专属 + -h/--helprunsetup 没有自己的开关。


调用形式

flower 的所有 argv 先过一遍 _with_default_cmd() 补默认子命令,再交给 argparse (cli.py:1437-1439)。这就是为什么 flower "帮我做一个 X" 能跑 —— 它被改写成了 flower go "帮我做一个 X"

补默认子命令的规则(cli.py:940-976):

  1. 全局开关的集合从主 parser 自身派生,不是硬编码的列表。nargs == 0 的算纯开关, 其余算带值开关。
  2. 从左往右扫,跳过全局开关。带值的连值一起跳,--workspace=/tmp 这种 = 写法也认。
  3. 停在第一个不是全局开关的 token。它若是 gorunonce 之一就原样交给 argparse; 否则在它前面插一个 go,于是它变成 go 的诉求正文。
  4. 扫完都没遇到位置参数(空 argv,或只有全局开关)→ 末尾补 go,进交互输入。
  5. 例外:argv 里含 -h--help 时原样返回,交给 argparse 打帮助。

判定用的常量是 _CMDS = ("go", "run", "once")(cli.py:937)—— setup 不在里面, 后果见setup

实际的改写结果

你敲的 实际解析成 效果
flower ["go"] 交互问"要做什么?"
flower -v ["-v", "go"] 同上,带 verbose
flower "帮我做一个 X" ["go", "帮我做一个 X"] 直接开跑
flower -w /tmp "做 X" ["-w", "/tmp", "go", "做 X"] 全局开关可以写在前面
flower --workspace=/tmp "做 X" ["--workspace=/tmp", "go", "做 X"] = 形式也认
flower "做 X" --timeout 0 ["go", "做 X", "--timeout", "0"] 子命令开关可以写在诉求后面
flower --timeout 0 "做 X" ["go", "--timeout", "0", "做 X"] 也可以写在前面
flower --new ["go", "--new"] 只给开关不给诉求 → 交互输入
flower once "hi" ["once", "hi"] 原样
flower run flows:main ["run", "flows:main"] 原样
flower run ["run"] argparse 报缺 target,不会当成诉求
flower go run ["go", "run"] 显式消歧:诉求正文就是 run
flower setup ["go", "setup"] 跑的是 go,诉求变成字符串 setup,见 setup
flower --help 原样 argparse 打帮助

runonce 这两个词不能直接当诉求正文,这是有意保留的歧义(cli.py:949-950)。 要让它们当诉求就写 flower go run

六种能用的写法

flower                                    # 1. 裸跑:交互问“要做什么?”或“接着上次?”
flower "帮我做一个 X"                       # 2. 位置参数给诉求
echo "帮我做一个 X" | flower --timeout 0    # 3. 管道喂 stdin
flower once "读一眼这个仓库"                 # 4. 单 agent
flower run flows.py:main                  # 5. 跑自定义流程
flower go setup                           # 6. 显式 go,把 setup 当诉求正文

模块形式 python -m flower.cli 等价于 flower(cli.py:1451-1452)。 容器包装器 docker/flowerbox 的参数与 flower 完全一致。

管道喂 stdin

ask_for_prompt()sys.stdin.isatty() 为假时不打印提示头,直接 input("> ") 读一行 (cli.py:993-1001)。所以 echo "..." | flower 能工作。

但随后会打一行警告,并且 stdin 线程立刻读到 EOF、退出:

! 标准输入不是终端,没人能回答提问。想让它自己判断就加 --timeout 0

管道跑就该配 --timeout 0:提问不再假装等 30 分钟,立刻落空,agent 自己判断并把假设写进 需求确认书的"未知与假设"那一段。


子命令

go

help 文本:一键跑:问清需求 → 派人干活(不写子命令时的默认)(cli.py:1275-1307)。

位置参数 ask,nargs="?" —— 不给就进交互输入。这是最常用的入口,flower "做 X" 走的就是它。

它做的事(cli.py:1190-1221):

  1. ensure_credentials() —— 检查凭证,并真打一次 API 探针,见首次运行的配置流程
  2. 唤醒探测:只读看一眼这个目录用过没有,一个字节都不写。
  3. 没给 ask 就打提示符问一句;输入 /new 等价于 --new,然后再问一次诉求。
  4. 接续的话打一行唤醒横幅。
  5. 造出三步的流程:确认需求设定目标干活, 每轮干活后面跟一个 干活·判定#N--clarify-only 只留第一步。
  6. 开跑。

唤醒横幅长这样(路径里的家目录会被换成 ~):

<- 在 ~/proj 接上上次  需求已确认 · 目标 7 条 · 干活上下文 71.4K · 第 3 次唤醒

需求已确认 恒有;目标 N 条 只在有判定清单时出现;干活上下文 X 需要能从 sessions.db 里查到那条会话的最后一轮上下文,查不到就不显示。

-W-Tgo 路径上被静默覆盖

这两个全局开关在 go 上写了也没用,不报错、不提示:

  • -W/--workbench:go 造出来的流程总是自带工作台, 而代码取的是 getattr(wf, "workbench", None) or args.workbench(cli.py:1038)—— 流程自带的那份永远优先。所以工作台恒为 <workspace>/.flower/ (--isolate 时是 <workspace>.parent/.flower-<名字>/),-W 改不了它。
  • -T/--trim:go 走的是 _drive(wf, args, trim=not args.no_trim)(cli.py:1221), 直接用 --no-trim 的反值,根本不看 args.trim。也就是说 go 路径上 裁剪默认就是开的,要关只能用 --no-trim

这两个开关只在 run(流程没自带工作台时)和 once 上生效。

go 的 11 个开关

开关 类型 默认 说明
--asks N int -1 提问额度。-1 或任何负数 = 不限;0 = 不许提问,第一次提问就 over_budget;N = 硬额度。超额时工具直接回绝,不阻塞运行
--rounds N int 3 干活的总轮数上限,不是额外轮数。每轮结束由独立的判定者判"做完了没有",没达成就打回去续跑同一条会话
--no-goal 开关 False 关掉目标看守:不生成 目标.md、不做判定,干活跑完就算完
--judge-can-run 开关 False 让判定者能跑命令。判定更硬,代价是它也就能改动工作区了
--timeout 秒 float 1800.0 等人回答多久。0 或负数 = 全自动,所有提问立刻落空,不假装等。语义见超时
--isolate 开关 False 每个 subagent 分一份 git worktree,即隔离要求 workspace 是 git 仓库,否则退出码 1。同时把工作台移到仓库外
--window N int 无(按模型名推) 模型上下文窗口。不给时:模型名含 1m 或不含 haiku → 1,000,000;含 haiku → 200,000。到 窗口 − 50000 就写交接书换代
--no-handoff 开关 False 关掉换代,退回 SDK 自带的压缩
--new 开关 False 这次别接上次。把上一段的 lineage.json + 需求.md + 目标.md 移动(不是删除)进 notes/archive/<YYYYmmdd-HHMMSS>/,再从头开始
--clarify-only 开关 False 只做前置确认,不往下干活 —— 流程里只剩 确认需求 一步
--no-trim 开关 False 关掉裁剪。go 路径上裁剪默认,这是唯一的关闭方式

取值上的边角,都不报错、都不提示:

  • --rounds 0--rounds 1 等价 —— 内部是 retries = max(0, rounds - 1),都跑 1 轮。
  • --asks 任何负数都等于不限,不只是 -1
  • --timeout 任何负数都等于 0,即全自动。
  • --window 0静默忽略(0 是 falsy,压根不传下去),退回按模型名推算的默认值。 负数会被传下去,然后被兜到 10000
  • --clarify-only 在已经确认过的目录上是空操作 —— 确认需求 这一步看到齐全的 需求.md 就跳过,而流程里只有这一步,于是什么都不会发生(除了唤醒次数 +1)。要重新确认得配 --new
  • go--help 结尾写着"全局开关(-v/-w/-r/-T)见 flower --help",这行漏了 -W

run

help 文本:运行一个 workflow(cli.py:1309-1312)。

位置参数 target,写法是 模块:属性。两种形式都支持(cli.py:1010-1031):

flower run mypkg.flows:build     # 按模块名 import
flower run flows.py:build        # 文件路径;会把父目录塞进 sys.path 再按文件名 import

拿到的属性如果可调用就调一次,拿返回值当流程;已经是流程对象就直接用。

run 没有任何自己的开关,只有 5 个全局开关。所以 --window--no-handoff 这些在这条路上 一律取默认值(代码用 getattr 兜底,cli.py:1041-1043)。要调它们,把参数写进你自己的流程里。

once

help 文本:跑一次单 agent(cli.py:1314-1324)。位置参数 prompt 必需。

它构造一个 AgentSpec(name="ad-hoc", …) 直接跑,不走 _drive。因此 once 上没有:

  • Ctrl-C 打断并说话(按下去就是普通的 KeyboardInterrupt)
  • stdin 应答线程、常驻底部的输入提示符
  • 旁路问答
  • SIGHUP / SIGTERM 抢救落账
  • 收尾那行 总花费 … · 清单 …
  • 凭证失败后的自动重配引导

运行清单里这一步的名字固定是 ad-hoc

开关 类型 默认 说明
-i, --instructions str 领域指令,叠加在 Claude Code 原生系统提示词之后,不替换它
-t, --tools str Read,Glob,Grep 逗号分隔的工具白名单。不给时就是这三个只读工具
-p, --permission-mode str default 取值只能是 defaultacceptEditsplanbypassPermissions 之一,给别的值 argparse 报错退出码 2
-b, --budget float 无上限 美元预算上限,超了就停
--resume SESSION_ID str 续跑某条已有会话
--fork 开关 False 分叉而不是续跑,配合 --resume

once 显示的用时和累计花费永远是 0

once 每收到一个事件就新建一个渲染器实例(cli.py:688-690cli.py:1239), 而计时起点和累计花费是存在实例上的(cli.py:500-501)。于是:

  • 收尾那行的 用时 恒为 0:00
  • 状态行里的 累计 $0.00 恒为 0,上下文 也从不累积

单步的真实花费要去 runs/manifest.json 里看 cost_usd 字段。gorun 路径持有 同一个渲染器实例,没有这个问题。

setup

help 文本:配置凭证(API key / 网关 / 模型),写到 ~/.config/flower/.env(cli.py:1326-1328)。 没有任何开关。

它做的事:读一遍 .env → 判断配过没有 → 起交互配置流程,reason重新配置。还没配过凭证。。屏幕内容见首次运行的配置流程

flower setup 现在跑不到这个子命令

默认子命令的判定常量 _CMDS = ("go", "run", "once")(cli.py:937)漏了 "setup", 但 parser 里确实注册了 setup(cli.py:1326)。于是 flower setup 被改写成 flower go setup —— 跑的是完整的 go 流程,诉求正文是字符串 setup:先验凭证, 再问需求,然后真的开始派人干活。加全局开关也一样,flower -v setup["-v", "go", "setup"]

没有任何一种 argv 能到达 setup 子命令。

要配凭证,现在只能靠这两条路,两条都能走到同一个交互界面:

  • 直接跑 flower "随便一句诉求",没配过凭证时它会先问;
  • 或者手写 ~/.config/flower/.env,键名见写出来的键

受牵连的还有几处文案:凭证被拒时打的 跑 `flower setup` 重配。.env 首行的注释 由 `flower setup` 写,指向的都是这个跑不到的命令。


全局开关

5 个全局开关同时挂在主 parser 和每一个子命令上(cli.py:1250-1266)。子命令上的那份用了 argparse.SUPPRESS,不给就不写属性,所以写在子命令前面还是后面都行,不会互相覆盖。 副作用是子命令的 --help 里看不到它们 —— 要看得跑 flower --help

开关 类型 默认 说明
-w, --workspace str . agent 的工作目录。会被 resolve() 成绝对路径并 mkdir -p工作台 .flower/ 建在这里面
-r, --run-dir str runs 会话存储和运行清单的目录。相对当前 CWD,不是相对 workspace
-v, --verbose 开关 False 多打点东西,见下
-W, --workbench 开关 False 启用工作台。go 上无效,只对 run(流程没自带工作台时)和 once 生效,此时工作台落在 <run_dir>/workbench/
-T, --trim 开关 False resume 时把旧的大工具结果换成文件指针,即裁剪go 上无效,那条路用 --no-trim 反向控制
-h, --help 开关 每个 parser 都有。argv 里出现它时跳过默认子命令改写,直接打帮助

-r/--run-dir 相对 CWD 这一条会咬人:flower -w /other/proj "做 X" 会把 runs/ 建在你敲命令的 目录,而 .flower/ 建在 /other/proj/ 下 —— 两份状态分家。要它们在一起就显式给 -r /other/proj/runs

-v 的 help 写的是"显示思考与工具结果",但主线程的思考默认就显示-v 实际额外打开的是:

  • subagent 的正文(默认不显示,只显示它的工具调用)
  • 正常的工具结果(默认只显示报错的那些)
  • prompt 事件
  • 启动前打一遍当前生效的凭证配置,token 打码只留前 4 位

最后这条走的是裸 print(),不经过输出消毒、不折行、不受终端写锁保护,并行跑多个 flower 时这几行可能被撕开。


运行中怎么和它说话

运行跑起来之后,终端一直在读你的输入。不需要等它提问,也不需要按什么键进入输入模式 —— 最后一行永远是可以打字的那一行。

常驻在最下面的输入提示符

有一条 daemon 线程 flower-stdin 全程读 stdin(cli.py:764-934),用 select 每 0.2 秒轮询一次, 不是阻塞读(这样停止信号叫得醒它;Windows 这类不支持 select 的流退化成阻塞读)。

它一直在读,不只在有提问的时候读。 理由是:如果只在提问时读,你在干活那几小时里敲的东西会留在 终端缓冲里,下一次提问时被当成答案吃掉 —— 你还没看见问题,问题就被回答了。

显示上,_say() 是唯一的输出口,每次输出前先擦掉提示符、输出完再重画(cli.py:309-315), 所以提示符不会被事件输出冲到屏幕上方。重画时连你打了一半还没回车的那几个字一起画回来 —— 它们存在 _PROMPT["buf"] 里(cli.py:183-192)。不这么做的话,内容其实没丢(还在终端行缓冲里, 回车照样发出去),但你看不见它,于是不敢确定、重打一遍。

提示符有两种文案,随"有没有待答提问"切换:

状态 屏幕最后一行
有待答提问 你的回答 (回车=跳过,让它自己判断) >
无待答提问 (直接说 = 加需求,下个检查点送达;? 开头 = 顺便问一句,不打扰它干活) >

逐字符输入模式与键位

要把"打了一半的字"画回来,flower 就得自己接管输入。stdin 是终端且能 import termios 时, 起 flower-stdin 线程之前先把终端设成 cbreak(cli.py:793-807)—— 用 cbreak 不用 raw,是为了让 Ctrl+C 仍然产生 SIGINT,Ctrl-C 那套才还在。 必须在起线程之前设:放进线程里有真实竞态,线程还没抢到 CPU 那一瞬间敲的字会被行模式吃掉, 表现为"输入丢了"(实测三次里稳定复现一次,cli.py:928-934)。

设不上就退回原来的整行 readline()(非终端、termios 不可用、tcgetattr 失败), 两条路都能用,只是行模式下没有下面这些键位(cli.py:883-899)。

编辑逻辑在 LineEditor 里(cli.py:320-414),纯状态机、不碰终端:

作用
可打印字符 插在光标处。UTF-8 用增量解码器攒够一个字符才入 buffer
Backspace / Ctrl+H 删光标前一个字符。行模式下终端按字节删,一个汉字要按三下还删出乱码,这里不会
← / → 真的移光标。整段转义序列被吃掉,不会把 [A 之类插进输入
Home / End(或 [1~ / [4~) 跳行首 / 行尾
Delete([3~) 向后删一个字符
Ctrl+A / Ctrl+E 行首 / 行尾
Ctrl+U 清空整行
Ctrl+D buffer 为空时才是 EOF;有内容时忽略
↑ / ↓ 什么都不做。没有历史记录,动了反而让人以为丢了东西(cli.py:335)
其它控制字符 忽略

回车把 buffer 交出去并清空,同时在屏幕上换一行 —— 你说过的话留在上面(cli.py:811-827)。

你敲的东西去哪了

你输入 有待答提问时 无待答提问时
空行(直接回车) 跳过这个问题,让它自己判断 什么都不做
? 开头 旁路问答,见下 同左
纯数字,且在选项范围内 换成对应的那个选项再作答 当普通文本处理
其它文本 作为答案送给提问的 agent 进收件箱,当作追加需求
EOF(Ctrl-D 或管道关闭) 拒答这个问题,摘掉提示符,线程退出 摘掉提示符,线程退出

进收件箱时会打一行回执:

+ 收到 (它下次查收件箱时会看到;已追加进确认书)

没有需求确认书可落盘时,后半句变成 没有确认书可落盘 —— 它可能活不过下一个步骤。收件箱不打断正在干活的执行者, 它下次主动查收件箱才会拿走。同一句话还会被追加进 notes/需求.md,不落盘就活不过步骤边界 —— 下一步是新会话,只读冻结件。

? 开头 = 旁路问答

? 开头的一行不会送给正在跑的 agent,而是交给旁路顾问:

? 现在到哪一步了

它起一条独立的 Runtime,run_dir<run_dir>/aside/,所以它的花费和会话血缘不会混进主 manifest.json。角色是只读的,工具只有 ReadGlobGrep,最多 12 轮, 花费上限 $0.5。它看到的上下文是最近 60 条事件(thinkingprompt 事件不进这个窗口), 每条截断到 200 字符,外加工作台的路径说明。

它是并发跑的,正在跑的运行一秒都不用等。回答长这样:

# 旁路
  <回答正文>
  ($0.0123,没有打扰正在跑的运行)

失败时打一行红字 # 旁路问答失败:<类型>: <消息>,不影响主流程。 退出时最多等旁路收尾 120 秒,等之前先打一行 (等 N 条旁路问答收尾…)

它说的话不会进入那次运行的上下文 —— 问了不影响运行,答完即弃。

全角 不触发旁路问答 —— 中文输入法用户会踩

判断旁路问答的那行代码是(cli.py:907):

if raw.startswith("?") or raw.startswith("?"):

两个字符都是半角 ASCII ?(0x3f)—— 逐字节验过。从写法看意图显然是想同时接受半角 ? 和中文输入法打出的全角 (U+FF1F),但实际写成了同一个字符。

后果:用全角 开头的一行不会被当成旁路提问,而是被当成"追加需求"静默送进收件箱, 进而被追加进 notes/需求.md。你看到的回执是 + 收到,不是 # 旁路

要问旁路,必须用半角 ? —— 打之前先把输入法切到英文,或者只把第一个字符打成半角。

屏幕上都是什么

图标一律是 ASCII,不是 emoji(cli.py:51-69)。原因写在代码注释里:emoji 和框线、几何、箭头 字符会触发终端字形回退,曾经导致两次终端崩溃。

图标 含义 图标 含义
= 步骤分隔线 + 完成 / 已答 / 收到
~ 思考、重试 x 失败 / 报错
> 派人 # 换代、旁路、任务
* 工具调用 - 状态行、列表项
? 提问 <- 接上次、交接落点
! 警告 / 打断 . 已跳过
\| subagent 的缩进竖线

旧文档里的 在真实终端里不存在

早期文档用 表示提问、用 表示唤醒行。代码里从来不是这两个字符 —— 提问的图标是半角 ?,唤醒和交接落点的图标是两个 ASCII 字符 <-

所以真实终端打出来的是:

  ? 这个工具要做成 CLI 还是库?
     1) CLI
     2) 库
     (还能问 5 次)
<- 在 ~/proj 接上上次  需求已确认 · 目标 7 条 · 第 3 次唤醒

不是 ❓ 这个工具……,也不是 ↩ 在 ~/proj 接上上次。照旧文档去 grep 日志会一无所获。

提问的五种状态,屏幕上分别是:

状态 屏幕输出
问出来了 ? <问题>,后面逐条列 1) 选项一,有额度时再加 (还能问 N 次)
答了 + <答案>
超时 ! 无人应答 —— 它会自己判断,把假设记进「未知与假设」
额度用完 ! 提问额度用完
你跳过了 . 已跳过

--asks 取不限(默认)时,最后那行"还能问 N 次"不显示。

换代写完交接书时是一整块:

# 上下文 950.0K/1000K —— 写交接准备换代
  - 现在在做    …
  - 已定的事    …
  - 走不通的    …
  - 下一步      …
<- 交接写在 ~/proj/.flower/notes/交接-干活.md
<- 新会话接手,上下文从 950.0K 重新开始

交接书降级时会多插一行红字 交接没写成,用了降级版本 —— 接手的人会自己去现场看

输出还做了两件你看不见的事:所有输出行先过一遍消毒,只放行 flower 自己的 SGR 颜色码, 模型或工具吐出来的清屏、移光标序列被整段吞掉;宽度取 max(40, min(终端列数, 110)), 所以宽终端上不会铺满整行,这是有意的。

起跑时的提示符

裸跑 flower(不带诉求)时先问一句。两种文案:

要做什么? 一句话就够,回车开始(Ctrl-C 退出)
> 
接着上次? 直接回车 = 接着做;也可以说点新的;/new = 重开一件事(Ctrl-C 退出)
> 

第二种只在这个目录已经跑过、需求.md 四段齐全时出现。

这个提示符读的是 input(),不经过 shell 解析。中文引号、空格、感叹号都能直接打 —— 这是它存在的全部理由。zsh 遇到中文右引号会进 dquote> 续行,看起来像卡住,其实一次都没启动。

  • 空输入 + 首次 → 退出,打 诉求是空的。直接 `flower` 然后按提示输入,或者 flower "帮我做一个 X"。
  • 空输入 + 唤醒 → 合法,就是"接着做"
  • 输入 /new → 等价 --new,归档上一段之后再问一次诉求
  • Ctrl-C / Ctrl-D → 退出,打 已取消

超时

--timeout 是 float,单位秒,默认 1800.0。三种取值:

行为
> 0 等这么多秒。超时则这次提问结算成 timeout,agent 自己判断
0 或负数 全自动。提问不进等待队列、不发 asked 事件、屏幕上不出现,立刻结算成 timeout
永远等 命令行做不到。内部支持"永远等",但 --timeout 是 float 且有默认值,没有任何写法能产生它。上限就是给一个很大的秒数

--timeout 0--timeout -1 完全等价。管道跑、CI 里跑、无人值守跑,用的都是这个。

提问没得到回答时,喂回给模型的工具结果是固定文案,四种:

结果 喂回模型的文案
额度用完 提问额度已用完。不要再问了 —— 把剩下的不确定项写进「未知与假设」那一段,按你自己的判断继续。
超时 无人应答。按你自己的判断继续,并把这个问题和你采用的假设写进「未知与假设」那一段。不要重复提问,也不要停在这里。
你跳过了 对方跳过了这个问题。按你自己的判断继续,并把假设写进「未知与假设」。
问题是空的 问题是空的。把问题写清楚再问。

Ctrl-C

两个位置的 Ctrl-C 语义完全不同。

在起跑提示符 > 上按 —— 直接退出程序,打 已取消

运行途中按 —— 打断当前这一轮,并给你一次说话的机会:

! 已打断这一轮。正在跑的 subagent 会丢掉半成品。
  要说什么?(直接回车 = 什么都不说,接着跑;再按一次 Ctrl+C = 退出)
> 

在这里直接回车就是只打断不说话,接着跑。当时有待答提问的话会多打一行 (有 N 个提问还等着,打断不影响它们)

再按一次 Ctrl+C 就是真退出,而且是未捕获的 KeyboardInterrupt —— 屏幕上会有一段 Python traceback,不是干净退出。

打断是协作式的:在消息边界干净断开,不硬取消任务。它不算一次失败尝试,不消耗重试次数。 续跑时会附一段说明,告诉模型"在飞的工具调用返回 interrupted 是打断的正常副作用,不是环境故障"。

这套自定义的 Ctrl-C 只在 sys.stdin.isatty() 时装(cli.py:1097)。管道里跑时保持 Python 默认行为, 也就是第一次就退出。once 路径不走这里,所以 once 上的 Ctrl-C 也是第一次就退出。

SIGHUP / SIGTERM

gorun 路径给 SIGHUPSIGTERM 都装了处理:先把在飞的那一步也写进 manifest.json 并标成 killed-by-signal,再恢复默认动作、真的走掉。

起因是终端崩溃时内核发 SIGHUP,默认动作直接终止进程,finally 不跑、清单不写 —— 一次运行的账目就丢了。非操作系统主线程或平台不支持时静默跳过。


首次运行的配置流程

gorunonce 三个入口开头都调 ensure_credentials()(cli.py:1392-1428),两道关

第一道:有没有凭证

按优先级找一遍凭证。找不到 ANTHROPIC_API_KEYANTHROPIC_AUTH_TOKEN 就起交互配置; 非交互(stdin 不是终端)时不阻塞,直接打这段然后退出:

缺少凭证:需要 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN。
最省事:跑一次 `flower setup`,把 token 存到 /Users/you/.config/flower/.env(装一次,处处生效)。
或者:在当前目录建 `.env`,或 export 进进程环境。
flower 不读 ~/.claude/settings.json —— 那是可移植性的代价。

这段文案有两处和实现对不上:第二行的 flower setup 目前跑不到(见 setup); 第四行和代码相反 —— flower 确实会把 ~/.claude/settings.jsonsettings.local.jsonenv 块当作最后一级回退,只借其中 9 个凭证键,不接管别的任何设置。打这行字的地方是 env.py:192(函数 check_credentials() 定义在 env.py:184),而真去读那两个文件的是 env.py:56-75:109-111;记在 issue #13以代码为准:它读。 完整的查找优先级和那 9 个键见配置参考

交互配置问什么

== 配置 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
  • 第 1 问必填。留空就打红字 没给 token,取消。 然后放弃配置。
  • 第 2、3 问可以留空。
  • stdin 不是终端时整个流程直接跳过,不阻塞。

写出来的键

你输入的 写成的键
token 以 sk-ant- 开头 ANTHROPIC_API_KEY
其它 token ANTHROPIC_AUTH_TOKEN
网关地址非空 ANTHROPIC_BASE_URL
模型名非空 ANTHROPIC_MODELANTHROPIC_DEFAULT_OPUS_MODELANTHROPIC_DEFAULT_SONNET_MODEL 三个一起写

文件路径是 ${XDG_CONFIG_HOME:-~/.config}/flower/.env,父目录会自动建。写法是整份覆盖, 空值的键跳过,写完 chmod 0600,然后立刻加载生效 —— 不用重开 shell。 首行固定是一句注释,提醒别提交进版本库。

第二道:凭证能不能用

配置齐了之后,打一行 - 验一下凭证…,然后真打一次 API

探针的细节:POST {BASE_URL}/v1/messages,max_tokens=16,默认超时 20 秒,走 stdlib 的 urllib,不引依赖。模型按 ANTHROPIC_DEFAULT_HAIKU_MODELANTHROPIC_MODELclaude-3-5-haiku-20241022 的顺序取。有 ANTHROPIC_API_KEY 就用 x-api-key 头, 否则用 authorization: Bearer <ANTHROPIC_AUTH_TOKEN>

max_tokens 特意设成 16 而不是 1:实测强制思维链的模型连思考都放不下,服务端要挣扎到 30 秒才返回; 设 16 只要 3.6 秒。

探针的结论分三类处理,差别很重要:

结论 触发条件 flower 做什么
auth HTTP 401 / 403,或者根本没凭证 ! 凭证被拒:<响应体前 160 字>,起交互重配,配完再验一次。非交互则退出码 1
config HTTP 404,或者 400 响应体里明说找不到 / 不存在(not_foundnot founddoes not existunknown modelno such modelinvalid model 之一) ! 网关地址或模型名不对:<…>,同上
net 连不上 / 超时 / DNS 失败 / TLS 失败 / 5xx (探针没打通:<前 80 字> —— 当作网络问题,照常开跑),不让你重配,直接开跑
ok 小于 400,或者判不准的一律放行 静默继续

config 的判据是收紧过的:Anthropic 风格的错误 JSON 里几乎必然出现 model 这个词, 拿它当"模型名不对"会把一次瞬时 400 误判成配置错误,然后逼人重配 —— 必须明说"找不到 / 不存在" 才算(env.py:176-182)。

net 这条是有意的:网络抖一下不该逼你重输 token,而且 flower 本身有断网挂起重连的机制。 看到"探针没打通"不用管,继续跑就是。

重配机会最多给一次。第二次还失败就退出。

探针只在交互式终端里打。 ensure_credentials() 满足下面任意一条就直接返回,不打这一次 API (cli.py:1413):调用方传了 probe=False、设了 FLOWER_NO_PROBE、 或者 stdin 不是终端(管道 / CI / 离线测试)。理由是非交互下探出问题也修不了,唯一效果是 "提前失败" —— 而提前失败在误判时比不探更糟。真有坏凭证,跑起来自然会炸,那条路由 跑挂了之后的自动重配接住。

跑挂了之后的自动重配

流程失败时,flower 会拿失败那一步的错误信息去匹配一条正则(401、invalid api keyauthenticationunauthorized无效…key/token/密钥)。命中且 stdin 是终端时, 当场打 ! 看起来是凭证不对:<前 120 字> 并起交互配置,配好了再打:

配好了。再跑一次刚才的命令 —— 同一目录会接着上次。

然后无论如何都以退出码 1 退出。once 路径没有这一段。


退出码

什么时候
0 正常跑完
1 所有主动退出。消息打到 stderr,没有 traceback。清单见下
2 argparse 参数错误:未知开关、缺位置参数、-p 给了 choices 之外的值
130 运行途中连按两次 Ctrl+C。是未捕获的 KeyboardInterrupt,带 Python traceback
被信号杀 SIGHUP / SIGTERM:先把在飞的那一步写进清单,再按默认动作走掉

退出码 1 的全部消息:

消息 什么时候
已取消 在起跑提示符上按 Ctrl-C 或 Ctrl-D
诉求是空的。直接 `flower` 然后按提示输入,或者 flower "帮我做一个 X"。 全新目录 + 直接回车
缺少凭证:需要 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN。…(共 4 行) 非交互 + 没凭证
凭证被拒,且无法交互配置。跑 `flower setup` 重配。 非交互 + 探针判 auth
网关地址或模型名不对,且无法交互配置。跑 `flower setup` 重配。 非交互 + 探针判 config
--isolate 要求 <路径> 是 git 仓库(每个 subagent 要分一份 worktree)。先 git init,或者去掉 --isolate。 --isolate 用在非 git 目录
要给一句诉求,例如 flower '帮我做一个 X' 诉求为空且目录没唤醒
在步骤 '<步骤名>' 中止 流程里某一步失败且策略是停
需要 模块:属性 形式,例如 flows:main flower run flows,漏了冒号
找不到 <路径>(当前目录 <cwd>)。给的是文件路径就要能对上;要按模块名导入就别带 .py flower run missing.py:main
导入 '<模块>' 失败:<原始消息> 目标模块 import 失败
'<模块>' 里没有 '<属性>' 模块里找不到那个属性

跑完(go / run 路径)最后打一行:

总花费 $1.2345 · 清单 /abs/path/runs/manifest.json

这个金额只算本进程的花费,不含上一次运行的 —— 尽管清单文件本身是跨进程累积的。


它在项目里创建了什么

两棵树:<run_dir>/(默认 ./runs/,相对 CWD)装账目和会话;<workspace>/.flower/工作台

runs/

路径 装什么
runs/sessions.db SQLite,全量 transcript。这是接续能接上的物质基础
runs/manifest.json 运行清单。JSON 数组,跨进程累积,案例页里的数字都能在这里复算
runs/lineage.json 血缘:{"workspace": …, "woke": N, "steps": {"步骤名": "session_id"}}。原子替换写入
runs/aside/ 旁路问答的独立 Runtime,自己的 sessions.dbmanifest.json花费和血缘不混进主清单
runs/workbench/ 只在用了 -W 且流程没自带工作台时出现(run / once 路径)

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 是本进程的标记,格式 YYYYmmdd-HHMMSS-<6 位 hex>。落盘策略是追加不覆盖:每次写之前 重读一遍文件,按 run 去重 —— 属于本进程的行换成最新的,别的进程的行原样留着。

步骤名有四种形态:

形态 什么时候
<步骤名> 第一次尝试
<步骤名>#retry<N> 普通重试
<步骤名>#round<N> 判定没过被打回来接着做
<步骤名>·判定#<N> 判定者那一步

被信号杀掉时,在飞的那一步也会被写进去,error 字段是 killed-by-signal

同一个目录并行跑多个 flower:manifest.json 安全(重读 + 按 run 归并), 但 lineage.json 是整份覆盖,两个进程会互相盖掉同名步骤的血缘。要并行就用不同的 -r

lineage.json 里存了 workspace 的绝对路径。对不上就当没有,静默退回新会话,不报错 —— 目录被拷走之后旧的 session_id 本来也查不到。

.flower/

路径 装什么
.flower/scripts/ 要跑第二次的脚本。首行写 # desc: 一句话,这句话会出现在索引里
.flower/artifacts/ 超过 2000 字符的长产出:报告、数据、日志。对话里只出现路径
.flower/notes/ 跨步骤的决策记录
.flower/spill/ 落盘:超过 4000 字符的工具结果落这里,上下文里只留一行指针加开头 400 字符。文件名是内容 sha256 前 16 位加 .txt
.flower/INDEX.md 上面几个目录的索引,注入协调者的系统提示词(subagent 继承不到)

go 路径固定在 notes/ 下生成这些:

文件 内容
notes/需求.md 冻结的需求确认书,四段:目标 / 验收标准 / 边界 / 未知与假设
notes/目标.md 冻结的目标,两段:目标 / 判定清单
notes/问答记录.md 全部提问和回答的追加记录(含状态),也包括你主动说的话。不进上下文,只作留档
notes/交接-<步骤名>.md 换代时写的交接书;上一代收进 notes/archive/交接/<步骤名>-<时间戳>.md
notes/archive/<YYYYmmdd-HHMMSS>/ --new/new 归档掉的 lineage.json需求.md目标.md。是移动,不是删除

--isolate 时工作台会挪到仓库外面:<workspace>.parent/.flower-<workspace 名>/。 worktree 是每个 agent 的私有副本,工作台是跨 agent 的共享层,共享的东西不能放进私有围栏里。 此时给模型的工作台路径是绝对路径。