45395 - シコウサクゴ -

exit code 0は正常ではない:AIに「意図通りに動いているか」を検証させる仕組みの導入

90個のスケジュールジョブを運用する本番システムで、「定期実行が数日間止まっているのに誰も気づかない」という障害が繰り返し発生した。リソース監視が16日間無効化されていた事故、plist設定ファイル30個が壊れていた事故、Dry-Runが稼働していなかった事故――いずれも共通する根本原因は**「exit code 0 = 正常動作」という前提**だった。

この問題をAIとの対話の中で分析し、ワークフロー全体に「意図検証(Intent Verification)」の原則を組み込んだ過程を記録する。


何が起きたのか:3つのサイレント障害

事故1:リソース監視の16日間無効化

リソース監視スクリプトが16日間、実質的に何もしていなかった。

1
# 表面上の状態
2
$ launchctl list | grep monitor_resources
3
78429 0 jp.data-processing.monitor_resources ← exit code 0、正常に見える
4
5
# 実際の状態
6
$ tail -1 /tmp/data_processing/monitor_resources.log
7
[2026-03-08 08:05:01] INFO: Starting resource monitor...
8
[2026-03-08 08:05:01] INFO: No resources to monitor. ← アクティブリソースがないのではなく、API接続できていない
9
[2026-03-08 08:05:01] INFO: Monitor completed. ← exit code 0 で正常終了

原因は、launchd plistの EnvironmentVariablesMONITORING_API_KEY が未設定だったこと。スクリプトは os.getenv("MONITORING_API_KEY")None を取得し、APIクライアントの初期化をスキップした。エラーは出ず、「監視対象なし」として正常終了していた。

事故2:plist 30ファイルの形式破損

AIに依頼して生成させたlaunchd設定ファイル(plist)30個が、実はXML形式ではなくJSON形式で出力されていた。

1
# AIが書いたコード(問題のあるバージョン)
2
import json
3
4
def generate_plist(config: dict, output_path: str) -> None:
5
with open(output_path, 'w') as f:
6
json.dump(config, f, indent=2) # ← JSON形式で出力
7
8
# macOS launchdはXML plistのみ受け付ける
9
# しかしキャッシュがあるため、既にロード済みのジョブは動作し続ける
10
# → macOS再起動で全ジョブ消失のリスクがあった

launchctl list にジョブが表示されるため正常に見えたが、plistファイル自体は壊れていた。

事故3:Dry-Runの長期間停止

複数のDry-Run(テスト運用)ジョブが、デプロイ時の設定ミスで数日間停止していた。

1
# ユーザーの発見時のメッセージ(会話履歴より)
2
「多くのDry-Runが稼働していなかった理由は何ですか?
3
原因と防止策を提案、本番ブランチでプラクティクスモード
4
移行時に同様の不具合が発生しないか検討。」

ジョブ自体は起動していたが、PYTHONPATH の設定不備やインポートエラーで即座に終了。exit code は 0 ではなかったが、Slackへのエラー通知は設定されておらず、誰にも通知されなかった。


核心の対話:「あなたは動作確認していない」

会話履歴を振り返ると、ユーザーからAIへの決定的な指摘があった。

1
ユーザー: 「定期実行が正しく実行されず数日間動いていませんでした。
2
というケースが多いように思う。
3
そもそもテストに漏れがあるのではないか。」

さらに、修正を実施した後に核心を突く指摘が入る。

1
ユーザー: 「あなたは動作確認していないから本番デプロイを
2
失敗し続けるのではないですか?
3
エラーコードが出ていない=正しく動いているではないです。
4
意図目的に沿っているかを確認するにはどうすれば良いですか?」

そしてワークフロー全体への改善要求が続く。

1
ユーザー: 「今後、要件定義やテストなどに意図目的が合っているかの
2
チェックや確認、項目の追加が必要ではありませんか?」

この対話が、「意図検証の原則(Intent Verification)」をワークフローに組み込む起点となった。


根本原因の分析:なぜAIは「exit code 0 = 正常」と判断したか

AIの思考パターン

AIがデプロイ後の確認で行っていた検証は、以下の3つだけだった。

Terminal window
1
# AIが行っていた「動作確認」
2
1. launchctl list | grep "data-processing" # ジョブが登録されているか
3
2. tail -5 /tmp/logs/latest.log # エラーが出ていないか
4
3. echo $? # exit code が 0 か

これは「プロセスが異常終了していないか」の確認であり、「意図通りの結果が生成されているか」の確認ではない。

問題の構造

1
正常終了 (exit 0)
2
3
┌────────┴────────┐
4
│ │
5
意図通りの出力あり 意図通りの出力なし
6
(本当の正常) (サイレント障害)
7
8
┌──────────┤
9
│ │
10
ファイル未生成 接続失敗を
11
だが正常終了 スキップして
12
正常終了

核心: exit code は「プロセスの終了状態」を示すだけであり、「ビジネスロジックの達成度」は示さない。データベースに0件インサートしても exit code 0。ファイルを0個生成しても exit code 0。API接続をスキップしても exit code 0。


解決策:意図検証の原則(Intent Verification)

CLAUDE.md に追加した原則

1
### 意図検証の原則(Intent Verification)
2
3
**exit code 0 = 正常動作ではない**
4
各ステージで「意図通りに動いているか」を明示的に定義・検証すること。
5
6
- **Requirements(B-4)**: 「成功の定義」セクション必須
7
— このジョブ/機能は**何を生成すべきか**
8
(ファイル種別・パス・件数・鮮度)を明記
9
10
- **Test Design(B-6)**: ユニットテスト + 結合テストに加え、
11
**運用テスト**(スケジュール環境で意図通りの生成物が出るか)を設計
12
13
- **デプロイ後検証(C-10)**: exit code 確認だけでなく、
14
**生成物の存在・鮮度・件数を確認**

ワークフローへの組み込み:5ステージへの浸透

Stage 1: 要件定義に「成功の定義」を必須化

1
## 成功の定義
2
3
このジョブは以下を生成する:
4
- ファイル: `/tmp/analysis_results/{date}_*.parquet`
5
- 件数: 最低10ファイル(10データソース分)
6
- 鮮度: 実行日の日付が含まれること
7
- サイズ: 各ファイル 1KB 以上
8
- 下流依存: 集計ジョブが読み取り可能なスキーマ

「何を生成するか」を自然言語で書くだけだが、これがあるだけでAIの動作確認の質が変わる。

Stage 2: テスト設計に「運用テスト」を追加

従来のテスト設計にはユニットテストと結合テストしかなかった。ここに「運用テスト」を追加した。

1
## 運用テスト設計
2
3
### 生成物の存在確認
4
- [ ] 手動実行後、期待されるファイルが出力ディレクトリに存在するか
5
- [ ] ファイルの件数が「成功の定義」の最小件数以上か
6
- [ ] ファイルのサイズが 0 バイトでないか
7
8
### 下流パイプラインの消費確認
9
- [ ] 生成されたファイルを下流ジョブが読み取れるか
10
- [ ] スキーマ(カラム名・型)が下流の期待と一致するか
11
12
### スケジュール環境での検証
13
- [ ] スケジュール実行後、ログにエラーがないか
14
- [ ] 翌日のヘルスチェックで鮮度が確認できるか

Stage 3: デプロイ手順書に「意図検証」セクションを必須化

1
## デプロイ後検証
2
3
### 基本検証
4
- [ ] `plutil -lint <plist>` でXML構文検証
5
- [ ] 手動実行でエラーなし
6
- [ ] `validate_launchd_health.sh` でエラー 0 件
7
8
### 意図検証(生成物チェック)← 新規追加
9
手動実行後に**期待される生成物が実際に存在するか**を確認。
10
exit 0 だけで判断しない。
11
12
1. 出力ディレクトリにファイルが生成されたか
13
2. ファイルの件数・サイズが妥当か
14
3. 下流ジョブが消費できる形式か
15
3 collapsed lines
16
### 翌日監視
17
- ヘルスチェックでログ鮮度 + パイプライン出力を確認
18
(スケジュールが発火し、かつ**意図通りの生成物が出ているか**を検証)

自動検証の実装:validate_launchd_health.sh の Check 9

「意図検証」を人間の記憶に頼っては同じ失敗を繰り返す。自動化した。

1
#!/bin/bash
2
# validate_launchd_health.sh(抜粋)
3
# Check 9: Pipeline Output Verification
4
5
echo "=== Check 9: Pipeline Output Verification ==="
6
7
check_pipeline_output() {
8
local name="$1"
9
local path_pattern="$2"
10
local min_count="$3"
11
local max_age_hours="$4"
12
13
# ファイルの存在確認
14
local count
15
count=$(find "$path_pattern" -type f 2>/dev/null | wc -l)
34 collapsed lines
16
17
if [ "$count" -lt "$min_count" ]; then
18
echo "ERROR: $name: expected >= $min_count files, found $count"
19
ERRORS=$((ERRORS + 1))
20
return
21
fi
22
23
# 鮮度確認(最新ファイルが指定時間以内か)
24
local newest
25
newest=$(find "$path_pattern" -type f -printf '%T@\n' 2>/dev/null \
26
| sort -rn | head -1)
27
28
if [ -n "$newest" ]; then
29
local age_hours
30
age_hours=$(echo "scale=1; ($(date +%s) - $newest) / 3600" | bc)
31
32
if (( $(echo "$age_hours > $max_age_hours" | bc -l) )); then
33
echo "ERROR: $name: newest file is ${age_hours}h old (max: ${max_age_hours}h)"
34
ERRORS=$((ERRORS + 1))
35
else
36
echo "OK: $name: $count files, newest ${age_hours}h ago"
37
fi
38
fi
39
}
40
41
# パイプライン出力の検証定義
42
check_pipeline_output "Data Analysis Parquet" \
43
"$DATA_DIR/calcAnalyticsDatas/*.parquet" 10 48
44
45
check_pipeline_output "Trigger Files" \
46
"$DATA_DIR/processTriggers/*.json" 1 48
47
48
check_pipeline_output "Report Parquet" \
49
"$DATA_DIR/calcReportDatas/*.parquet" 50 48

このスクリプトは毎朝06:30に自動実行され、結果をSlackに通知する。

1
# 正常時
2
✅ [launchd ヘルスチェック] 2026-03-29 06:30
3
対象: 90ジョブ | エラー: 0 | 警告: 0
4
Check 9 Pipeline Output: ALL OK
5
6
# 異常時
7
❌ [launchd ヘルスチェック] 2026-03-29 06:30
8
対象: 90ジョブ | エラー: 3 | 警告: 1
9
Check 9 Pipeline Output:
10
ERROR: Trigger Files: expected >= 1 files, found 0
11
ERROR: Report Parquet: newest file is 72.3h old (max: 48h)

「意図」をAIに理解させるための対話パターン

plistの設定変更を行う際に、設定の「経緯と意図と目的」をAIに調査させる対話パターンが定着した。

1
# 会話履歴より
2
ユーザー: 「#1を本当に実施して良いか再度詳細調査、
3
2つのplistが生成された経緯と意図と目的を把握」
4
5
ユーザー: 「中程度 #3: data_source_a.monitor_resources が月曜のみ
6
(機会損失)再度詳細調査、
7
Weekday=1が設定された経緯、意図目的を把握」

これは「変更前に、なぜ現在の設定がそうなっているかを調べろ」という指示であり、AIがgit log、コミットメッセージ、ADR、ドキュメントを横断的に調査して「意図」を復元する。

Terminal window
1
# AIが実行する調査コマンドの例
2
git log --all --oneline -- '**/data_source_a*monitor*.plist'
3
git show <commit_hash> -- _launched/data_source_a.monitor_resources.plist
4
grep -r "Weekday" _launched/*.plist

意図が不明な設定は変更しない。これが「意図検証の原則」のデプロイ前バージョンである。


導入前後の比較

Before(意図検証なし)

1
デプロイ
2
3
launchctl list で確認 ← ジョブ登録のみ確認
4
5
exit code 0 を確認 ← プロセス終了のみ確認
6
7
「正常」と判断
8
9
数日後:「あれ、ファイルが生成されていない?」

検出までの平均時間: 3〜16日(人間が別の作業中にたまたま気づく)

After(意図検証あり)

1
デプロイ
2
3
plutil -lint で構文検証 ← ファイル形式の確認
4
5
手動実行 + 生成物確認 ← 意図通りの出力があるか
6
7
翌朝 validate_launchd_health.sh ← 自動検証(Check 9)
8
9
Slack通知で結果確認 ← 「来ない=異常」のハートビート

検出までの時間: 最大24時間(翌朝のヘルスチェックで自動検出)


意図検証チェーンの全体像

1
Requirements → Design → Test Design → Implementation → Deploy
2
│ │ │ │ │
3
「成功の定義」 設計の 運用テスト設計 テスト実行 意図検証
4
を明記 意図を を追加 + QA (生成物
5
ADRに記録 ワークフロー チェック)
6
7
validate_launchd_
8
health.sh Check 9
9
(毎朝自動実行)

各ステージで「意図」が伝搬される仕組みになっている。要件定義の「成功の定義」が、最終的にヘルスチェックの検証項目になる。


学んだこと

  1. exit code 0 は「プロセスが異常終了しなかった」だけであり、「期待する出力が生成された」ことは保証しない。この区別をCLAUDE.mdに明記したことで、AIの動作確認の精度が根本的に変わった

  2. 「意図」は伝搬しなければ消える。要件定義に書いた「成功の定義」がテスト設計・デプロイ手順書・自動検証まで一貫して伝わる仕組みが必要。人間の記憶に頼ると、ステージをまたぐたびに忘れる

  3. 自動検証が最後の砦。手動のチェックリストは「忘れる」「面倒でスキップする」の2つのリスクがある。validate_launchd_health.sh の Check 9 のような自動検証を毎日走らせることで、人間もAIも見落とすサイレント障害を24時間以内に検出できる


まとめ

  1. 「定期実行が数日間止まっていた」障害の根本原因は、AIが「exit code 0 = 正常」と判断していたこと。ユーザーからの「エラーコードが出ていない = 正しく動いているではない」という指摘が、ワークフロー全体の見直しにつながった

  2. 解決策は「意図検証の原則」をワークフローの全5ステージに組み込むこと。要件定義の「成功の定義」から始まり、テスト設計の「運用テスト」、デプロイ手順書の「生成物チェック」、そして自動ヘルスチェックの Check 9 まで、一貫して「意図通りの結果が出ているか」を検証する

  3. この原則はAI駆動開発に限った話ではない。しかし、AIは「エラーが出ていなければ正常」と判断しやすい傾向があり、「ビジネスロジックの達成度」を自発的に検証しない。CLAUDE.mdで明示的に指示することで初めて、AIは「意図」を意識した検証を行うようになった

Article title:exit code 0は正常ではない:AIに「意図通りに動いているか」を検証させる仕組みの導入
Article author:45395
Release time:2026-04-02