Skip to content

上限と再試行の方針を 1 つの宣言に集め、同じ失敗の打ち直しを止める #2343

Open
3 of 12 issues completed

Description

@takemi-ohama

大方針 #2335(異常系・ログ・回復とデータ・入出力の形を 1 つの実行の核へ集める)の柱の 1 本。子の課題は、この柱の仕組みがそろえば解消する可能性があるものである。

具体例

2026-10-31 の本番の配布のプランで、release のステップが同じ CI の失敗(run 38353378585 の runtime-smoke (agy))で落ち続けた。プランの判定役(judge)は 7 回とも「外部の待ちの揺れなので同じステップをもう一度」と返し、ステップの上限まで 366 秒、同じ失敗した run を読み直した(#1983)。打ち直しの回数は数えていたが、原因が前回と同じかは誰も見ていなかった。

2026-10-30 には、cross-refactoring の最終ゲートの CI の待ちが、上限 1154 秒のうち 112 秒を残して「結論を得られない」で終わった。同じコミットのチェックはすべて成功していた(#2224)。待ちの部品が、上限の時刻に最後の照会を打たずに止まるためである。

どちらも、上限と再試行の値と止め方が、それぞれの部品の中に書かれていることから起きる。この課題が終わると、上限と再試行の方針は 1 つの宣言から読まれ、同じ原因の打ち直しは決まった回数で止まり、止まった理由が結果に出る。

依頼(原文)

この課題の題と本文(「何をするか」「受け入れ条件」「進め方」)は、起票者(課題を書いた AI)が 2026-11-01 に #2335 の組み直しの記録として書いた文で、利用者の指示の原文として扱わない。利用者の指示の原文は、#2335 が引用した次の指摘である。

異常系処理とログ出力、異常時の回復処理が統一されていないから起こる不具合・課題が多い。1 件ずつ対処してもきりがない

内部データ構造と出力のフォーマットの不統一の影響も大きそう

NDF は Skill の Script 化から発展してきたため、内部データ構造や入出力契約がほぼ整理されていない

出力だけでなく入力もそろえるべき

(書き手: 利用者、日付: 2026-11-01。#2335 の「調べ方」の節が引用した指摘)

解釈: 利用者が求めているのは、異常時の回復処理を NDF 全体でそろえ、同じ型の不具合を 1 件ずつではなく型ごとに無くすことである。この課題はそのうち「待ちと上限」の型(#2335 の型 D)を受け持つ。上限と再試行の値の置き場を 1 つにし、回復の手段である再試行を 1 つの部品にそろえる。

起票者の推定

題・本文のうち、上の利用者の原文に無く、起票者と要求の工程が推して足した条件。S1〜S4 は課題の本文の起票者、S5〜S10 は要求の工程(2026-11-04)が人へ問わずに最も筋の通る案として選んだ。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。

# 条件 推した出所 この要求での扱い
S1 上限(時間・予算・待ち)と再試行の方針を 1 つの宣言に置き、実行の核が当てる。コードに timeout=<数字> の直書きを残さない(テストを除く) 課題の本文「何をするか」・受け入れ条件 1。#2335 の起票者の推定 S5 受け入れ条件 1・2
S2 再試行の実装を lib/waits.py の 1 つにし、ほかの再試行の関数を残さない 課題の本文の受け入れ条件 2 受け入れ条件 3
S3 同じ原因の失敗の打ち直しは決まった回数で止まり、理由が結果に出る 課題の本文の受け入れ条件 3。#2335 の受け入れ条件 5a 受け入れ条件 4・5
S4 子の課題は 1 件ずつ直さず、柱の仕組みを入れた後に再現の手順を 1 回ずつ通し、閉じるか理由つきで残すかに分ける 課題の本文「進め方」・受け入れ条件 4 受け入れ条件 8・境界の「行わない」
S5 直書きを禁じる対象を timeout=<数字> だけでなく、上限・再試行の回数・待ちの間隔の定数(例 DEFAULT_TIMEOUT = 1800)にも広げ、構造チェックで判定する #2335 の受け入れ条件 5 の書き換え(定数を通すと直書きを言い換えただけになる)。#2335 の設計の I12 受け入れ条件 1・2
S6 実験版(plugins/ndf/scripts/experimental/)は直書きの検査の対象から外す 実験版は課題を起こさずにその場で試す経路で、本体へ移すときに安定版の検査を通る(AGENTS.md の「安定版と実験版」) 前提 4
S7 同じ原因かどうかは、例外の型と、数字・時刻・パスを伏せた失敗の理由の文から作る「原因の鍵」で見る #2335 の設計の決定 13 受け入れ条件 4
S8 同じ原因の打ち直しの上限は、関数の呼び出しの再試行と、プランの判定役が返すステップの打ち直しの両方に当てる #1983 はステップの打ち直しで起きた。呼び出しの再試行だけに当てると #1983 が残る 受け入れ条件 4・5
S9 待ちは上限の時刻に最後の照会を 1 回打ってから止まる #2224 の受け入れ条件。止まる時刻を上限より前に置くと、上限の値を宣言に置いても実際の上限が宣言と食い違う 受け入れ条件 6
S10 この柱の子の課題は、#2335 が型 D(待ち・上限)に分けた 13 件のうち、マイルストーン「30 根本解決 3」にある 9 件とする #2335 の要求の「対象の課題」と、マイルストーン 30 の一覧(2026-11-04 に gh issue view で確かめた) 前提 5・受け入れ条件 8

目的

  • NDF のスクリプトが、上限の値と再試行の回数・間隔をそれぞれ自分で持たずに済む状態にする。値を変えるときに直す場所が 1 つになり、入口の契約から上限をたどれる
  • 同じ原因の失敗を上限まで打ち直さない。打ち直しが結果を変えないと分かった時点で止まり、止まった理由(原因と回数)が結果に残るので、人へ戻ったときに追い直さずに済む
  • 待ちが宣言した上限を使い切ってから止まる。上限より前に「結論を得られない」で止まらない

前提

対象範囲

含む:

  • 上限の宣言(plugins/ndf/contracts/limits.json)を作り、limits.py の表と、各部品の上限・再試行の回数・待ちの間隔の既定値をそこへ移す
  • 構造チェック(scripts/check-script-structure.py)に、上限・再試行の数字のリテラルの検査と、lib/waits.py の外の再試行のループの検査を足す
  • 再試行のループを持つ関数(2026-11-04 の時点で supervise_lib/paths.py の _add_worktree_with_retry、lib/post_queue.py の retry など)を lib/waits.py の呼び出しへ置き換える
  • 原因の鍵を作り、同じ原因の打ち直しを宣言の回数で止める仕組み。呼び出しの再試行(lib/waits.py)と、プランの判定役が返すステップの打ち直しの両方に当てる
  • 待ちが上限の時刻に最後の照会を打ってから止まり、止まった理由を結果に出すこと
  • 子の課題 9 件の再現の手順を通し、閉じるか理由つきで残すかを本文に記録すること

含まない:

ドメインイベント

番号は #2335 の要求のドメインイベント E7・E8 を細かくしたもの。

# イベント 引き金 失敗したとき 順序の前提
P4-E1 上限の宣言を読んだ 実行の核の起動・limits.py の呼び出し 宣言が無い・壊れている・方針の名前が無い → 核が stopped(理由に宣言のパスと方針の名前) —
P4-E2 呼び出しが失敗した 部品の呼び出しの例外か、再試行の条件に当たる結果 — P4-E1
P4-E3 失敗から原因の鍵を作った P4-E2 鍵を作れない(理由の文が空など) → 回数だけで数え、理由に「原因を見分けられない」を書く P4-E2
P4-E4 同じ原因の失敗を数えた P4-E3 — P4-E3
P4-E5 間隔を置いて打ち直した P4-E4 の回数が宣言の上限より小さい 打ち直しの合計の待ちが上限を超える → P4-E6 P4-E4
P4-E6 打ち直しを止めた 同じ原因の回数が上限に達した、または待ちの合計が上限に達した — P4-E4
P4-E7 止めた理由を結果に書いた P4-E6 — P4-E6
P4-E8 判定役がステップの打ち直しを返した プランのステップの失敗 — —
P4-E9 ステップの打ち直しを同じ原因の上限で止めた P4-E8 の失敗の原因の鍵が前回と同じで、回数が上限に達した — P4-E8
P4-E10 待ちが上限の時刻に最後の照会を打った 待ちの次の眠りが上限を超える 照会が失敗する → 理由に照会の失敗を書いて止まる P4-E1

用語

用語 意味
上限の宣言 上限の秒・再試行の回数・待ちの間隔・同じ原因の上限を、方針の名前ごとに持つ 1 つのファイル(plugins/ndf/contracts/limits.json)
方針の名前 上限の宣言の 1 行を指す名前。入口の契約と部品の呼び出しが、値でなくこの名前で上限を引く
原因の鍵 失敗が前回と同じ原因かを見る値。例外の型と、数字・時刻・パスを伏せた失敗の理由の文から作る
再試行 失敗した同じ呼び出しを、同じプロセスの中で間隔を置いて打ち直すこと
ステップの打ち直し プランの判定役が、失敗したステップを同じステップのまま流し直すと返すこと

受け入れ条件

  • 1. 上限(時間・予算・待ち)・再試行の回数・待ちの間隔の既定値は上限の宣言(plugins/ndf/contracts/limits.json)1 か所にあり、limits.py と各部品はそこから値を読む。宣言の値を 1 つ変えると、その方針の名前を使う部品の振る舞いが変わる(テストで、宣言を差し替えて部品の上限が変わることを見る)
  • 2. テストと実験版を除く plugins/ndf/ の Python のコードに、timeout=<数字> の直書きと、上限・再試行の回数・待ちの間隔の定数への数字のリテラルがあると、構造チェック(python3 scripts/check-script-structure.py)が終了コード 1 で落とす。変更の後の develop では 0 で通る
  • 3. 再試行のループは lib/waits.py だけにある。lib/waits.py の外で再試行のループを書くと構造チェックが落とす。tenacity を import するのは lib/waits.py だけである
  • 4. 同じ原因の鍵の失敗が上限の宣言の回数に達すると、呼び出しの再試行は止まり、待ちの合計の上限まで回らない。原因の鍵が変わった失敗は別に数える(テストで、同じ理由の失敗を続けると宣言の回数で止まり、理由の文の数字だけが違う失敗も同じ原因として数えることを見る)
  • 5. 止まったときの結果は stopped で、理由に原因の鍵の要旨と回数が出る(例: 「同じ原因(CI の失敗: runtime-smoke)で 3 回失敗した」)。ステップの打ち直しにも同じ上限が効き、本番の配布のプランで judge が同じ CI の失敗に打ち直しを 7 回返し、上限まで同じ run を読み直す → 同じ失敗が続いたら止めて人へ戻す #1983 の再現の手順(同じ CI の失敗が続く release のステップ)では、宣言の回数で止まって人へ戻る
  • 6. 待ちは、次の眠りが上限を超えるとき、上限の時刻に最後の照会を 1 回打ってから止まる。上限で止まったときの結果は stopped で、理由に待った秒・照会の回数・上限の秒が出る(待ちの部品 wait_until が上限の時刻に最後の照会を打たず、上限の最大 2 分前に CI 待ちを打ち切る #2224 の再現の手順で確かめる)
  • 7. 宣言が無い・壊れている・方針の名前が宣言に無いとき、部品は組み込みの値へ黙って戻らず、終了コード 1 と理由(宣言のパスと方針の名前)で止まる
  • 8. 子の課題 9 件(前提 5)のそれぞれを、閉じた(再現の手順を通して解消を確かめた)か残した(理由つき)かに分けて、この課題の本文に記録する。閉じた課題には、通した手順と結果をコメントで残す
  • 9. 退行しない: 既存の全体テスト(uv run --frozen --project . --all-extras pytest . -q -n 4)が通る。limits.py check の順序のチェック(無進捗の許容 < 監視の上限 < CLI の上限)は、宣言を読んで同じ組を判定する

非機能の条件

大項目 条件
性能・拡張性 宣言の読み込みで、入口の起動(python3 <入口> --help)が遅くならない。変更の前後で 10 回ずつ測った中央値の差を設計 Pull Request に書き、#2335 の上限 150 ms の内訳に収まることを示す
運用・保守性 上限で止まった理由(方針の名前・原因の鍵・回数・待った秒)が、結果と実行の記録の両方に残り、後から 1 回の実行を追える
移行性 移す値は今の既定値と同じにする。値の移し替えの前後で、各部品の上限の秒・回数が変わらないことをテストで見る

影響

対象 影響
公開インタフェース limits.py のコマンド(cli-timeout・monitor-timeout・check)の引数と出力は変えない。利用者が上書きに使う環境変数(MONITOR_TIMEOUT など)は変えない
データ 上限の宣言のファイルを追加する。保存済みのデータの移行は無い
既存の振る舞い 同じ原因の打ち直しが、今より早く止まる(今はステップの上限か待ちの合計まで回る)。待ちが上限の時刻まで照会を続ける(今は最大 2 分前に止まる)

検証手段

項目 手段
テスト uv run --frozen --project . --all-extras pytest . -q -n 4(受け入れ条件 1・4〜7・9)
静的解析・構造の検査 python3 scripts/check-script-structure.py(受け入れ条件 2・3。CI のジョブ script-structure)、python3 plugins/ndf/scripts/lib/limits.py check
再現の手順 子の課題 9 件の本文の再現の手順を 1 回ずつ通す(受け入れ条件 5・6・8)
手動確認 リリース後テストで、上限で止まった実行の結果に方針の名前と理由が出ることを 1 件見る

前提とする取り決め

項目 参照先 / 決めたこと
プロジェクト構造 宣言は plugins/ndf/contracts/ に置く(#2336 が作った契約の一覧と同じ置き場)。plugins/ から factory/ を参照しない(AGENTS.md)
コーディング規約 再試行と待ちは lib/waits.py(tenacity を呼ぶ唯一のモジュール)を通す。新しい依存は足さない(#2335 の前提 5)
テスト戦略 部品の単体テストで、宣言を差し替えて上限と回数が変わることと、同じ原因の数え方を見る。.md の文言を照合するテストは書かない(AGENTS.md)

境界

区分 内容
常に行う 既存テストと構造チェックを通す。値は今の既定値のまま移す
確認してから行う 上限の既定値そのものを変えること(承認ゲート 1・2 で人が見る)
行わない 子の課題を 1 件ずつ直すこと。上書きの経路を新しく作ること。hook を核へ移すこと

未決

項目 誰が決めるか 期限
1. 同じ原因の上限の既定の回数と、原因の鍵を作るときに伏せる部分の規則 この柱の設計 設計 Pull Request
2. シェルのスクリプトの上限の直書き(前提 8 の 4 か所)を検査の対象に入れるか この柱の設計 設計 Pull Request
3. 構造チェックが「再試行のループ」と「待ち」を見分ける規則(受け入れ条件 3) この柱の設計 設計 Pull Request
4. プランの判定役のステップの打ち直しに、原因の鍵をどこで渡すか(判定役への入力か、判定の後の核か) この柱の設計 設計 Pull Request
5. #2335 の本文の timeout=<数字> 195 か所と、打ち直した値(2026-11-01 に 17、2026-11-04 に 20)の食い違いの出所。#2335 の未決をこの柱が受け持つ この柱の設計(契約の一覧の数え方で確かめる) 設計 Pull Request

進め方

子の課題は 1 件ずつ直さない。柱の仕組みを入れた後に、子の課題の再現の手順を 1 回ずつ通し、解消したものを理由を添えて閉じる。設計は #2335 の設計(マイルストーン「28 根本解決 1: 設計と契約の棚卸し」)の後に、この柱ごとに行う。

(柱の記号: P4。2026-11-01 の組み直しで作った)

進行

モード: —

  • 要求と受け入れ条件 — 2026-11-04 08:06
  • 作業場所の用意
  • 設計
  • 素材の収集と出典の確定
  • ドキュメント再構成 — 2026-11-04 08:25
  • ドキュメントレビュー
  • 計画
  • 実装
  • 構造改善
  • 実装レビュー
  • 完了判定
  • Pull Request
  • 確定仕様化
  • 後片付け
  • 配布
  • 体裁レビュー
  • リリース後テスト
  • 振り返り
  • 棚卸し

人の判断待ち(開発ループ lroot3)

  • 判定: unknown(承認ゲート 1)
  • 理由: 設計の中身は MVV に沿っている。上限の値を 1 つの宣言にまとめ、同じ原因の打ち直しを決まった回数で止める。これは現象ごとに直さず共通の層で直す形で、Value 7「根本原因の場所で直す」と Value 3「やめる・縮めるを既定にする」に当たる。上限の数字の判定は構文木で行い、待ちと再試行は待ちの部品(lib/waits.py)にまとめる。これは Value 5「決まった手順はスクリプトに、判断と中身は AI に」に当たる。/実装は 3 本の Pull Request に分けて、1 回の承認で読み切れる差分に収める(決定 12)。子の課題 9 件は新しい Issue を起こさず、再現の手順で閉じるか理由をつけて残す。Value 2「人が判断できる量に収める」と Value 3 に反しない。/上限の値の上書きは、今ある環境変数と引数のまま受ける。宣言は配布物に置き、ai-plugins の形を既定に埋め込まない。Value 6「どのプロジェクトでも使える形を保つ」に反しない。/配布の前に取れる検査が止まったまま、原因が確かめられていない。Pull Request のテストの表では、要求の検査(requirements-check)が終了コード 2 で止まった。理由は、この Pull Request で追加した要求文書 issues/issue-2343-requirements.md を「見つからない」として読めなかったことで、原因は未確認と書かれている。要求文書が工程の検査を通る形かを、材料から判定できない。本文が未確認と正直に書いている点は Value 1「小さく出して使って確かめる」に沿う。ただし、通っていない検査を残したまま承認の関門を自動で省くと、確かめていないものを済んだ扱いにすることになる。このため人へ戻す。/この Pull Request は文書と用語集だけを変える。秘密・認証・利用者の設定(.ndf/)・指示書・配布物の取得元(C1〜C8・P1)には触れない。一方、後に続く実装の Pull Request は認証の部品(plugins/ndf/scripts/lib/auth.py)と google-workspace の Skill の下の数字を移す。設計文書の「未確認のまま残ること」も、どちらも .ndf/pace.json の境界の置き場に当たり、MVV の判定では通さず人が見ると書いている。これは今回の変更には含まれないので、承認が要る操作の欄には入れていない。
  • 根拠の項目: Mission, Vision, Value 1, Value 2, Value 3, Value 5, Value 6, Value 7, C2, C7, P1
  • 設計 Pull Request: NDF のスクリプト: 上限と再試行を 1 つの宣言から引き、同じ原因の打ち直しを決まった回数で止める(#2343) #2481
  • 答え: approved
  • 答え方: python3 /persistent/ai/.claude/plugins/cache/ai-plugins/ndf/10.27.16/scripts/sprint-state.py loop answer lroot3 2343 --approved|--rejected|--revise --note <指摘ファイル>

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions