命令行参考¶
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/--help。run 和 setup 没有自己的开关。
调用形式¶
flower 的所有 argv 先过一遍 _with_default_cmd() 补默认子命令,再交给 argparse (cli.py:1437-1439)。这就是为什么 flower "帮我做一个 X" 能跑 —— 它被改写成了 flower go "帮我做一个 X"。
补默认子命令的规则(cli.py:940-976):
- 全局开关的集合从主 parser 自身派生,不是硬编码的列表。
nargs == 0的算纯开关, 其余算带值开关。 - 从左往右扫,跳过全局开关。带值的连值一起跳,
--workspace=/tmp这种=写法也认。 - 停在第一个不是全局开关的 token。它若是
go、run、once之一就原样交给 argparse; 否则在它前面插一个go,于是它变成go的诉求正文。 - 扫完都没遇到位置参数(空 argv,或只有全局开关)→ 末尾补
go,进交互输入。 - 例外: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 打帮助 |
run 和 once 这两个词不能直接当诉求正文,这是有意保留的歧义(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:提问不再假装等 30 分钟,立刻落空,agent 自己判断并把假设写进 需求确认书的"未知与假设"那一段。
子命令¶
go¶
help 文本:一键跑:问清需求 → 派人干活(不写子命令时的默认)(cli.py:1275-1307)。
位置参数 ask,nargs="?" —— 不给就进交互输入。这是最常用的入口,flower "做 X" 走的就是它。
它做的事(cli.py:1190-1221):
ensure_credentials()—— 检查凭证,并真打一次 API 探针,见首次运行的配置流程。- 唤醒探测:只读看一眼这个目录用过没有,一个字节都不写。
- 没给
ask就打提示符问一句;输入/new等价于--new,然后再问一次诉求。 - 是接续的话打一行唤醒横幅。
- 造出三步的流程:
确认需求→设定目标→干活, 每轮干活后面跟一个干活·判定#N。--clarify-only只留第一步。 - 开跑。
唤醒横幅长这样(路径里的家目录会被换成 ~):
需求已确认 恒有;目标 N 条 只在有判定清单时出现;干活上下文 X 需要能从 sessions.db 里查到那条会话的最后一轮上下文,查不到就不显示。
-W 和 -T 在 go 路径上被静默覆盖
这两个全局开关在 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 | 取值只能是 default、acceptEdits、plan、bypassPermissions 之一,给别的值 argparse 报错退出码 2 |
-b, --budget | float | 无上限 | 美元预算上限,超了就停 |
--resume SESSION_ID | str | 无 | 续跑某条已有会话 |
--fork | 开关 | False | 分叉而不是续跑,配合 --resume 用 |
once 显示的用时和累计花费永远是 0
once 每收到一个事件就新建一个渲染器实例(cli.py:688-690、cli.py:1239), 而计时起点和累计花费是存在实例上的(cli.py:500-501)。于是:
- 收尾那行的
用时恒为0:00 - 状态行里的
累计 $0.00恒为 0,上下文也从不累积
单步的真实花费要去 runs/manifest.json 里看 cost_usd 字段。go 和 run 路径持有 同一个渲染器实例,没有这个问题。
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。角色是只读的,工具只有 Read、Glob、Grep,最多 12 轮, 花费上限 $0.5。它看到的上下文是最近 60 条事件(thinking 和 prompt 事件不进这个窗口), 每条截断到 200 字符,外加工作台的路径说明。
它是并发跑的,正在跑的运行一秒都不用等。回答长这样:
失败时打一行红字 # 旁路问答失败:<类型>: <消息>,不影响主流程。 退出时最多等旁路收尾 120 秒,等之前先打一行 (等 N 条旁路问答收尾…)。
它说的话不会进入那次运行的上下文 —— 问了不影响运行,答完即弃。
全角 ? 不触发旁路问答 —— 中文输入法用户会踩
判断旁路问答的那行代码是(cli.py:907):
两个字符都是半角 ASCII ?(0x3f)—— 逐字节验过。从写法看意图显然是想同时接受半角 ? 和中文输入法打出的全角 ?(U+FF1F),但实际写成了同一个字符。
后果:用全角 ? 开头的一行不会被当成旁路提问,而是被当成"追加需求"静默送进收件箱, 进而被追加进 notes/需求.md。你看到的回执是 + 收到,不是 # 旁路。
要问旁路,必须用半角 ? —— 打之前先把输入法切到英文,或者只把第一个字符打成半角。
屏幕上都是什么¶
图标一律是 ASCII,不是 emoji(cli.py:51-69)。原因写在代码注释里:emoji 和框线、几何、箭头 字符会触发终端字形回退,曾经导致两次终端崩溃。
| 图标 | 含义 | 图标 | 含义 |
|---|---|---|---|
= | 步骤分隔线 | + | 完成 / 已答 / 收到 |
~ | 思考、重试 | x | 失败 / 报错 |
> | 派人 | # | 换代、旁路、任务 |
* | 工具调用 | - | 状态行、列表项 |
? | 提问 | <- | 接上次、交接落点 |
! | 警告 / 打断 | . | 已跳过 |
\| | subagent 的缩进竖线 |
旧文档里的 ❓ 和 ↩ 在真实终端里不存在
早期文档用 ❓ 表示提问、用 ↩ 表示唤醒行。代码里从来不是这两个字符 —— 提问的图标是半角 ?,唤醒和交接落点的图标是两个 ASCII 字符 <-。
所以真实终端打出来的是:
不是 ❓ 这个工具……,也不是 ↩ 在 ~/proj 接上上次。照旧文档去 grep 日志会一无所获。
提问的五种状态,屏幕上分别是:
| 状态 | 屏幕输出 |
|---|---|
| 问出来了 | ? <问题>,后面逐条列 1) 选项一,有额度时再加 (还能问 N 次) |
| 答了 | + <答案> |
| 超时 | ! 无人应答 —— 它会自己判断,把假设记进「未知与假设」 |
| 额度用完 | ! 提问额度用完 |
| 你跳过了 | . 已跳过 |
--asks 取不限(默认)时,最后那行"还能问 N 次"不显示。
换代写完交接书时是一整块:
# 上下文 950.0K/1000K —— 写交接准备换代
- 现在在做 …
- 已定的事 …
- 走不通的 …
- 下一步 …
<- 交接写在 ~/proj/.flower/notes/交接-干活.md
<- 新会话接手,上下文从 950.0K 重新开始
交接书降级时会多插一行红字 交接没写成,用了降级版本 —— 接手的人会自己去现场看。
输出还做了两件你看不见的事:所有输出行先过一遍消毒,只放行 flower 自己的 SGR 颜色码, 模型或工具吐出来的清屏、移光标序列被整段吞掉;宽度取 max(40, min(终端列数, 110)), 所以宽终端上不会铺满整行,这是有意的。
起跑时的提示符¶
裸跑 flower(不带诉求)时先问一句。两种文案:
第二种只在这个目录已经跑过、需求.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 语义完全不同。
在起跑提示符 > 上按 —— 直接退出程序,打 已取消。
运行途中按 —— 打断当前这一轮,并给你一次说话的机会:
在这里直接回车就是只打断不说话,接着跑。当时有待答提问的话会多打一行 (有 N 个提问还等着,打断不影响它们)。
再按一次 Ctrl+C 就是真退出,而且是未捕获的 KeyboardInterrupt —— 屏幕上会有一段 Python traceback,不是干净退出。
打断是协作式的:在消息边界干净断开,不硬取消任务。它不算一次失败尝试,不消耗重试次数。 续跑时会附一段说明,告诉模型"在飞的工具调用返回 interrupted 是打断的正常副作用,不是环境故障"。
这套自定义的 Ctrl-C 只在 sys.stdin.isatty() 时装(cli.py:1097)。管道里跑时保持 Python 默认行为, 也就是第一次就退出。once 路径不走这里,所以 once 上的 Ctrl-C 也是第一次就退出。
SIGHUP / SIGTERM¶
go 和 run 路径给 SIGHUP 和 SIGTERM 都装了处理:先把在飞的那一步也写进 manifest.json 并标成 killed-by-signal,再恢复默认动作、真的走掉。
起因是终端崩溃时内核发 SIGHUP,默认动作直接终止进程,finally 不跑、清单不写 —— 一次运行的账目就丢了。非操作系统主线程或平台不支持时静默跳过。
首次运行的配置流程¶
go、run、once 三个入口开头都调 ensure_credentials()(cli.py:1392-1428),两道关。
第一道:有没有凭证¶
按优先级找一遍凭证。找不到 ANTHROPIC_API_KEY 或 ANTHROPIC_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.json 和 settings.local.json 的 env 块当作最后一级回退,只借其中 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_MODEL、ANTHROPIC_DEFAULT_OPUS_MODEL、ANTHROPIC_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_MODEL → ANTHROPIC_MODEL → claude-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_found、not found、does not exist、unknown model、no such model、invalid 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 key、 authentication、unauthorized、无效…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 路径)最后打一行:
这个金额只算本进程的花费,不含上一次运行的 —— 尽管清单文件本身是跨进程累积的。
它在项目里创建了什么¶
两棵树:<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.db 和 manifest.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 的共享层,共享的东西不能放进私有围栏里。 此时给模型的工作台路径是绝对路径。