agentleFS
s977043s977043/PlanGate/.agents/skills/ho-apply-script/SKILL.md

ho-apply-script

Skill3 starsChanged 11 days agoMITView on GitHub

Hardening Override 対象パスへの patch を Human が安全に適用するための、検証つきスクリプトを組み立てる。Use when: .github/workflows/** / scripts/hooks/** / .claude/settings*.json など AI が編集できないパスの変更を Human に渡す時、apply-*.sh を新規に書く時。

  • Deletes or force-pushes

The file

Files
SKILL.md

HO Apply Script

Hardening Override(HO)対象パスは AI が編集できない。AI は patch と「検証つき適用 スクリプト」までを用意し、実行は Human が行う(責務 4 分類の Human-owned)。

本 skill はそのスクリプトの型を定める。毎回ゼロから書くと同じバグを踏むため。

When to use

  • HO 対象パス(正本: mode-classification.md の 「承認境界周辺の変更」の 12 カテゴリ)を変更する patch を Human に渡すとき
  • scripts/apply-*.sh を新規に書くとき

When not to use

  • 非 HO パスの変更(AI が直接編集して PR にすればよい)
  • patch を作ること自体(それは patch 文書側の仕事。本 skill は適用の型)

Inputs

入力例
patch 文書のパスdocs/working/_reports/1288-ci-layer2-and-push-lane-patch.md
marker 名PG-PATCH-BEGIN / PG-PATCH-END
対象ファイル.github/workflows/sync-plugin-plangate.yml
適用後に成立すべき述語「層 2 の検査が清浄なツリーで空」「ta-71 が 27/0」

Outputs

apply(既定)/ --verify / --rollback の 3 モードを持つ POSIX sh スクリプト 1 本。 commit も push もしない。

Procedure

1. 骨格

#!/bin/sh
set -eu
REPO="${PLANGATE_APPLY_REPO:-/absolute/path/to/repo}"   # 検証用に上書き可能にする
DOC="docs/working/_reports/<name>.md"
TARGET=".github/workflows/<name>.yml"
MODE="${1:-apply}"
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
die() { printf 'ABORT: %s\n' "$*" >&2; exit 1; }
ok()  { printf '  ok  %s\n' "$1"; }
cd "$REPO" || die "repo に入れない: $REPO"

2. 前提の確認

# origin/main から切った feature ブランチを必須にする(main への直接 commit / push は禁止)
br=$(git rev-parse --abbrev-ref HEAD)
case "${br}" in
  HEAD|main|master|release/*) die "feature ブランチで実行すること(現在: ${br})" ;;
esac

# base 検証。origin/main を最新にしてから分岐元を確かめる
git fetch -q origin || die "origin の fetch に失敗"
git merge-base --is-ancestor origin/main HEAD || die "origin/main から分岐していない"

# 未コミット変更。**対象ファイル自身は除外する**(下記の落とし穴 2)
dirty=$(git status --porcelain \
  | grep -v 'docs/working/_audit/skip-decision-log.jsonl' \
  | grep -vF -- "$TARGET" || true)
[ -z "$dirty" ] || { printf '%s\n' "$dirty" >&2; die "未コミットの変更がある"; }

[ -f "$DOC" ] || die "patch 文書が無い: $DOC(feature ブランチの base に patch 文書が含まれていない可能性)"

3. 抽出(awk 方式。落とし穴 3)

awk 'BEGIN{q=sprintf("%c",96)}
     /^<!-- PG-PATCH-BEGIN -->$/{b=1;next}
     /^<!-- PG-PATCH-END -->$/{exit}
     b && substr($0,1,1)==q{f=!f;next}
     f' "$DOC" > "$T/p.patch"
[ -s "$T/p.patch" ] || die "patch 抽出が空: $DOC"
head -1 "$T/p.patch" | grep -q '^diff --git ' \
  || die "抽出結果が unified diff で始まっていない"

4. 適用(fwd と rev の両方を見る。落とし穴 5)

if git apply --check "$T/p.patch" 2>/dev/null; then
  git apply "$T/p.patch" || die "適用に失敗"
elif git apply --check -R "$T/p.patch" 2>/dev/null; then
  printf '既に適用済みです。検証だけ行います。\n'
else
  die "fwd も rev も当たらない(patch が stale か、対象が既に変更されている)"
fi

5. 検証

marker の存在確認だけで終えない。 「適用後に成立すべき述語」を実際に測る。

grep -qF -- "$MARKER" "$TARGET" || die "marker が無い: $MARKER"   # -- 必須(落とし穴 1)
python3 -c "import yaml,sys; yaml.safe_load(open('$TARGET'))" || die "YAML として読めない"
_out=$(sh tests/extras/ta-NN.sh 2>&1 || true)
printf '%s\n' "$_out" | grep -q '\[FAIL\]' && { printf '%s\n' "$_out" | tail -20 >&2; die "ta-NN に FAIL"; }

6. rollback

git apply --check -R "$T/p.patch" 2>/dev/null || die "reverse-apply できない"
git apply -R "$T/p.patch" || die "reverse-apply に失敗"

5-bis. hook の rc を測るときの 3 つの罠

# NG: SKIP_REASON を外すと no-task の「SKIP 拒否」が全ファイルで rc=2 になり、
#     HO block と区別できない。**HO は SKIP より優先**して block される設計なので、
#     SKIP_REASON は設定したままで HO 判定を分離できる。
env -u PLANGATE_SKIP_REASON sh "$HOOK" ...

# NG: パイプの後で $? を取ると hook ではなくパイプの rc になる
printf '...' | sh "$HOOK" 2>&1 | head -1 ; rc=$?

# OK
_rc=0
printf '{"tool_input":{"file_path":"%s"}}' "$path" \
  | PLANGATE_SKIP_REASON="${PLANGATE_SKIP_REASON:-probe}" \
    sh "$HOOK" >/dev/null 2>&1 </dev/null || _rc=$?

検証の前提が成立しているかを先に assert する。 「実 repo の HO が rc=2 であること」を 最初に測り、そうでなければ patch の問題ではなく検証環境の問題 として die すること。 これを入れないと、hook が発火していない環境で「全部 rc=0 だから壊れた」と誤読する。

落とし穴(すべて実際に踏んだもの)

#落とし穴症状対処
1grep -qF "- '...'"BSD grep が - '...' をオプションと解釈し usage: を吐いて検証が落ちるgrep -qF -- "$pat"
2dirty ガードで対象ファイルを除外していない適用後は対象が必ず M になるので、再実行と --rollback が自分のガードで止まるgrep -vF -- "$TARGET" で除外。想定外の手編集は --check が fwd/rev とも通らないことで捕まる
3marker 抽出が sed -e '1d' -e '$d' の 2 回がけmarkdown formatter がfence 長を正規化し marker 前後に空行を入れるため 2 行余るfence 長・空行に依存しない awk
4grep -c ... || printf '0'0 件のとき 0\n0 の二重出力 → [: integer expression expected|| true で受ける
5git apply --check を fwd だけ見るrc=0 でも適用可能とは限らない。同じ形の並びが複数あると offset で別位置に当たり、配線が 2 重になる(実例あり)fwd / rev の両方を測り、git apply -v で offset を確認する
6$var の直後にマルチバイト文字("現在: $br)" 等)bash が br) を識別子に取り込み set -u で br<0xE3>: unbound variable。スクリプトが 1 行目で死ぬ${br} と brace で囲む。日本語メッセージに変数を埋めるなら既定でこれ
7hook の挙動を複製環境で検証しようとする複製では REPO_ROOT の解決が安定せず、同じコマンドで rc=2 と rc=0 が混ざる。(unknown) = HO 判定に到達していない状態も出るhook の挙動検証は実 repo で行う。複製の dry-run は「patch が当たるか / rollback で戻るか」に限定する
8検証 probe の grep パターンにシェル/正規表現のメタ文字が入っている(grep -c '_x="${y#A|B}"' 等)$ { | が正規表現として解釈され常に 0 件。適用済みの patch を「未適用」と誤判定する文字列一致は grep -F。加えて positive control(既知の一致するパターンで 1 件以上返ることを同時に確認)を置く。挙動は grep 実装依存(実測: ugrep 7.8.4 / macOS で BRE=0 件・-F=1 件。GNU grep とは異なりうる)なので、なおさら -F で固定する
9前提 assert 自体の偽陽性を疑わない5-bis の「実 repo の HO が rc=2」assert が rc=0 を返して die したが、同じ probe を手で 3 通り実行すると全て rc=2 だった。patch は正しいのにスクリプトが止まるdie する前に probe の入力(path・stdin・env)をそのまま stderr へ出す。die メッセージは「検証環境の問題」で止まる旨と再現用の 1 行を必ず含める
10probe() をコマンド置換で呼び、関数内で設定した変数を親で読む$( ) はサブシェルなので _reason 等は親に戻らない。set -u 下では unbound variable でスクリプトが検証の途中で死ぬ(実際に Human の実行時に発生)返す値は 1 レコードに畳む(printf '%s|%s' "$_rc" "$_reason" → 親で ${r%%|*} / ${r#*|})。関数の副作用に依存しない
11適用スクリプトを連結実行する(for s in a.sh b.sh; do sh "$s"; done)1 本目が残した差分を 2 本目の dirty ガードが検出して abort する。「未コミットの変更がある」で止まり、2 本目が入らないfeature ブランチで適用 → commit → PR。まとめて回したいなら、各スクリプトの dirty ガードが直前のスクリプトの対象ファイルも除外する設計にする
12workflow patch が、それが gate する PR 自身が追加するファイルを前提にする(例: TASK-1608 の requirements ファイル)HO 側を先に merge するとファイル不在で CI が落ち、PR 側を先に merge すると gate が無い。適用順序がデッドロックする(出典: TASK-1608 L2/L3 レビュー(PR #1611))step に if: hashFiles('<file>') != '' ガードを付け HO 側を先行 merge 可能にする。skip 中は検証済み扱いにしない。hashFiles ガードは sentinel を消すと恒常 skip・CI 緑になる。対照 assert を無条件で置く(ハーネスと requirements の両方が存在することを、ハーネスとは別のテストファイルで検査し、両方同時削除でも FAIL)。HO verify も ハーネスあり + requirements なし で FAIL(出典: PR #1611 L3 M-2 とその限定確認)

Checklist

適用スクリプトを Human に渡す前に:

  • 複製環境で 4 モードすべてを実走(apply / 再実行(冪等)/ --verify / --rollback)
  • --rollback 後に git status --porcelain -- <target> が 0 行
  • grep に -- が付いている
  • dirty ガードが対象ファイル自身を除外している
  • 検証が marker の存在だけでなく述語(テスト・パース・実挙動)を測っている
  • $var の直後にマルチバイト文字が無い(grep -nE '\$[A-Za-z_][A-Za-z0-9_]*[()・「」]' <script> が 0 件)
  • hook の挙動検証は実 repo 前提になっている(複製で測っていない)
  • 検証の grep が -F か、メタ文字を含まない(${...} を含むパターンを BRE で書いていない)
  • 前提 assert が die するとき、probe の入力と再現用 1 行を出力する
  • 関数の戻り値をコマンド置換で受けるとき、必要な値がすべて stdout に乗っている(サブシェルなので変数は親に戻らない)
  • 他の適用スクリプトと連結実行しない前提になっている(feature ブランチで適用 → commit → PR)
  • verify は YAML 解析失敗・解析器不在・依存不足を fail にする(warn で通さない。出典: TASK-1608 L2/L3 レビュー(PR #1611))
  • workflow に step を足すとき、job の timeout-minutes 未設定時は既定 360 分。step timeout 合計 + 既存 step の実測(ばらつきを見込んだ余裕)≤ job timeout。収まらなければ別 job に分ける(必要なら needs と required check 登録を検討) を確認した(出典: 同上)
  • commit も push もしない
  • PLANGATE_APPLY_REPO 等で repo パスを上書きでき、複製で dry-run できる

Quality bar

「--check が rc=0 だった」は検証ではない。 複製環境で実際に適用し、適用後の述語が 成立することと、--rollback で差分 0 に戻ることを実測してから渡す。

Example prompt

docs/working/_reports/1288-ci-layer2-and-push-lane-patch.md の PG-PATCH を .github/workflows/sync-plugin-plangate.yml へ適用するスクリプトを、 ho-apply-script の型で作って。検証は「層 2 の述語が清浄なツリーで空」と 「ta-71 が 27/0」。複製で 4 モード実走してから渡すこと。

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.