Skip to content

feat(hooks): judge session changes with jev-lint model rules at Stop - #150

Open
furedea-agent[bot] wants to merge 9 commits into
mainfrom
feat/74-stop-model-rules
Open

furedea-agent[bot] wants to merge 9 commits into
mainfrom
feat/74-stop-model-rules

Conversation

@furedea-agent

@furedea-agent furedea-agent Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Stop 時に,セッションの変更を jev-lint review のモデルルール(M-05,M-21,M-60)で採点する.既存の検証 Stop フローに統合し,指摘は「未採点の入力に対して 1 回だけ block,同じ入力では通知のみ」とする.ADR-0021 をこれに合わせて改訂した.

判断してほしい点:下記「ADR の想定と違った点」(jev-lint review は 1 回で「開始 HEAD からの作業ツリー差分」を採点できない).

挙動

状況 結果
未採点の入力で指摘あり {"decision":"block","reason":...} を 1 回(エージェントに見える)
同じ入力で指摘あり systemMessage のみ(findings unchanged; not rerun),再 block しない
stop_hook_active が立っている その Stop では systemMessage のみ.見せられなかった指摘は「未提示」として記録し,同じ入力の次の Stop で 1 回 block
モデルルールが block reason は「検証の報告,続けてモデルルールの指摘」
AGENT_MODEL_RULES=off 保存済みレシートより先に判定し,常に 1 行の通知のみ
キルスイッチ / 信頼されない repo / Keychain 項目なし / op・jev-lint なし / timeout / 非 0 終了 / 読めない JSON systemMessage の 1 行のみ,block しない,レシートを残さず次の Stop で再試行
検証が block モデルルールは走らせない

統合の理由(別 hook にしなかった)

  • 別 Stop hook は検証と並列に走り,「同じ入力では block しない」を 1 か所で適用できず,エージェントに見せるには 2 つ目の block が要る.
  • そのため session_gate.stop() が検証の後に model_rules_gate.advise() を呼び,results.json のレシート(model_rules)を同じ流儀で扱う.verify の明示コマンドは従来どおり検証のみ.

ADR の想定と違った点(実機で確認)

ADR-0021 は「開始 HEAD に対する作業ツリーの差分を 1 回の jev-lint review --base で」と書いていたが,jev-lint 0.7.0 の dist/diff.js を読み,実機(キー不要の --dry-run)で確かめたところ次のとおりだった.

  • --base <commit> は <commit>...HEAD(コミット済みのみ).ステージ済み・未コミット・未追跡は対象外.
  • --base なしは「作業ツリー vs HEAD」+未追跡ファイル(未コミットのみ).

そこで 2 回のレビューに分けた(コミット済み:--base <開始 HEAD>,未コミット:--base なし).上限 20 は 2 つで共有し,同じ rule・file・line の指摘は 1 件にまとめる.開始 HEAD は baseline.json の head に記録する(古い記録や空 repo では未コミットのみ).ADR にこの理由を書いた.テスト test_model_rule_files.py が両モードの対象を固定している.

process 境界

  • jev-lint review --json --no-config --fail-on error -R <rules> -c <cache> [--base <commit>] を op run --environment <id> -- の下で実行.件数の事前計測は --dry-run(キー不要,op なし).
  • ブリーフに無い追加:--no-config(repo の .jev-lint.yaml が送信先 --base-url を変えるのを防ぐ),--fail-on error(警告の指摘で非 0 終了にならないため).
  • 子プロセスの環境は許可リスト(PATH,HOME,USER,LANG,LC_ALL,XDG_STATE_HOME,TMPDIR)のみ.トークンは op の子にだけ渡す.stderr は捨てる.token と key は読まず・出さず・ログに残さない(Credentials.__repr__ も隠す).
  • timeout 60 秒は process group ごと kill.予算は min(90, 600 - 20 - 検証が使った時間) で,残りが無ければ実行しない(Keychain 検索は 5 秒).
  • 孤児プロセスと,その上限: Claude Code が hook を強制終了する(timeout,Ctrl+C,セッション終了)と,hook は後始末できず,macOS には親の死を子に伝える仕組みも無いため,op と jev-lint が残りうる.そこで子プロセスを /usr/bin/timeout -s TERM -k 5 <予算> の下で起動し,hook が死んでも自分の期限で終わるようにした(--dry-run を含む全ての子).hook 内の process group kill は,その 1 秒後の保険として残す.
    • 上限:予算(最大 60 秒)+ 5 秒.
    • timeout の解決: /usr/bin/timeout を絶対パスで呼ぶ(PATH に左右されず,PATH 上の偽物に差し替えられない).macOS は BSD 版,Linux は GNU 版が同じ場所にある.両方が受け付けるものだけを使う(短いオプション -s TERM -k 5,期限は小数の秒.BSD 版の man page で確認).--version の判定(probe)はやめた./usr/bin/timeout が無ければ,通知なしで hook 内の kill だけに戻る(その場合,hook が殺されると子は自分で終わるまで残る).
    • 終了状態: 124(期限切れ),137(GNU が KILL を送った後),-9(BSD の timeout は自分が属する process group に KILL を送るため,自身がシグナルで死に,Python では負の値で見える)をすべて「timed out」として通知する.
    • 実測(macOS,BSD timeout,キー不要): hook を SIGKILL したあと,TERM を受け付ける子は 2.00 秒後(予算 2 秒),TERM を無視する子は 2.96 秒後(予算 2 秒+ -k 1)に終了した._Child.run 経由では,TERM を無視する子が予算 1 秒+ -k 5 の 6.01 秒で終わり,「timed out」と判定された.
  • 未コミットのレビューは,baseline と内容が異なるファイル(このセッションが変えた・作ったファイル)だけを渡す.最初から dirty で触っていないファイルは送らない.
  • 既知の限界(コミット済みのレビュー): --base <開始 HEAD> は start...HEAD 全体を採点する.セッション中に git pull や rebase で取り込んだ上流のコミットも対象になり,共有上限 20 を使いうる.baseline は内容のスナップショットで,どのコミットがセッション由来かを区別できないため,この PR では解消しない(claude-review の P3,返信済み).
  • 件数上限:--dry-run の subjects と cached で,キャッシュ済みを除いて 20 以下ならそのまま.超える場合はファイル単位で貪欲に採り,残りを報告.

ルール(Python / TypeScript)

agents/hooks/lint/model-rules/<language>/<id>/rule.yml(lint/rules の ast-grep 静的ルールとは別ディレクトリ).しきい値 0.5 は未調整の仮値で,助言用.Go と Rust は後続.

ルール Python の matcher TypeScript の matcher
command-query-naming(M-05) function_definition function_declaration,method_definition
error-message-names-operation(M-21) function_definition かつ raise_statement を含む 同左の関数かつ throw_statement を含む
known-defect-marker-names-removal(M-60) decorated_definition で decorator が xfail call_expression で test.fails / it.fails
  • M-60 は注釈がデコレータ(呼び出し)の外にあるため state: located(ファイル全体を送る).他は local.
  • 矢印関数は M-05 の対象外(今は拾わない).
  • 散文ルールは削除していない(昇格前).散文→チェックの対応表:M-05(AGENTS.md の命名),M-21(python-style のエラーメッセージ),M-60(python-style testing の xfail 注釈)はいずれも散文を残したまま助言チェックを追加.

キャッシュ書き込みの guard とその限界

secret_path_policy.json に **/agent-harness/model-rules/** を追加(テストあり).この方針は access 種別を区別せず,パスを名指しする呼び出しを拒否する.限界:リテラルのパスしか見ない.実行時にパスを組み立てるコマンドや,書き込むプログラムは対象外で,sandbox は state ディレクトリ配下への書き込みを許している.偶発的・安易な編集への対策であり,意図的な書き換えは防げない(ADR と DESIGN に明記).

新しい lint 規則との関係 / ignore の削除

なし.

検証

  • uv run --frozen pytest:4642 passed,1 skipped(最終コミット 76bdf08 で実行)
  • timeout まわりは Red 先行:fake を絶対パスに置き,argv(-s TERM -k 5 <秒>)と終了状態(124,137,-9)のテストを先に変えて,期待どおりの理由で失敗するのを確認してから実装した.file-length(500 行)に当たったため,テストを「判断」と「プロセス境界」の 2 ファイルに分け,共通の準備を model_rules_support.py に出した(テスト数は同じ).
  • CI:codex-review 以外すべて通過(claude-review は最新コミットで P0/P1 なし).codex-review は Codex の利用上限("You have reached your Codex usage limits")で失敗しており,共通ルールどおり許容(ready, codex skipped: usage limit).all-green はその集約として赤.
  • claude-review の指摘はすべて対応済み(P1: stop_hook_active で見せられなかった指摘,P2: Stop 予算,キルスイッチの順序,未触のファイルの送信,検証報告の欠落).セキュリティレビューの「レシート偽造」は,検証の既存レシートと同じ信頼モデルのため範囲外とし,コメントで回答(別 Issue 候補).audit の deptry は known_first_party に 5 モジュールを追加して解消.
  • ruff check,ruff format --check,ty check:すべて通過
  • 新規テスト(fake の security / op / jev-lint を PATH に置く.Keychain・1Password・モデル・ネットワーク不使用,キー不使用):
    • block 1 回,同入力は通知のみ,変更入力で再 block,stop_hook_active,clean,対象 0 件
    • argv と環境の完全一致,token は op の env だけ,許可リスト外の変数なし,token が出力に出ない
    • 件数上限(ファイル単位,キャッシュ済み除外,2 レビューで共有),キルスイッチ,信頼されない repo は何も読まない・送らない,Keychain 欠落,プログラム欠落,非 0 終了,timeout,読めない出力,ロック競合,一度失敗しても次で再試行
    • 開始 HEAD の記録(空 repo,旧記録,不正な値)
    • 実 jev-lint review --dry-run で rule の読み込みと subject の選択(Python 4/1/1,TypeScript 3/1/1),コミット済み・未コミット・未追跡の扱い
  • 未検証:モデルを実際に呼んだ結果(キーがないため).--json の findings の形は dist/types.d.ts に合わせた fake で確認しており,実際の出力は新しいマシンの準備(docs/DESIGN.md)の後に一度確認が必要.

TSDD に関する正直な注記

実装を先に書き,既存 3602 件が通ることを確認してからテストを書いた(Red を先に取っていない).その代わり,fake で境界の契約(argv・env・秘密の非露出)を固定し,実 jev-lint で matcher を確認した.ミューテーションテストは未実施.

ADR の改訂

「指摘は block しない」を「未採点の入力に 1 回だけ block,同じ入力では二度と block しない」に置き換えた.理由:Stop の非 block 出力(systemMessage)はユーザーにしか届かず,エージェントが指摘を見られない.毎回 block するとエージェントが指摘を断った場合に同じ入力で止められ続ける.Environment ID と token はともに Keychain から読む(ブリーフに従い,ADR の AGENT_MODEL_RULES_OP_ENVIRONMENT を置き換え).

新しいマシンの準備

docs/DESIGN.md と docs/DESIGN_ja.md の「Stop 時のモデルルール」に記載.

スコープ・出典・前提

🤖 Generated with Claude Code

furedea and others added 3 commits November 11, 2026 04:34
Model rules judge the commits made since the session started.

Registration therefore keeps the starting commit in baseline.json.

A record without one, or an empty repository, has none.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Run jev-lint review under op after the verification passes, for repositories of trusted owners.

Findings on inputs not judged before block Stop once; the same inputs only notify.

Anything that prevents a run is a one-line message and never blocks.

Add Python and TypeScript rule files for M-05, M-21 and M-60, and a guard on the verdict cache.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he setup

Replace never-block with one block per set of inputs, as the verification receipts do.

Record that jev-lint review needs two reviews.

Record that the Environment ID and the token come from the Keychain.

Document the one-time setup of a new machine in DESIGN.md and DESIGN_ja.md.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 10, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-11T07:40:56.975926Z
We couldn’t complete this review. Comment @codex review to try again.
76bdf08 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread agents/hooks/lib/model_rules_gate.py Outdated
_save(record, state, {"input": inputs, "outcome": "clean", "summary": message})
return Advice(message)
message = model_rules_report.describe(outcome)
_save(record, state, {"input": inputs, "outcome": "findings", "summary": message})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — If the first run happens while stop_hook_active is set, the agent never sees the findings.

The "findings" receipt is saved even when stop_hook_active stops the block, so the findings only go out in a systemMessage, which only the user sees. On the next Stop with the same inputs, _reused matches the receipt and returns notify-only.

This is a common path:

  1. Stop 1: verification blocks, so model rules don't run.
  2. The agent fixes the problem.
  3. Stop 2 (stop_hook_active=true): verification passes, model rules run, and they find something.
  4. The findings appear only in systemMessage, the receipt is saved, and the agent stops.
  5. Every later Stop on these inputs is "unchanged; not rerun".

So the main promise ("findings on unjudged inputs block once so the agent sees them") fails right after any verification block.

The receipt already stops the same inputs from blocking twice, so suppressing the block under stop_hook_active isn't needed to prevent a loop. Either block anyway, or record that the findings were not yet shown (e.g. "outcome": "findings-unshown") so the next Stop without stop_hook_active blocks once. The test test_the_stop_hook_blocks_through_the_provider_entry_point only covers the case where the receipt came from a non-active Stop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 は妥当でした.912f298 で修正しました.stop_hook_active により block できなかった指摘は,レシートに「findings-unshown」として記録し,同じ入力の次の(active でない)Stop で 1 回だけ block します.その後は shown に変わり,通知のみです.active のままの Stop では何度でも通知のみで,ループしません.テスト test_findings_that_could_not_block_because_the_stop_hook_was_active_block_once_at_the_next_stop と test_unseen_findings_still_do_not_block_while_the_stop_hook_stays_active を追加しました.

Comment thread agents/hooks/lib/session_gate.py Outdated
if result.get("decision") == "block":
return result
with record.lock(unbounded=True):
advice = model_rules_gate.advise(record, stop_hook_active=stop_hook_active)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The model-rules budget does not account for time verification already used.

advise() is called with the default BUDGET_SECONDS = 90, measured from when advise starts. Verification can use up to GATE_BUDGET_SECONDS = 540, and the Stop hook timeout in agents/hooks.json is 600s. In the worst case that is 540 + 90 = 630s, plus up to 2×10s of Keychain lookups that are not bound by the deadline. That can exceed the hook timeout.

When the provider kills the hook:

  • No receipt is saved.
  • The op/jev-lint children survive, because they were started with start_new_session=True and are not in the hook's process group.

The PR description says the budget is limited by the remaining Stop time, but nothing passes that value in. Suggest passing budget_seconds=min(BUDGET_SECONDS, HOOK_TIMEOUT - elapsed - margin) from the time the Stop began, and adding a test for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 は妥当でした(PR 本文に書いた残り時間の上限が未実装でした).912f298 で修正しました.session_gate.stop が Stop 開始からの経過を測り,モデルルールの予算を min(90, 600 - 20 - 経過) にして渡します.残りが無ければ実行しません.Keychain 検索の timeout も 10 秒から 5 秒にし,超過がマージン 20 秒に収まるようにしました.テスト test_the_model_rules_get_only_the_time_the_stop_hook_has_left と test_no_model_rules_run_when_the_stop_hook_has_no_time_left を追加しました.なお,プロバイダが hook を強制終了した場合に op と jev-lint の process group が残る点は,予算内で終わる前提のためこの PR では扱いません(jev-lint 自体は API 呼び出しの上限で終わります).

@github-actions

Copy link
Copy Markdown

Review summary

Blocking (P0/P1): 1

  • P1 agents/hooks/lib/model_rules_gate.py:59: findings found while stop_hook_active is set never reach the agent. The receipt is saved anyway, so later Stops on the same inputs only notify. This happens on the common path where verification blocks, the agent fixes it, and the next Stop finds model-rule findings.
  • P2 agents/hooks/lib/session_gate.py:73: the model-rules budget is a flat 90s and ignores time verification already used. Worst case is 540s + 90s + Keychain lookups, which exceeds the 600s hook timeout. A killed hook also leaves the op/jev-lint process group running.

Everything else checked out:

  • argv and environment allowlist
  • token kept out of repr
  • --prefixed path filtering
  • the cap shared across the two reviews
  • trusted-owner gating before any Keychain read
  • the secret-path policy entry matching the cache location

@furedea-agent

furedea-agent Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

自動セキュリティレビューの指摘(モデルルールのレシートは results.json にあり,エージェントが書き換えうる)への回答.

  • 事実としては正しい.results.json の model_rules を偽造すると,「clean」や「unchanged」に見せて 1 回きりの block を回避できる.
  • 検証の既存レシート(checks)も同じファイルと同じ信頼モデルで,偽造すれば検証の block も回避できる.この PR が弱点を新しく増やすわけではない.
  • 影響は,助言用でしきい値が未調整のルールをエージェントが 1 回見ずに済むことにとどまる.書き込みや秘密の漏えいにはつながらない.
  • 提案された HMAC(Keychain 項目が増える)と verification/**/results.json の path policy 拒否(検証自身が使うファイルまで巻き込む)は,検証全体のレシートの設計変更であり,この PR の範囲外.レシートの保護は別 Issue として扱うのが妥当と考える.

このため,今回コードは変更しない.

furedea and others added 2 commits November 11, 2026 04:46
The audit job reported DEP001 for the sibling modules the Stop hook imports.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e Stop budget

Findings kept from blocking by stop_hook_active are recorded as not yet shown.

They block once at the next Stop on the same inputs, so the agent still sees them.

The model rules get only the time left after verification, within the Stop timeout.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 10, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.


def _advise(record: SessionRecord, stop_hook_active: bool, deadline: model_rules_run.Deadline) -> Advice:
inputs = _inputs(record)
receipt = record.results().get(RECEIPT_KEY)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The kill switch is ignored when a receipt can be reused. _advise checks the saved receipt before _judge → _preconditions looks at AGENT_MODEL_RULES. If a findings-unshown receipt exists (it was saved while stop_hook_active was set) and the user then sets AGENT_MODEL_RULES=off, the next Stop on the same inputs still returns {"decision":"block"}. Other receipts keep showing the "findings unchanged" message. The PR's behavior table says the kill switch gives only a one-line systemMessage and never blocks. Suggested fix: check the kill switch, and probably the trusted-repo check too, before reading the receipt. Also add a test for kill switch plus an existing findings-unshown receipt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

妥当でした.7b910e5 で修正しました.AGENT_MODEL_RULES=off を,保存済みレシートの再利用より前(advise の最初)で判定します.テスト test_the_kill_switch_wins_over_a_stored_receipt_of_unseen_findings を追加しました.

Comment thread agents/hooks/lib/model_rules_gate.py Outdated
cache = model_rules_run.cache_path(state_directory())
start = record.head
committed = start is not None and head_commit(record.root) != start
bases = (start, None) if committed else (None,)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The uncommitted review (--base omitted) does not judge only this session's changes. It judges the whole working tree against HEAD, plus every untracked file that git does not ignore. The only gate is that baseline.changed_paths is non-empty, and the plan never narrows the paths to those session changes. So if the session starts in a dirty tree (user edits already in progress, or untracked scratch files), one small agent edit sends all of those files to the model. Findings in them then block the agent as if it wrote that code. Suggested fix: when calling jev-lint, pass only the paths that record.baseline.changed_paths(current) returns, intersected with the plan. Also add a test where the session starts in a dirty tree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

妥当でした(ADR は過剰包含を許容していましたが,狭められるため直しました).7b910e5 で,未コミットのレビューを baseline と内容が異なるファイル(このセッションで変えた,または作ったファイル)に限定し,そのパスだけを jev-lint に渡します.最初から dirty で触っていないファイルは送りません.触ったファイルは HEAD との差分全体で採点するため,そのファイルの以前の未コミット部分は含まれます(baseline がファイル単位の署名であるため.ADR に明記しました).テスト test_a_file_the_session_never_touched_is_not_sent_even_when_it_is_untracked と test_a_new_file_created_by_the_session_is_sent を追加し,argv の完全一致テストも更新しました.

@github-actions

Copy link
Copy Markdown

Review summary

No blocking findings (no P0 or P1). Two P2s are posted inline:

  • P2 model_rules_gate.py:52: the kill switch AGENT_MODEL_RULES=off is checked only after a saved receipt is reused. So a findings-unshown receipt can still block after the user turns the feature off.
  • P2 model_rules_gate.py:115: the uncommitted review judges the whole dirty working tree and untracked files, not only this session's changes. Edits and untracked files that existed before the session can be sent to the model and block the agent.

What I checked and found sound: the token appears only in the op child's environment, and the environment allowlist is applied. The process group is killed on timeout, and the Stop budget stays inside 600s. The cap is shared between the two reviews. Receipts are not written when a run is prevented, and stop_hook_active blocks once at most.

🤖 Generated with Claude Code

…hanged

Check AGENT_MODEL_RULES=off before a stored receipt can make the rules speak.

Limit the review of uncommitted changes to the files that differ from the baseline.

Files that were already dirty and that the session never touched are not sent.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 10, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread agents/hooks/lib/session_gate.py Outdated
record, stop_hook_active=stop_hook_active, budget_seconds=min(model_rules_gate.BUDGET_SECONDS, left)
)
if advice.blocking:
return {"decision": "block", "reason": advice.message}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — When the model rules block, this throws away the verification's non-blocking systemMessage.

Under the "same inputs never block again" rule, a check that keeps failing on unchanged inputs (or an invalid test configuration that has already been reported) shows up only as result["systemMessage"]. If the model rules then find something to block on, this Stop returns only {"decision": "block", "reason": advice.message}. So:

  • the user never sees that verification is still failing or unresolved at this Stop, and
  • the agent's block reason mentions only the advisory findings, not the failing checks.

Example: verification fails → it blocks once → the agent declines the fix and stops again on the same inputs → verification now only notifies → the model rules have new findings and block. The failing-verification notice disappears from that Stop.

Fix: keep the verification message, for example by adding it to the reason ("\n".join(filter(None, (result.get("systemMessage"), advice.message)))) or by returning it as systemMessage next to the block. No test covers what happens to a verification systemMessage when the model rules block; please add one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

妥当でした.2315672 で修正しました.モデルルールが block するとき,block の reason を「検証の systemMessage,続けてモデルルールの指摘」にし,検証の報告を捨てません.テスト test_a_model_rules_block_keeps_the_verification_report を追加しました.

@github-actions

Copy link
Copy Markdown

Review summary: 0 blocking (P0/P1), 1 P2.

  • P2 agents/hooks/lib/session_gate.py:83: when the model rules block, stop() drops the verification's non-blocking systemMessage. For example, a check that keeps failing on unchanged inputs is reported only through that message, so neither the user nor the agent sees it at that Stop. There is no test for this.

Checked and found no problems:

  • Kill switch, then the trust gate, then the Keychain read, in that order.
  • Child environment allowlist and --no-config.
  • Process-group kill on timeout.
  • Receipt reuse and the block-once logic with stop_hook_active.
  • The cap shared between the two reviews.
  • Recording of the starting HEAD.
  • The verdict-cache path matching the secret-path policy ($XDG_STATE_HOME/agent-harness/model-rules).

🤖 Generated with Claude Code

A check that keeps failing on unchanged inputs is reported only through systemMessage.

The block reason now starts with that message instead of dropping it.

Refs #74

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 10, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

Review summary (read-only)

No blocking findings, and nothing else I'd flag.

What I checked:

  • Stop integration (session_gate.stop): model rules run only after verification returns no block. The model_rules receipt survives verification's save_results calls because state is read, updated in place and written back. The budget is min(90s, 600 − 20 − elapsed), so verification plus model rules fit inside the 600s Stop timeout.
  • Block-once semantics: the clean, shown and unshown receipts, the stop_hook_active handling, the kill switch winning over a stored receipt, and Prevented leaving no receipt all match the PR description and the ADR change.
  • Secrets and process boundary: the token goes only into the op child's environment, and repr hides it. The environment is allowlisted. stderr is discarded. --no-config stops the repo config from redirecting the base URL. Paths starting with - are filtered out. On timeout the whole process group is killed. The trusted-owners check and the cheap preconditions run before anything is read from the Keychain.
  • Cap logic (_capped_plan, review_all): the cap is shared across both reviews, and cached subjects are excluded in the uncapped path. If no whole file fits, judged == 0, so the review is skipped rather than falling back to the whole diff.
  • Exception handling: StateError and LockTimeoutError subclass ValueError, so advise() catches them. head_commit covers empty repos and bad values.
  • Tests: tests/hooks/test_model_rules.py covers the boundary contracts (argv, env, token never exposed), cap sharing, untouched untracked files, lock contention and retry after a prevented run.

Known gap, already stated in the PR: the real --json findings shape is checked only against a fake built from dist/types.d.ts. It needs one live run once the Keychain and 1Password setup is in place.

macOS has no parent-death signal, so a hook killed by Claude Code left op and jev-lint running.

Run each child under timeout --signal=TERM --kill-after=5s <budget>s.

The in-hook process-group kill stays as a backstop.

A missing or unusable timeout falls back to it silently.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 11, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

start = record.head
reviews = []
if start is not None and head_commit(record.root) != start:
reviews.append(model_rules_run.Review(record.root, start, RULES_DIRECTORY, cache))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The committed review covers all of start...HEAD, with no limit to the files this session touched. The uncommitted review is limited by _changed_files, so the two behave differently. If the session runs git pull, a merge, or a rebase onto an updated main, other people's upstream commits fall inside start...HEAD. Those commits get sent to the model and count against the shared 20-subject cap. _capped_plan picks files greedily in batch order, so upstream files can push out the session's own files, and the receipt then stops those files from ever being judged on the same inputs. One option is to pass _changed_files(record) as paths here too, or the intersection with git diff --name-only start...HEAD.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

現象は正しく,既知の限界として PR 本文に追記しました.ただし提案された修正では解消しません._changed_files は「作業ツリーの内容が baseline と異なるファイル」を返すため,git pull や rebase で取り込まれた上流の変更ファイルも baseline と異なり,そのまま含まれます.start...HEAD との共通部分を取っても同じです.baseline は内容のスナップショットで,どのコミットがセッション由来かは持っていないため,この方式では区別できません.区別するにはセッション中の自分のコミットを記録する仕組み(コミット hook など)が別途必要で,この PR の範囲外です.影響は「上流のファイルが共有上限 20 を押し出しうる」ことに限られ,推測ではなく上限の既存の仕様(ファイル単位の貪欲採用,採れなかった分は報告)の範囲内です.未コミットのレビューは,取り込み後の作業ツリーが HEAD と同一なら差分が空なので影響を受けません.

@github-actions

Copy link
Copy Markdown

Review summary

No P0/P1 findings.

Checked:

  • Integration of session_gate.stop() → model_rules_gate.advise(). The model_rules receipt survives verification's rewrites of results.json, because state is updated in place. The lock is released between gate() and the model rules, so there is no reentrancy problem.
  • The once-only block rule, stop_hook_active, and the UNSHOWN → SHOWN transition. Tracing them confirmed no block loop.
  • The kill switch is evaluated before the receipt. Trust gate → Keychain → send happen in that order.
  • Process boundary: the env allowlist, the token passed only to the op side, --no-config, process-group kill, and the timeout wrapper.
  • Every rule is severity: warning, so --fail-on error cannot turn a finding into a non-zero exit.
  • Cap math: cost excludes cached subjects, and a judged == 0 review asks the model nothing.
  • Tests cover the changed behaviour well, including argv/env, cap, lock, timeout, the kill switch, untrusted repos, and the recorded HEAD.

Finding:

  • P3: The committed review (--base <start>) is not limited to files the session touched. Upstream commits brought in by git pull or a rebase can be sent to the model and use up the shared cap (inline comment).

PATH cannot be relied on to hold GNU coreutils, and a PATH lookup can be hijacked.

macOS ships a BSD timeout and Linux a GNU one at /usr/bin/timeout.

Use only options both take: -s TERM -k 5 and a plain number of seconds.

Drop the --version probe. Fall back to the in-hook kill when the file is absent.

Read exit status 124, 137 and -9 (BSD timeout dies of the KILL it sends) as a timeout.

Split the model rules tests into delivery and process-boundary files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@furedea

furedea commented Nov 11, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

Review summary (read-only): no P0–P3 findings.

What I checked:

  • Stop flow (session_gate.stop, model_rules_gate.advise): the model rules don't run when verification blocks; stop_hook_active stops any repeat block (the findings are saved as unshown and block once on the next Stop with the same inputs); AGENT_MODEL_RULES=off is checked before any saved receipt; a skipped run (Prevented) saves no receipt, so the next Stop retries. Errors like StateError/SnapshotError are subclasses of ValueError, so advise catches them.
  • Running the subprocesses (model_rules_run): the child environment is an allowlist and the token goes only to the op child. Children run under /usr/bin/timeout and their process group is killed if they overrun. File paths that start with - are filtered out before they become arguments. The subject cap is shared between the committed and uncommitted reviews.
  • Fingerprint and baseline: the fingerprint covers the worktree snapshot, the start HEAD and the rule files. head in baseline.json is optional, so older records still work.
  • The verdict cache path matches the new secret_path_policy entry.

Non-blocking note: the PR description says timeout is looked up on PATH and test-run with true. The code and ADR-0021 instead use the absolute /usr/bin/timeout with no test run, so the description is out of date. Worth updating so reviewers aren't misled.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant