From facdd4ec588ea7918c788ac0beeacb327526bd67 Mon Sep 17 00:00:00 2001 From: mine_take Date: Wed, 22 Jul 2026 09:36:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(TASK-0872):=20H-3=20HO=20patch=204=20?= =?UTF-8?q?=E4=BB=B6=E3=82=92=20Human=20=E9=81=A9=E7=94=A8=20=E2=80=94=20c?= =?UTF-8?q?3-prime=20=E5=8F=97=E7=90=86=E9=85=8D=E7=B7=9A=20+=20run=20?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20Plan-first=20=E5=8C=96=20+=20schema=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bin/plangate: validate/exec preflight に _plangate_c3_dispatch(c3-prime 受理・legacy 経路 byte 非変更) - .claude/commands/ai-loop-workflow.md + plugin 対: run 入口 TASK-XXXX 必須化(byte 同一) - schemas/c3-prime.schema.json: cross-field 制約込み新設 - 適用は Human 対話実行(HO 常時 block 遵守)・検証 411 passed/0 failed・承認記録は patches/ho-apply-approval.md Refs #872 #887 Co-Authored-By: Claude Fable 5 --- .claude/commands/ai-loop-workflow.md | 8 +- bin/plangate | 154 ++++++++++----- .../TASK-0872/patches/ho-apply-approval.md | 6 + plugin/plangate/commands/ai-loop-workflow.md | 8 +- schemas/c3-prime.schema.json | 178 ++++++++++++++++++ 5 files changed, 309 insertions(+), 45 deletions(-) create mode 100644 schemas/c3-prime.schema.json diff --git a/.claude/commands/ai-loop-workflow.md b/.claude/commands/ai-loop-workflow.md index 625eb6ec..c3e8cad3 100644 --- a/.claude/commands/ai-loop-workflow.md +++ b/.claude/commands/ai-loop-workflow.md @@ -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・摩擦台帳の要約を表示 - (引数なし) — 前提チェックのみ実施して結果を報告 diff --git a/bin/plangate b/bin/plangate index d974cb60..8fba9115 100755 --- a/bin/plangate +++ b/bin/plangate @@ -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 [--mode ] | --dir [--mode ]\n' >&2 @@ -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 @@ -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: @@ -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) diff --git a/docs/working/TASK-0872/patches/ho-apply-approval.md b/docs/working/TASK-0872/patches/ho-apply-approval.md index deadd410..b077c2cb 100644 --- a/docs/working/TASK-0872/patches/ho-apply-approval.md +++ b/docs/working/TASK-0872/patches/ho-apply-approval.md @@ -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 を正とする diff --git a/plugin/plangate/commands/ai-loop-workflow.md b/plugin/plangate/commands/ai-loop-workflow.md index 625eb6ec..c3e8cad3 100644 --- a/plugin/plangate/commands/ai-loop-workflow.md +++ b/plugin/plangate/commands/ai-loop-workflow.md @@ -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・摩擦台帳の要約を表示 - (引数なし) — 前提チェックのみ実施して結果を報告 diff --git a/schemas/c3-prime.schema.json b/schemas/c3-prime.schema.json new file mode 100644 index 00000000..18394c5c --- /dev/null +++ b/schemas/c3-prime.schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/s977043/plangate/schemas/c3-prime.schema.json", + "title": "C-3' (c3-prime) Approval Record", + "description": "Machine-readable C-3' arbiter decision record bound to a Plan Package (approvals/c3.json with approval_kind=c3-prime). Contract: docs/workflows/ai-loop/c3-prime-contract.md", + "type": "object", + "required": [ + "task_id", + "approval_kind", + "phase", + "decision", + "source_sha", + "plan_hash", + "plan_package_hash", + "artifact_hashes", + "c1_evidence_ref", + "c2_evidence_ref", + "reviewers", + "policy_ref", + "issued_at", + "issued_by" + ], + "properties": { + "task_id": { + "type": "string", + "pattern": "^TASK-[0-9]{4}$" + }, + "approval_kind": { + "type": "string", + "const": "c3-prime" + }, + "phase": { + "type": "string", + "const": "C-3'" + }, + "decision": { + "type": "string", + "enum": [ + "AUTO_APPROVED", + "HUMAN_ESCALATED", + "BLOCKED" + ] + }, + "source_sha": { + "type": "string", + "pattern": "^[0-9a-f]{7,40}$", + "description": "Commit SHA observed by the W-check; must equal arbiter target_sha at issue time (R-011)" + }, + "plan_hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 of plan.md alone (same contract as legacy c3-approval / EH-3)" + }, + "plan_package_hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 of the canonicalized artifact_hashes JSON (sort_keys, compact separators)" + }, + "artifact_hashes": { + "type": "object", + "required": [ + "pbi-input.md", + "plan.md", + "todo.md", + "test-cases.md", + "review-self.md", + "review-external.md" + ], + "properties": { + "pbi-input.md": { "$ref": "#/$defs/sha256" }, + "plan.md": { "$ref": "#/$defs/sha256" }, + "todo.md": { "$ref": "#/$defs/sha256" }, + "test-cases.md": { "$ref": "#/$defs/sha256" }, + "review-self.md": { "$ref": "#/$defs/sha256" }, + "review-external.md": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": false + }, + "c1_evidence_ref": { + "type": "string", + "minLength": 1 + }, + "c2_evidence_ref": { + "type": "string", + "minLength": 1 + }, + "reviewers": { + "type": "object", + "required": ["model_a", "model_b"], + "properties": { + "model_a": { "$ref": "#/$defs/reviewerSnapshot" }, + "model_b": { "$ref": "#/$defs/reviewerSnapshot" } + }, + "additionalProperties": false + }, + "policy_ref": { + "type": "string", + "minLength": 1 + }, + "issued_at": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" + }, + "issued_by": { + "type": "string", + "minLength": 1 + }, + "derived_loopspec_hash": { + "$ref": "#/$defs/sha256", + "description": "Optional: SHA-256 of the canonicalized LoopSpec derived per contract section 6 (AC-10 reproducibility)" + } + }, + "patternProperties": { + "^_": { + "type": "string", + "description": "Annotation-only keys (same convention as c3-approval.schema.json). c3_status is intentionally NOT allowed (contract section 5)." + } + }, + "additionalProperties": false, + "allOf": [ + { + "$comment": "Contract sections 2/4 (#887 review): AUTO_APPROVED requires both reviewer verdicts to be approve. Cross-field constraint so a tampered AUTO_APPROVED+reject record fails schema validation, not only the PR-2 strict-JSON check.", + "if": { + "properties": { + "decision": { "const": "AUTO_APPROVED" } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "reviewers": { + "properties": { + "model_a": { + "properties": { "verdict": { "const": "approve" } } + }, + "model_b": { + "properties": { "verdict": { "const": "approve" } } + } + } + } + } + } + } + ], + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "reviewerSnapshot": { + "type": "object", + "required": [ + "verdict", + "plan_hash", + "source_sha", + "plan_package_hash", + "evidence_ref" + ], + "properties": { + "verdict": { + "type": "string", + "enum": ["approve", "reject"] + }, + "plan_hash": { "$ref": "#/$defs/sha256" }, + "source_sha": { + "type": "string", + "pattern": "^[0-9a-f]{7,40}$" + }, + "plan_package_hash": { "$ref": "#/$defs/sha256" }, + "evidence_ref": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + } +}