invoke-task-conventions について
Pythonのinvoke CLIにおけるタスクの命名と配線を整えるスキルです。読みやすいコマンド名やモジュールの名前空間に関する規約に加え、タスクのインポート、改名、対話型サブプロセスの注意点をまとめています。
主な機能
- 01動詞を先頭に置くタスク命名規約
- 02定着したCLIコマンド名の維持
- 03モジュール内で明示的なタスク名前空間を定義
- 04ソースモジュールとタスクリストの照合
- 05invoke実行時の対話入力を回避
活用シーン
- 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 はすべて <subject> <verb> の形式です。また、Azure CLI ではこれを明示的に必須としています(「コマンドは [noun] [noun] [verb] パターンに従う必要があります」)。各ルールの先行事例と根拠については、references/rationale.md を参照してください。
例と測定結果は、この著者自身のリポジトリ群(power-user-linux-setup、repo-tasks、scaffoldapy で生成されたプロジェクト)から得たものです。ルールを試したのがこれらのリポジトリだったためです。これらは invoke のルールであり、どの tasks.py にも当てはまります。リポジトリ名が出てきたら、ご自身のリポジトリに置き換えてください。
3 つのルール
1. タスク名は動詞で始めます。 生成するものではなく、動作を名前にします。apt.base ではなく apt.install-base、ai.skills ではなく ai.install-skills とします。Azure CLI でも同じルールを「すべてのコマンド名に動詞を含める」としています。
複数語のタスク名では、動詞を先頭に置きます(zsh.configure-omz、ide.configure-pycharm)。ただし、inv --list で一連の名前として並び、その隣接性が重要な場合は、目的語を先頭にしたままにします(format-check/format-apply、lint-check/lint-apply)。これはケースごとの判断であり、2 つ目のルールではありません。
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 の cli-allowlist/tools.toml にある [inv] エントリの allow_overrides。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 を逆の順序だと指摘しますが、実際に一度そうなったことがあります。
適切な場合は、主体を表す名前空間を優先してください。その配下のタスクが、動作ではなくスコープによって本当に異なる場合に限り、動作を表す名前空間を使います。
動詞の使い方を統一する
意味ごとに動詞を 1 つ選び、類義語を交互に使うのではなく、名前空間をまたいで再利用してください。install、configure、set、write、create、clean、fix、check、apply、print、render などです。Azure CLI のガイドラインも同じ点を指摘し(「異なる種類のオブジェクト間で使う動詞に一貫性を持たせる」)、標準的な意味と衝突する動詞を避けるよう、さらに注意を促しています。
タスクの docstring がすでに動詞で始まっている場合、その動詞を通常は名前にします。docstring が「Cap persistent journal size」で始まるタスクは cap-journal-size に、「List a project's published versions」で始まるタスクは list-versions にするのが適切です。このリポジトリ群での名前変更のいくつかは、まさにこの方法で見つかりました。
名前変更は影響範囲が広いコード変更です
名前変更を単なる文字列置換として扱わないでください。
- Python 関数名は CLI 名と一緒に変更されます(invoke は一方から他方を導出します)。また、フェーズのリストや
pre=チェーンは関数オブジェクトを直接参照します。名前の変更漏れがあると、インポート時にAttributeErrorが発生します。これはむしろ好都合です。inv --listの読み込みが成功すれば、コード側の変更が完了したと確認できます。 - タスク名はモジュールの外でも、さまざまな文章で参照されています。 24 個のタスクを一括変更した際は、
docs/*.mdの全ファイル、setup.tomlのヘッダーコメント、Dockerfile、CI ワークフロー、ブートストラップスクリプト、他のリポジトリのドキュメントなど、53 個のファイルが対象になりました。 - 機械的な置換では、CLI 名と Python の識別子を見分けられません。 同じタスクについて、両方の表記が同じファイルに現れることもあります。ある名前変更スクリプトは、ユーザー向けの出力ラベルに
[ai.install_skills]を書き込んでしまいました。テストではラベルのテキストを検証していないため、どのテストでも検出されませんでした。スクリプトで名前を変更した後は、文字列リテラル内にあるスネークケース表記を grep し、タスクを実行して出力を確認してください。 - 見出しも確認してください。 Markdown の見出しにあるタスク名を変更すると、そのページのアンカーも変わり、厳密なドキュメントビルドでリンク切れが発生します。編集前に、参照元となる
<page>.md#リンクを grep してください。 - デプロイ済みのコピーも再デプロイが必要です。 名前を変更したタスクが、どこかにインストールされるファイル(常時読み込まれる指示ファイルや、インストール済みのスキルなど)で参照されている場合、リポジトリのソースを編集しただけでは変更の半分しか完了していません。
古い名前を @task(aliases=[...]) で残すのではなく、名前を完全に変更してください。エイリアスを残すと CLI の表面が恒久的に二倍になり、今後の検索でもすべてのタスクに別名が一つずつ増えます。きれいに切り替えるためのコストは、1 セッション分のタイプミスだけです。
配線上の落とし穴
インポートされたタスクは公開タスクになります。 Collection.from_module はモジュールの名前空間で見つけたすべての Task オブジェクトを追加します。インポートされたタスクも定義されたタスクと区別されないため、pre= チェーン用に取り込まれたタスクが、インポート先モジュールの名前で二重に公開されます。この結果、誰も宣言していないタスクが 4 つ生成されました(test.unit と並ぶ quality.unit、および、それぞれを所有する venv/direnv/agents タスクと並ぶ dev-env.create、dev-env.allow、dev-env.claude-hook)。さらに、別のリポジトリがそのうち 1 つを正式な名前としてドキュメントに記載するまで、問題は見過ごされていました。
タスクをインポートするモジュールでは、明示的なモジュールレベルの名前空間を宣言する必要があります。from_module は自動スキャンよりもこちらを優先します。
ns = Collection(setup) # only what this module actually owns
別のリポジトリのタスクを名前で参照する場合は、コマンドがたまたま実行できるかではなく、そのリポジトリのモジュールのソースを確認してください。 コマンドが動作しても、誰かの import 文による偶然かもしれません。
名前空間は配線されて初めて存在します。 選択した上流モジュールから独自の Collection を構築する利用側のリポジトリでは、インポートしていないモジュール内のタスクが気付かないうちに失われます。依存関係を更新したら、テストスイートだけでなく inv --list も実行してください。
タスクでは、入力を待つ処理を実行してはいけません
c.run では、pty=True を指定しても実行できません。 原因は独立して 2 つあり、後者はこのファミリーが標準で使うインタープリターでは致命的です。
- invoke は stdin をエコーし、子プロセスと stdin の取り合いになります。
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 は stdin を転送できません。
terminals.bytes_to_read()はfcntl.ioctl(stdin, FIONREAD, b" ")を呼び出しますが、4 バイトの結果に対して 2 バイトのバッファーを渡しています。Python 3.14 より前のバージョンではオーバーフローが黙って発生していましたが、3.14 ではfcntl.ioctlが強化され、最初のキー入力でSystemError: buffer overflowが発生し、invoke の stdin スレッドが終了します。pty の有無にかかわらず、子プロセスには何も届きません。上流の pyinvoke/invoke#1070 で修正作業中ですが、invoke 3.0.3 時点では未リリースです。
| Python | stdin の転送が必要な c.run |
|---|---|
| 3.10–13 | 動作しますが、入力したテキストがエコーされます(原因 1) |
| 3.14 | 停止します — 子プロセスには何も転送されません |
pty=True は注意すべき落とし穴です。 子プロセスが自身の pty でエコーを制御するため、原因 1 は解消されるように見えますが、原因 2 には何も対処しません。そのため、インタープリターが変わるまでは正しいパターンに見えますが、変わると出力のないまま停止します。
このルールを満たす方法は 2 つあります。対話型の子プロセスは、実際のターミナルを継承する通常の subprocess として実行し、それ以外はすべて非対話型にします。最初に sudo で認証し、その後は sudo -n を使います。また、apt には DEBIAN_FRONTEND=noninteractive と --force-confold を指定します。
リポジトリ内を rg 'pty=True' で監査し、該当箇所をすべて確認してください — このパターンは認証情報の入力プロンプトで最もよく使われていますが、まさに停止を避けたい場面です。最初の inv wsl.install で問題に遭遇し、sudo のプロンプトの後に、パスワードが平文で表示されたまま停止しました。その後、コンテナー内で 3.10〜3.14 の各バージョンを使い、2 つの原因を再現しました。
既存のリポジトリを監査する
inv --list | grep -oP '^ [a-z0-9.-]+' で全体像を把握できます。記憶に頼らず、モジュールのソースと照らし合わせて確認してください。この比較によって、ルール違反と、宣言されていないタスクの両方を見つけられます。次に各名前を、動詞を先頭に置いたもの、コミュニティの慣例(ルール 2)、アクションの名前空間(ルール 3)、実際の違反のいずれかに分類します。意図的に準拠しているケースは、後から確認できる場所に記録してください。そうしないと、次に読む人が「修正」してしまいます — gitflow.feature-start は、git flow feature start を反映しているとわかるまでは違反に見えます。