Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .claude/commands/ai-loop-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ arbiter 裁定 → exec → rubric grader)。本コマンドは前提確認と

$ARGUMENTS に以下の形式で渡される:

- `run <対象の説明>` — 新しい run を開始(LoopSpec 作成から)
- `run TASK-XXXX` — **Plan-first の正式入口**(TASK ID 必須)。既存の Plan Package
(pbi-input / plan / todo / test-cases + C-1/C-2 evidence)を起点に production run を
開始する。LoopSpec は `scripts/ai-loop/plan_package.py` の `derive_loopspec()` で
Plan Package から決定論派生し、`production: true` + `plan_package` ブロックを
arbiter へ渡す(契約正本: `docs/workflows/ai-loop/c3-prime-contract.md`)。
TASK ID を伴わない自由文だけの `run <説明>` は **production run を開始できない**
(TASK-0872 / #872。Plan Package 束縛の無い run を Wチェックへ進めない)
- `status` — 直近 run の状態・decision record・摩擦台帳の要約を表示
- (引数なし) — 前提チェックのみ実施して結果を報告

Expand Down
154 changes: 111 additions & 43 deletions bin/plangate
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,35 @@ cmd_status() {
fi
}

# c3-prime / legacy c3.json の受理を判別・検証する共有ヘルパー(TASK-0872 PR-2)。
# approvals/c3.json の approval_kind を strict JSON で判別し、c3-prime なら
# scripts/ai-loop/c3prime_verify.py で Plan Package 束縛を全数再検証する
# (契約 §4/§5: legacy grep 経路は c3-prime に流用しない)。
# 第2引数に expected_sha を渡すと source_sha との厳密一致を強制する(exec 時)。
# 戻り値: 0=c3-prime 受理 / 10=legacy(呼び出し側が grep 経路で処理)/ 1=c3-prime NG
_plangate_c3_dispatch() {
_c3d_dir="$1"
_c3d_expected="${2:-}"
_c3d_verify="$plangate_root/scripts/ai-loop/c3prime_verify.py"
if [ ! -f "$_c3d_verify" ]; then
# 検証器が無い環境では c3-prime を受理できない(fail-closed / #889 high)。
# approval_kind キーが物理的に存在しない場合のみ legacy(10) 委譲。キーが
# 存在すれば(値が何であれ・型違い・空文字含む)検証不能として NG(1)。
_c3d_present=$(python3 -c "import json,sys
try: d=json.load(open(sys.argv[1]))
except Exception: print('ERR'); sys.exit(0)
print('YES' if isinstance(d,dict) and 'approval_kind' in d else 'NO')" "$_c3d_dir/approvals/c3.json" 2>/dev/null || echo "ERR")
[ "$_c3d_present" = "NO" ] && return 10
return 1
fi
if [ -n "$_c3d_expected" ]; then
python3 "$_c3d_verify" "$_c3d_dir" "$_c3d_expected" >&2
else
python3 "$_c3d_verify" "$_c3d_dir" >&2
fi
return $?
}

cmd_validate() {
if [ $# -eq 0 ]; then
printf 'Usage: plangate validate <TASK-XXXX> [--mode <mode>] | --dir <path> [--mode <mode>]\n' >&2
Expand Down Expand Up @@ -967,29 +996,42 @@ cmd_validate() {
failures=$((failures + 1))
else
printf ' [PASS] approvals/c3.json exists\n'
c3_status=$(grep '"c3_status"' "$c3_file" 2>/dev/null | sed 's/.*"c3_status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
if [ "$c3_status" = "APPROVED" ]; then
printf ' [PASS] c3_status = APPROVED\n'
else
printf ' [FAIL] c3_status = %s (expected APPROVED)\n' "${c3_status:-missing}"
# TASK-0872 PR-2: c3-prime は Plan Package 束縛を全数再検証(契約 §4)。
# legacy(approval_kind 無し)は従来の grep 経路で処理する。
# set -eu 下で非ゼロ戻り(10=legacy / 1=NG)が中断を招かないよう `|| _c3_rc=$?` で受ける。
_c3_rc=0
_plangate_c3_dispatch "$work_dir" || _c3_rc=$?
if [ "$_c3_rc" = "0" ]; then
printf ' [PASS] c3-prime record verified (decision=AUTO_APPROVED, Plan Package 束縛整合)\n'
elif [ "$_c3_rc" = "1" ]; then
printf ' [FAIL] c3-prime verification failed (see message above)\n'
failures=$((failures + 1))
fi

# plan_hash check
plan_file="$work_dir/plan.md"
if [ -f "$plan_file" ]; then
recorded_hash=$(grep '"plan_hash"' "$c3_file" 2>/dev/null | sed 's/.*"plan_hash"[[:space:]]*:[[:space:]]*"sha256:\([^"]*\)".*/\1/' || echo "")
if [ -z "$recorded_hash" ]; then
printf ' [WARN] plan_hash not found in c3.json\n'
else
# _c3_rc == 10: legacy 経路(従来どおり grep で c3_status / plan_hash 検証)
c3_status=$(grep '"c3_status"' "$c3_file" 2>/dev/null | sed 's/.*"c3_status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
if [ "$c3_status" = "APPROVED" ]; then
printf ' [PASS] c3_status = APPROVED\n'
else
current_hash=$(plangate_sha256 "$plan_file")
if [ "$current_hash" = "$recorded_hash" ]; then
printf ' [PASS] plan.md hash matches c3.json (no post-approval modification)\n'
printf ' [FAIL] c3_status = %s (expected APPROVED)\n' "${c3_status:-missing}"
failures=$((failures + 1))
fi

# plan_hash check
plan_file="$work_dir/plan.md"
if [ -f "$plan_file" ]; then
recorded_hash=$(grep '"plan_hash"' "$c3_file" 2>/dev/null | sed 's/.*"plan_hash"[[:space:]]*:[[:space:]]*"sha256:\([^"]*\)".*/\1/' || echo "")
if [ -z "$recorded_hash" ]; then
printf ' [WARN] plan_hash not found in c3.json\n'
else
printf ' [FAIL] plan.md hash MISMATCH — plan was modified after C-3 approval\n'
printf ' recorded: sha256:%s\n' "$recorded_hash"
printf ' current: sha256:%s\n' "$current_hash"
failures=$((failures + 1))
current_hash=$(plangate_sha256 "$plan_file")
if [ "$current_hash" = "$recorded_hash" ]; then
printf ' [PASS] plan.md hash matches c3.json (no post-approval modification)\n'
else
printf ' [FAIL] plan.md hash MISMATCH — plan was modified after C-3 approval\n'
printf ' recorded: sha256:%s\n' "$recorded_hash"
printf ' current: sha256:%s\n' "$current_hash"
failures=$((failures + 1))
fi
fi
fi
fi
Expand Down Expand Up @@ -2002,19 +2044,43 @@ cmd_exec() {
return 1
fi

c3_status=$(grep '"c3_status"' "$work_dir/approvals/c3.json" 2>/dev/null \
| sed 's/.*"c3_status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
if [ "$c3_status" != "APPROVED" ]; then
printf 'error: C-3 gate not approved (status=%s)\n' "${c3_status:-missing}" >&2
return 1
# TASK-0872 PR-2: c3-prime は Plan Package 束縛を全数再検証(契約 §4)。
# exec は対象 commit(HEAD)に対して実行するため、source_sha と HEAD の一致を強制する。
# legacy(approval_kind 無し)は従来の grep + plan_hash 経路で処理する。
# set -eu 下で非ゼロ戻り(10=legacy / 1=NG)が中断を招かないよう `|| _c3_rc=$?` で受ける。
_exec_head=$(git -C "$plangate_root" rev-parse HEAD 2>/dev/null || echo "")
# c3-prime を exec する場合、HEAD が解決できない環境(非 git)では source_sha
# 照合を skip して受理してはならない(fail-closed / #889 R2 high)。
# approval_kind=c3-prime かつ HEAD 空なら即 BLOCK。
if [ -z "$_exec_head" ]; then
_c3_kind=$(python3 -c "import json,sys
try: d=json.load(open(sys.argv[1]))
except Exception: print(''); sys.exit(0)
print(d.get('approval_kind','') if isinstance(d,dict) else '')" "$work_dir/approvals/c3.json" 2>/dev/null || echo "")
if [ "$_c3_kind" = "c3-prime" ]; then
printf 'error: c3-prime exec requires resolvable HEAD (git rev-parse HEAD failed)\n' >&2
return 1
fi
fi
_c3_rc=0
_plangate_c3_dispatch "$work_dir" "$_exec_head" || _c3_rc=$?
if [ "$_c3_rc" = "1" ]; then
printf 'error: C-3 gate not approved (c3-prime verification failed; see above)\n' >&2
return 1
elif [ "$_c3_rc" = "10" ]; then
c3_status=$(grep '"c3_status"' "$work_dir/approvals/c3.json" 2>/dev/null \
| sed 's/.*"c3_status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
if [ "$c3_status" != "APPROVED" ]; then
printf 'error: C-3 gate not approved (status=%s)\n' "${c3_status:-missing}" >&2
return 1
fi

# Gate check: plan_hash integrity (EH-3 integration at exec entry)
# plan.md must not have been modified after C-3 approval.
plan_file="$work_dir/plan.md"
c3_file="$work_dir/approvals/c3.json"
if [ -f "$plan_file" ]; then
recorded_hash=$(python3 -c "import json,sys
# Gate check: plan_hash integrity (EH-3 integration at exec entry)
# plan.md must not have been modified after C-3 approval.
plan_file="$work_dir/plan.md"
c3_file="$work_dir/approvals/c3.json"
if [ -f "$plan_file" ]; then
recorded_hash=$(python3 -c "import json,sys
try:
d=json.load(open(sys.argv[1]))
except Exception:
Expand All @@ -2023,21 +2089,23 @@ if not isinstance(d,dict):
print(''); sys.exit(0)
v=d.get('plan_hash','')
print(v[7:] if isinstance(v,str) and v.startswith('sha256:') else '')" "$c3_file" 2>/dev/null || echo "")
if [ -n "$recorded_hash" ]; then
if command -v sha256sum >/dev/null 2>&1; then
current_hash=$(sha256sum "$plan_file" | awk '{print $1}')
else
current_hash=$(shasum -a 256 "$plan_file" | awk '{print $1}')
fi
if [ "$recorded_hash" != "$current_hash" ]; then
printf 'error: plan_hash mismatch -- plan.md modified after C-3 approval\n' >&2
printf ' Recorded: sha256:%s\n' "$recorded_hash" >&2
printf ' Current : sha256:%s\n' "$current_hash" >&2
printf ' Action : Re-approval required (update c3.json plan_hash) or revert plan.md.\n' >&2
return 1
if [ -n "$recorded_hash" ]; then
if command -v sha256sum >/dev/null 2>&1; then
current_hash=$(sha256sum "$plan_file" | awk '{print $1}')
else
current_hash=$(shasum -a 256 "$plan_file" | awk '{print $1}')
fi
if [ "$recorded_hash" != "$current_hash" ]; then
printf 'error: plan_hash mismatch -- plan.md modified after C-3 approval\n' >&2
printf ' Recorded: sha256:%s\n' "$recorded_hash" >&2
printf ' Current : sha256:%s\n' "$current_hash" >&2
printf ' Action : Re-approval required (update c3.json plan_hash) or revert plan.md.\n' >&2
return 1
fi
fi
fi
fi
# _c3_rc == 0: c3-prime 受理(Plan Package 束縛整合済み)

# Record session start
ts=$(plangate_now)
Expand Down
6 changes: 6 additions & 0 deletions docs/working/TASK-0872/patches/ho-apply-approval.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ sh scripts/sync-plugin-plangate.sh && sh tests/run-tests.sh # 非退行を
未適用の間、c3-prime artifact は `bin/plangate` の legacy grep 経路で `c3_status`
不在により **FAIL**(受理されない)。schema-validate も #887 F-8 により
schema 不在で **ERROR**。いずれも安全側で、Shadow Config は発生しない。

## 適用記録(H-3 完了)

- **適用**: 2026-07-22 Human が対話実行(`git apply bin-plangate.patch` + `.new` 3 件 cp・APPLY OK 確認)
- **検証(オーガナイザー実測)**: 4 ファイルとも patches/ 完成形と byte 一致(cmp exit 0)/ sync dry-run drift 0 / `tests/run-tests.sh` **411 passed / 0 failed・exit 0**(TA-55 HO 全鎖 SKIP→PASS: c3-prime 受理・改竄 reject・legacy 委譲・未知 approval_kind fail-closed)
- 注: 未 commit 状態での再実行時に 1〜3 件の揺れを観測(既知の TC-05 系偽陽性・commit で解消するパターン)。クリーン 1 回実行の exit 0 を正とする
8 changes: 7 additions & 1 deletion plugin/plangate/commands/ai-loop-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ arbiter 裁定 → exec → rubric grader)。本コマンドは前提確認と

$ARGUMENTS に以下の形式で渡される:

- `run <対象の説明>` — 新しい run を開始(LoopSpec 作成から)
- `run TASK-XXXX` — **Plan-first の正式入口**(TASK ID 必須)。既存の Plan Package
(pbi-input / plan / todo / test-cases + C-1/C-2 evidence)を起点に production run を
開始する。LoopSpec は `scripts/ai-loop/plan_package.py` の `derive_loopspec()` で
Plan Package から決定論派生し、`production: true` + `plan_package` ブロックを
arbiter へ渡す(契約正本: `docs/workflows/ai-loop/c3-prime-contract.md`)。
TASK ID を伴わない自由文だけの `run <説明>` は **production run を開始できない**
(TASK-0872 / #872。Plan Package 束縛の無い run を Wチェックへ進めない)
- `status` — 直近 run の状態・decision record・摩擦台帳の要約を表示
- (引数なし) — 前提チェックのみ実施して結果を報告

Expand Down
Loading