关于 invoke-task-conventions
用于规范 Python invoke 命令行任务命名和接线方式的技能,涵盖可读命令名与模块命名空间惯例,也记录任务导入、重命名和交互式子进程处理中的陷阱。
主要功能
- 01采用动词优先的任务命名惯例
- 02保留已有的 CLI 命令名称
- 03在模块中定义明确的任务命名空间
- 04对照源模块审查任务列表
- 05避免通过 invoke 运行交互式输入
适用场景
- 01统一多个仓库中的任务名称
- 02依赖更新后检查 invoke 任务接线
- 03重命名任务并核对引用和文档
- 04检查启动交互式命令的任务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ theodoread/ agent-skills --skill invoke-task-conventions安装后在 Claude Code 中输入/invoke-task-conventions确认已加载。
附带文件references/rationale.md
详细介绍
命名和连接 invoke 任务
inv <namespace>.<task> 应读作祈使命令。命名空间是主体,任务是动作:inv apt.install-base 就是“apt:安装基础内容”,inv zsh.fix-history 就是“zsh:修复历史记录”。
这是主流的 CLI 形式,并非本地自创——gh pr create、docker image prune、kubectl config set-context 都采用 <主体> <动词> 的形式,而 Azure CLI 更是明确要求这样做(“命令必须遵循 [名词] [名词] [动词] 模式”)。关于先例以及以下各项规则背后的依据,请参见 references/rationale.md。
示例和测量数据来自作者自己的仓库系列——power-user-linux-setup、repo-tasks 以及由 scaffoldapy 生成的项目——因为这些规则是在这些项目中试用的。这些是 invoke 的规则,适用于任何 tasks.py;提到这些仓库时,都可以换成你自己的仓库。
三条规则
1. 任务名称以动词开头。 命名动作,而不是产出的内容——用 apt.install-base,而非 apt.base;用 ai.install-skills,而非 ai.skills。Azure CLI 也提出了同样的规则:“所有命令名称都应包含动词”。
多词任务名称将动词放在前面(zsh.configure-omz、ide.configure-pycharm),但有一个例外:如果一对名称在 inv --list 中能构成一个系列,且相邻排列正是重点,就保留宾语在前的形式——format-check/format-apply、lint-check/lint-apply。这要逐个判断,不是第二条规则。
2. 社区惯例优先于规则。 如果某个名称已有 CLI 惯例,就保留它——status、list、version、check、diff。读者自然会想到 inv gnome.status;gnome.show-status 虽然更一致,却更糟。包装某个具名子命令的任务也一样:deps.tree 包装 uv tree,因此保留原名。
这些名称不仅易读,而且关乎实际权限:自 2026-08-25 起,本机 Claude Code 的允许列表(power-user-linux-setup 中 [inv] 条目的 allow_overrides,位于 cli-allowlist/tools.toml,并由 inv allowlist.apply 按各运行框架生成)会仅凭惯例,在每个仓库中自动批准 inv <ns>.status|check|diff|list|show、<ns>.print-*、<ns>.check-*、<ns>.list-* 和 <ns>.*-check。名称属于这些形式的任务必须只检查,绝不能修改——如果 deploy.check 还会修复它发现的问题,就会在未经提示的情况下运行。会修改内容的任务应使用体现修改操作的名称(fix、apply、sync、configure)。
3. 有些命名空间本身就是动作。 setup、verify、clean、deploy、test。这种情况下,叶级名称改为指明范围或对象:verify.all、clean.caches、deploy.all、test.unit。这是对规则 1 明确说明的例外,不是对它的违反——只看到规则 1 的读者会认为 test.unit 的顺序颠倒了,而这种情况已经发生过一次。
适合时优先使用主体命名空间。只有当其下的任务确实按范围而非动作区分时,才使用动作命名空间。
动词用法保持一致
每种含义选定一个动词,并在不同命名空间中重复使用,不要交替使用同义词——install、configure、set、write、create、clean、fix、check、apply、print、render。Azure CLI 的指南也强调了同一点(“在不同类型的对象中使用的动词应保持一致”),并另外提醒不要使用与标准含义冲突的动词。
如果任务的文档字符串已经以动词开头,通常就用那个动词命名:文档字符串以“限制持久化日志的大小”开头的任务,应命名为 cap-journal-size;以“列出项目已发布的版本”开头的任务,应命名为 list-versions。这个系列中的多项重命名,都是用这种方式发现的。
重命名是影响范围很广的代码变更
不要把重命名当作字符串替换:
- Python 函数名会随 CLI 名称一起改变(invoke 会从一个名称推导出另一个),而阶段列表或
pre=链会直接引用函数对象。漏掉重命名会在导入时触发AttributeError——这反而是好事:inv --list能正常加载,就证明代码部分已经全部完成。 - 任务名称会在模块之外的许多说明文字中出现。 在一次包含 24 个任务的处理过程中,共涉及 53 个文件,包括所有
docs/*.md、setup.toml的头部注释、一个 Dockerfile、CI 工作流、引导脚本,以及其他仓库的文档。 - 机械式处理无法区分 CLI 名称和 Python 标识符,而同一任务的两种拼写可能出现在同一个文件中。一次重命名脚本把
[ai.install_skills]写进了面向用户的输出标签;没有测试发现这个问题,因为测试不会断言标签文本。脚本重命名后,搜索字符串字面量中的蛇形命名拼写,并运行任务、查看其输出。 - 检查标题。 Markdown 标题中的任务名称变更会改变该页面的锚点,严格的文档构建会因悬空链接而失败。编辑前,搜索指向该页面的
<page>.md#链接。 - 已部署的副本需要重新部署。 如果已重命名的任务出现在会被安装到某处的文件中——例如始终加载的指令文件或已安装的技能——那么只编辑仓库源文件只完成了一半的变更。
直接彻底重命名,不要通过 @task(aliases=[...]) 保留旧名称。别名会永久扩大 CLI 的表面范围,让每个任务在今后的每次搜索中都有第二个名称;彻底更名的代价,不过是一个需要适应新名称的工作时段。
接线陷阱
导入的任务就是已发布的任务。 Collection.from_module 会添加它在模块命名空间中找到的每个 Task 对象;导入的任务与定义的任务无法区分,因此,为 pre= 链导入的任务会以导入模块的名称再次发布。这导致出现了四个无人声明的任务(test.unit 旁边的 quality.unit;以及属于 venv/direnv/agents 任务的 dev-env.create、dev-env.allow 和 dev-env.claude-hook),而且这个问题长期未被发现,以至于另一个仓库把其中一个记录成了正式名称。
任何导入任务的模块都必须声明显式的模块级命名空间,from_module 会优先使用它,而不是自动扫描:
ns = Collection(setup) # only what this module actually owns
按名称引用其他仓库的任务时,请检查该仓库的模块源代码,不要只看命令能否运行。命令能正常运行,可能只是某人的导入语句碰巧使然。
只有接好线,命名空间才会存在。 如果使用者仓库从所选的上游模块构建自己的 Collection,就会悄无声息地丢失未导入模块中的所有任务。依赖更新后运行 inv --list,不要只运行测试套件。
任务不得运行任何需要等待键盘输入的内容
无论是否使用 pty=True,都不能通过 c.run 执行。 原因有两个,彼此独立;第二个原因在此系列默认使用的解释器上是致命的:
- invoke 会回显标准输入,并与子进程争抢输入。
Runner.should_echo_stdin()的逻辑是(not using_pty) and isatty(stdin),因此在终端中执行非 PTY 的c.run时,它会打印读到的每个字节——它已将终端设为 cbreak 模式,否则用户将什么也看不到。与此同时,sudo会从/dev/tty(同一个终端)读取输入,谁先读到字节,字节就会给谁:密码会被打印出来,子进程却收不到,于是再次提示输入,运行看起来就像卡住了一样。用一个任务演示:其全部内容只有c.run("head -n 1 > /dev/null")——输入SUPERSECRET后,它会被原样打印出来。 - 在 Python 3.14 上,invoke 完全无法转发标准输入。
terminals.bytes_to_read()会调用fcntl.ioctl(stdin, FIONREAD, b" ")——用 2 字节的缓冲区接收 4 字节的结果。在 Python 3.14 之前的所有版本中,这都会静默溢出;3.14 加强了fcntl.ioctl的检查,会抛出SystemError: buffer overflow,导致 invoke 的标准输入线程在第一次按键时退出。无论是否使用 PTY,子进程都收不到任何输入。上游 pyinvoke/invoke#1070,修复尚未发布;截至 invoke 3.0.3 仍未发布。
| Python | 需要转发标准输入的 c.run |
|---|---|
| 3.10–13 | 可正常运行,但会回显输入的文本(原因 1) |
| 3.14 | 卡住——没有任何输入会转发给子进程 |
值得特别指出的是,pty=True 是个陷阱。 它看似能解决原因 1,因为子进程会在自己的 PTY 上控制回显;但对原因 2 毫无帮助——因此它看起来一直是正确的模式,直到解释器版本升级后,程序便会无输出地卡住。
有两种做法符合这条规则。交互式子进程应作为普通 subprocess 运行,并继承实际终端;其他所有内容都应设为非交互式:先进行一次 sudo 身份验证,之后使用 sudo -n;为 apt 设置 DEBIAN_FRONTEND=noninteractive,并添加 --force-confold。
使用 rg 'pty=True' 审查仓库,并逐个检查所有匹配项——这种模式最常见于凭据提示附近,而那里恰恰是最不该卡住的地方。第一次运行 inv wsl.install 时,我就用代价高昂的方式发现了这个问题:程序在 sudo 提示后卡住,密码以明文回显;随后在容器中使用 3.10–3.14 复现了这两个原因。
审查现有仓库
inv --list | grep -oP '^ [a-z0-9.-]+' 可以列出全部任务。对照模块源代码逐个检查,不要凭记忆判断——只有这样,才能发现违规项和无人声明的任务。然后将每个名称归入以下类别:已采用动词开头的命名、社区惯例(规则 2)、操作命名空间(规则 3),或确实违规。应将有意遵循惯例的情况记录在可靠的位置,否则后续读者会把它们“修正”掉——如果不了解它与 git flow feature start 保持一致,gitflow.feature-start 看起来就像违规项。