エージェントが返してきたコマンドをそのまま Windows のターミナルへ貼り、最初の && で止まる。個人開発でプラットフォームをまたいでいると、この一行の落差に何度か時間を取られます。
原因は Antigravity 側にはありません。コマンドが落ちる先が違うだけです。macOS と Linux なら bash か zsh、Windows なら PowerShell。学習元になったワンライナーの多くは前者の形をしています。
8月20日のハブ 2.9.1 で .ps1 / .psm1 / .psd1 と pwsh / ps1 のコードフェンスに構文ハイライトが入りました。PowerShell を書かせる前提が整いつつある、という合図として私は受け取りました。そこで手元に PowerShell 7.6.5 を用意し、エージェントが書きがちな形をひとつずつ流しています。
いちばん引っかかったのは && ではなく、終了コードのほうでした。
&& が通るかどうかは、PowerShell の世代で分かれます
まず自分の環境がどちらなのかを見ます。1行で済みます。
$PSVersionTable.PSVersion
$PSVersionTable.PSEdition手元の環境では 7.6.5 と Core が返りました。Core なら PowerShell 7 系、Desktop なら Windows に標準で入っている Windows PowerShell 5.1 です。Windows 機を買ってきたままの状態なら、多くは後者から始まります。
この世代差が、エージェントの出力とそのままぶつかります。
| 項目 | Windows PowerShell 5.1 | PowerShell 7.x |
|---|---|---|
&& / || の連結 | 予約語として弾かれる(構文エラー) | 使える |
curl / wget | Invoke-WebRequest の別名 | 別名なし(実体が動く) |
Out-File の既定エンコーディング | UTF-16LE(BOM 付き) | UTF-8(BOM なし) |
| 入手方法 | Windows に同梱 | 別途インストール |
&& については、PowerShell 7.6.5 で実際に流しました。
pwsh -NoProfile -c 'true && echo "chained-ok"'
# chained-ok通ります。同じ文字列を 5.1 に渡すと、実行される前に構文として弾かれます。エージェントは「コマンドが失敗した」ではなく「そもそも解釈されなかった」返答を受け取るため、直そうとして別のコマンドを書き、また弾かれる、という往復が起きます。
もうひとつ、起動コストも測っておきました。pwsh -NoProfile -c '1' を5回まわして 228 / 211 / 217 / 217 / 232 ミリ秒。同じ環境の bash -c 'true' は3回とも 3 ミリ秒でした。1タスクあたり数十回コマンドを打つエージェント運用では、ここだけで十数秒が積み上がります。プロファイルの読み込みを外す -NoProfile は、それでも付けておく価値があります。
エージェントが curl と書くと、5.1 では別のコマンドが動きます
PowerShell 7.6.5 で別名の有無を確認しました。
Get-Alias curl -ErrorAction SilentlyContinue # 何も返らない
Get-Alias wget -ErrorAction SilentlyContinue # 何も返らない7 系では curl と wget の別名が外されています。つまり curl -s https://example.com はそのまま実体の curl に届きます。
ところが 5.1 では、この2つが Invoke-WebRequest の別名として定義されたままです。-s や -H は Invoke-WebRequest の知らないパラメータですから、通信に失敗したのではなく「パラメータ名が違う」というエラーが返ります。エラーメッセージにネットワークの話が一切出てこないので、原因を探す方向が最初からずれます。
回避は単純で、外部コマンドは拡張子まで書くことです。
curl.exe -s https://example.com
node.exe --versionなお ls や cat のような別名は、7 系でもプラットフォームによって扱いが違います。私が確認できたのは linux-x64 版の 7.6.5 上の挙動なので、ここで根拠として使えるのは curl と wget の2つだけです。Windows 機での別名一覧は Get-Alias を一度打って、自分の目で確かめるのが確実です。
失敗が 1 に潰れるので、成否の自動判定が狂います
ここが今回いちばん気になった部分です。PowerShell に -c(-Command)でコマンドを渡すと、中で起きた終了コードがそのまま外へ出てこない場合があります。実測した結果を並べます。
| 渡した形 | 内側の終了コード | 呼び出し元が受け取った値 |
|---|---|---|
pwsh -NoProfile -c 'exit 3' | 3 | 3 |
pwsh -NoProfile -c 'exit 42' | 42 | 42 |
-c の中で外部コマンドが 3 で終了 | 3 | 1 |
同上に ; exit $LASTEXITCODE を追加 | 3 | 3 |
-c の中で ls が存在しないパスに失敗 | 2 | 1 |
同上に ; exit $LASTEXITCODE を追加 | 2 | 2 |
pwsh -NoProfile -File ./t.ps1(中身は exit 3) | 3 | 3 |
失敗するコマンド || echo fallback | 2 | 0 |
exit を自分で書いたときは値がそのまま届きます。外部コマンドが失敗したときだけ、3 も 2 も等しく 1 に丸められます。失敗したこと自体は伝わりますが、どう失敗したかは消えます。終了コードで分岐する後段の処理を持っている場合、この差は効きます。
最後の行はさらに紛らわしく、|| の右辺が成功すると全体は 0 で返ります。ログには失敗が出ているのに、呼び出し元は成功として受け取る形です。
$? の読み方にも癖がありました。
# 直後に読むと False
& ./somecommand ; $q = $?; "q=$q code=$LASTEXITCODE" # q=False code=3
# 間に別の文を挟むと True へ戻る
& ./somecommand ; "何か" | Out-Null; "q=$? code=$LASTEXITCODE" # q=True code=3$? は直前の1文の結果しか覚えていません。$LASTEXITCODE のほうは外部コマンドの値を保持し続けるので、判定に使うならこちらです。
私はこの2つを運用の既定にしました。-c を使うなら末尾に exit $LASTEXITCODE を置く。可能なら -File でスクリプトを渡す。bash 側でも似た消え方をすることがあり、そちらは呼び出し側のパイプが Antigravity CLI の失敗を握りつぶしていた話にまとめています。原因は違いますが、症状はよく似ています。
リダイレクトで残したログは、世代でバイト列が変わります
エージェントに実行ログを書き出させて、後から自分で読む。よくやる形です。ここにも世代差があります。
PowerShell 7.6.5 で書き出して、バイト単位で見ました。
"日本語ログ" | Out-File -FilePath ./enc7.txt
(Get-Item ./enc7.txt).Length # 1616 バイトでした。先頭を並べると e6 97 a5 e6 9c ac e8 aa 9e e3 83 ad e3 82 b0 0a で、BOM なしの UTF-8 に改行が1つ付いた形です。grep でも Python でも、そのまま読めます。
5.1 では Out-File と > の既定が UTF-16LE(BOM 付き)です。同じ5文字が倍近いバイト数になり、先頭に BOM が入ります。ファイルを開けば人間の目には正しく見えるのに、後段のパーサやエージェントが読むと1文字ごとにヌルバイトが挟まって見える、という食い違いが起きます。ログが「空に見える」ときの原因が、たいていこれでした。
書き出すときは明示します。
"日本語ログ" | Out-File -FilePath ./log.txt -Encoding utf8utf8NoBOM の指定は 7 以降でのみ使えます。5.1 に留まる環境で BOM を避けたいなら、[System.IO.File]::WriteAllText() を使うほうが確実です。
なお、これは書き出したファイルの話です。ターミナルの表示そのものが化ける場合は原因が別のところにあるので、Antigravity 内蔵ターミナルで日本語が文字化けするときの原因と対処を先に見てください。
この前提は、会話ではなくルールファイルに置きます
ここまでの4点は、チャットで毎回言い添えれば済みます。ただ、続きません。会話が長くなれば埋もれますし、新しいセッションを開けば消えます。
Antigravity には、ルールをファイルとして持たせる仕組みがあります。ハブ 2.9.1 の Customizations 設定ではスキル・ルール・プラグイン・カスタムエージェントがセクションごとに並び、CLI 1.1.15 以降は markdown で定義したエージェントの frontmatter から rules: でルールファイルを名指しできます。ルールツリー全体を継承させず、必要なものだけを確実に効かせたい場合に向いた形です。
私が Windows 向けに置いている内容は、この程度の分量です。
---
name: windows-shell
description: Windows 上でコマンドを出すときのシェル前提
---
- コマンドは PowerShell 7 系(pwsh)で実行される前提で書く
- 外部コマンドは拡張子つきで書く(curl.exe / node.exe)
- ファイルへ書き出すときは -Encoding utf8 を明示する
- -c で渡すスクリプトは exit $LASTEXITCODE で終えるエージェント側からは、次のように名指しします。
---
name: release-helper
rules:
- windows-shell.md
---書いた前提が本当に効いているかどうかは、また別の話です。設定は、書き方を間違えても黙って無視されることがあります。効いていない設定をどうやって見つけるかは無視された設定キーが CI を通り抜ける経路と、その塞ぎ方で扱いました。ルールを増やす前に一度読んでおくと、無駄な追加を減らせます。
次にやること
Windows 機を開いて $PSVersionTable.PSEdition を1回打ってください。Desktop が返ったなら、PowerShell 7 を入れるところが最初の一歩になります。それだけで、この記事に書いた4点のうち3点は自動的に解消します。
私自身、プラットフォームをまたぐたびに同じところで足を止めています。手元で測った数字が、同じ場所で止まっている方の時間を少しでも減らせたなら嬉しいです。