運用ルールを実行に変える方法――新規項を止めるゲートの実測

スマートホームを描いたアイキャッチ画像

新しい運用ルールに執行形:がなければ、書き込みを止めるようにしました。固定fixtureでは、マーカーなしの新規項はblocked、同じ項にマーカーを加えるとpassになっています。

これは、学びを文章に残す習慣はあるのに、その学びを次の修正へ接続できず、未実行のまま積み上がる運用者のための小さな関所です。読み終えるころには、どこまでを機械的に止め、どこからを人やエージェントの判断へ残すかを、自分の運用に合わせて再現・判断できるはずです。

某所ラボでは初回tickで新規17件をキューへ追加し、修正便を1件投入、未執行26件を記録しました。ただし現在は日次運転ではありません。2026年8月18日以降、判断と投入は月曜10:20の週次、7日間に最大1回・最大1件というhard capで動かしています。

目次

「書いた」を完了にしないため、入口で新規項だけを止めます

規範に一項加えた時点で、実行先が何もないことがあります。後から監査で拾う方法だけでは、新しい不足は次の監査まで残ります。そこでcommit前に、直前版との差分から新規の項番号を取り、追加された行に執行形:がなければ終了コード1で拒否するゲートを置きました。

ここで固定コードに任せるのは、意味の正しさではなく構造です。マーカーがあることと、そのマーカーが指す機構が本当に役立つことは別の問題です。後者まで正規表現へ押し込むと、ルールはすぐに読めない例外集になります。

段階 固定側が確認すること 判断に残すこと
書き込み時ゲート 新規項の有無、執行形:の有無 執行形の内容が妥当か
事後監査 入力収集、結果形式、状態保存 未執行項、重複、古い参照の評価
修正便の投入 選択数が1件、選択項目がキューにあること 次に直すべき1件
着地確認 失敗回数と状態遷移 2回目の失敗後にどう扱うか
図1: 構造ゲートと週次監査の境界・7日間に最大1判断、最大1投入

図1の要点は、固定側が「未執行を意味で決めない」ことです。新規項の入口は止めますが、次に取り組むべき項目は、日本語のミッションと実物を読んで選びます。

実行形の有無と内容の妥当性は、別の検査に分けます

この分離は、自己申告をそのまま成果にしないためでもあります。以前に扱ったAIエージェントの自己申告を検証する方法と同じく、「書いた」「設定した」という文言ではなく、参照先や着地を確認対象にします。

後段の監査は、規範ファイル、各領域のcontext、ローカルskillの説明を実物から発見し、執行形の参照先、規範の重複、生の設定値の複写、存在しない参照を点検します。入口のマーカー検査は、この監査を置き換えるものではありません。新しい不足を早く止めるための前段です。

許可したものだけを通す考え方は、前回の名指しで選ぶ統合関所ともつながっています。今回はさらに手前で、「実行先のない新規項」を通さないようにしました。

固定fixtureで、拒否・選択・再試行停止を再現します

実運用のstateやGit indexに接続しない、標準ライブラリだけのstandaloneです。Python 3.10以降が必要で、networkや外部packageは不要です。Pythonから起動するため、ファイルに実行権限がなくても構いません。

ただし、これは構造ゲートとキュー境界の再現です。「何が未執行か」という意味判断はfixtureに置き換えています。執行形:が存在しても、参照先の実在や内容の正しさまでは保証しません。

#!/usr/bin/env python3
"""Replay the structural write gate and one-item queue boundary offline."""

from __future__ import annotations

import argparse
import json
import re
from dataclasses import asdict, dataclass


ITEM_RE = re.compile(r"^([0-9]+[a-z]{0,2})\.\s+\*\*", re.M)
MARKER_RE = re.compile(r"執行形.{0,4}[::]")


@dataclass
class QueueItem:
    item_id: str
    status: str
    retry_count: int = 0
    last_failure: str = ""


def check_new_items(staged: str, head: str, added_lines: list[str]) -> dict[str, object]:
    """Apply the production hook's structural marker check to supplied text."""
    new_items = sorted(set(ITEM_RE.findall(staged)) - set(ITEM_RE.findall(head)))
    marker_present = any(MARKER_RE.search(line) for line in added_lines)
    blocked = bool(new_items) and not marker_present
    return {
        "new_items": new_items,
        "marker_present": marker_present,
        "gate": "blocked" if blocked else "pass",
    }


def select_named_queued(queue: list[QueueItem], selected_id: str) -> QueueItem | None:
    """Accept at most one named item, and only while it is queued."""
    matches = [item for item in queue if item.item_id == selected_id and item.status == "queued"]
    return matches[0] if matches else None


def apply_failure(item: QueueItem, reason: str) -> None:
    """Return the first failure to the queue; stop for a decision after the second."""
    if item.retry_count < 1:
        item.status = "queued"
        item.retry_count += 1
    else:
        item.status = "pending_owner"
    item.last_failure = reason


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--demo", action="store_true", help="run the fixed offline fixture")
    args = parser.parse_args()
    if not args.demo:
        parser.error("--demo is required")

    head = "0c. **Existing rule**\n    執行形: existing mechanism\n"
    without_marker = "35. **Observed failure must trigger recovery**\n"
    with_marker = without_marker + "    執行形: fixed recovery mechanism\n"

    print(json.dumps({
        "case": "new-item-without-marker",
        **check_new_items(without_marker, head, without_marker.splitlines()),
    }, ensure_ascii=False, sort_keys=True))
    print(json.dumps({
        "case": "new-item-with-marker",
        **check_new_items(with_marker, head, with_marker.splitlines()),
    }, ensure_ascii=False, sort_keys=True))

    queue = [
        QueueItem("rule-audit-b-12", "queued"),
        QueueItem("rule-context-schema-3", "awaiting_integration"),
        QueueItem("rule-skill-bloat-7", "completed"),
    ]
    picked = select_named_queued(queue, "rule-audit-b-12")
    if picked is None:
        raise RuntimeError("fixture selection unexpectedly failed")
    print(json.dumps({
        "case": "one-item-boundary",
        "queue_items": len(queue),
        "selected": picked.item_id,
        "selected_count": 1,
    }, ensure_ascii=False, sort_keys=True))

    picked.status = "submitted"
    apply_failure(picked, "task branch push rejected")
    print(json.dumps({
        "case": "first-failure", "item": asdict(picked)}, ensure_ascii=False, sort_keys=True))

    picked.status = "submitted"
    apply_failure(picked, "task branch push rejected")
    print(json.dumps({
        "case": "second-failure", "item": asdict(picked)}, ensure_ascii=False, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

保存名をe3-enforcement-demo.pyとして、次の1回だけ実行します。

$ PYTHONDONTWRITEBYTECODE=1 python3 e3-enforcement-demo.py --demo
2026-08-25 00:19:39 JSTの実行出力を開く

Linux x86_64 / Python 3.14.4 のoffline task worktreeで実行し、終了コードは0でした。

{"case": "new-item-without-marker", "gate": "blocked", "marker_present": false, "new_items": ["35"]}
{"case": "new-item-with-marker", "gate": "pass", "marker_present": true, "new_items": ["35"]}
{"case": "one-item-boundary", "queue_items": 3, "selected": "rule-audit-b-12", "selected_count": 1}
{"case": "first-failure", "item": {"item_id": "rule-audit-b-12", "last_failure": "task branch push rejected", "retry_count": 1, "status": "queued"}}
{"case": "second-failure", "item": {"item_id": "rule-audit-b-12", "last_failure": "task branch push rejected", "retry_count": 1, "status": "pending_owner"}}
fixtureのケース 観測値 読み取り
新規項、マーカーなし gate=blocked、新規項は35 新しい学びだけを入口で止める
同じ新規項、マーカーあり gate=pass 内容評価は後段へ渡す
3件のキュー 選択数1 一度に広げない
同一便の2回目の失敗 pending_owner 自動反復をそこで止める
図2: standalone実走の状態遷移・3件から選択は1件、2回目の失敗で判断待ち

図2では、最初の失敗がqueuedへ戻る一方、同じ便の2回目はpending_ownerへ移っています。失敗を見なかったことにして繰り返す側はありません。この境界があるため、1件の修正便が広がり続けるのを防げます。

週次hard capは、遅さではなく観測の余白です

図解: 週次hard capの観測余白(平日日次 / 2026年8月18日の監査後 / 月曜10:20 / 7日間 / 次の判断前)
図解: 週次hard capの観測余白(AI生成)

当初は平日日次に1件ずつ消化する説明でした。しかし、2026年8月18日の監査後は週次へ縮退しました。現行条件は、月曜10:20に実判断と投入を行い、7日間で最大1判断・最大1投入です。本線へ統合せず、task branchへのcommitとpushで止めます。

この変更で、キューを速く減らすことよりも、選んだ1件が本当に着地したかを次の判断前に確認するほうを優先しました。自動化は、すべてを自動で決めることではありません。確認しなければならない場所を、減らしすぎないことでもあります。

入口のマーカーを、成果保証として読まないでください

注意: 執行形:は「実行先を添えた」という構造上の合図です。参照先が存在すること、規範が重複しないこと、設定値が不用意に複写されていないことまでは証明しません。必ず後段監査と着地確認を残してください。

また、standaloneは隔離された固定fixtureです。実運用の状態やGit indexを変更しません。実際の規範監査がどの項目を未執行と判断するかは、ここに示した正規表現からは導けません。

次は、落とし先まで検証できる1件だけを選びます

現物は、入口で新しい学びを止めるゲートと、通過後の中身を週次で点検する監査の二段で動いています。一度に修正するのは最大1件で、失敗を無限に回すこともありません。

運用ルールを増やす前に、次の新規項へ執行形:を添え、その参照先を後段で確認できるかを書き出してください。まだ1件を安全に着地させる経路がないなら、ルールを増やさず様子を見る判断も妥当です。

この検証を回している環境

この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。

ラボ構成のまとめを見る →
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次