Skip to content

1 回の実行を識別子 1 つで追える記録とログにそろえる #2338

Open
1 of 13 issues completed

Description

@takemi-ohama

大方針 #2335(異常系・ログ・回復とデータ・入出力の形を 1 つの実行の核へ集める)の柱の 1 本(柱の記号: P2)。#2335 の設計(issues/issue-2335-design.md)が決めた実行の記録の形・置き場・寿命を前提にし、この課題はそれを満たしたと言える条件を決める。

依頼(原文)

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

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

#2338。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。

(書き手: AI の起動指示、日付: 2026-11-04)

この課題の初版の本文(「何をするか」「受け入れ条件」1〜4「進め方」)は、起票者(課題を書いた AI)が 2026-11-01 の組み直しで書いた文で、利用者の指示の原文として扱わない。初版から読み取った条件は「起票者の推定」に並べる。

解釈: 利用者が求めているのは、ログ出力の不統一から生まれる不具合を 1 件ずつでなくまとめて無くすことである。この柱では、1 回の起動で何が起き、なぜ止まったかを、1 つの記録から後で確かめられる状態にする。

起票者の推定

題・初版の本文のうち、上の利用者の原文に無く、起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。

# 条件 推した出所 この要求での扱い
S1 実行の識別子は実行の核が振る 初版「何をするか」1 文目 受け入れ条件 1・前提 1
S2 開始・終了・判断・失敗を 1 つの形の JSON Lines へ書く 初版「何をするか」1 文目 受け入れ条件 1・1c
S3 標準エラーへの診断も同じ関数を通し、print(..., file=sys.stderr) などの直書きを残さない 初版「何をするか」2 文目・受け入れ条件 2 受け入れ条件 2・2a(シェルは核が受ける形にした。前提 4)
S4 止まった理由は切らずに残す 初版「何をするか」3 文目・受け入れ条件 3 受け入れ条件 3・3a
S5 使用量の帳簿もこの記録から数える 初版「何をするか」4 文目 前提 3(帳簿は今の置き場に残し、行に実行の識別子を足してつなぐ。帳簿の集計を実行の記録から数え直さない)・受け入れ条件 1d
S6 子の課題は 1 件ずつ直さず、柱を入れた後に再現の手順を 1 回ずつ通して閉じるか残すかを決める 初版「進め方」・受け入れ条件 4 受け入れ条件 4・境界の「行わない」
S7 設計は #2335 の設計の後に、柱ごとに行う 初版「進め方」 前提 1

目的

前提

対象範囲

含む:

含まない:

ドメインイベント

# イベント 引き金 失敗したとき 順序の前提
E1 実行の識別子を振った 核の起動。環境変数 NDF_RUN_ID があれば親の識別子として受ける — 入力の解決(#2335 の E2)の後
E2 記録の置き場を決め、始まりの行を書いた E1 書けない: 標準エラーへ理由を出して stopped(前提 6) E1
E3 30 日を超えた日付のディレクトリを消した 核の起動 消せない: 診断を書いて続ける(前提として受け入れ条件 1e) E2
E4 本体が判断・診断を書いた 本体の ctx.log 書けない: 前提 6 E2
E5 シェルの本体が標準エラーへ書いた行を、診断として記録へ書いた シェルの本体の出力 書けない: 前提 6 E2
E6 子の実行が親の識別子を持ち、同じファイルへ行を書いた 核を通る入口が別の入口を子プロセスで起こした 書けない: 前提 6。並行に書いた行が混ざる: 受け入れ条件 1b 親の E2
E7 使用量の帳簿・プランの状態の記録の行へ実行の識別子を書いた 帳簿・状態の記録の追記 識別子が無い(核の外の起動): 空のまま書く E1
E8 本体が止まった理由(例外・stopped・SIGTERM・時間切れ)を核が受けた 本体の終わり方 — E2
E9 終わりの行(結果 JSON の全体と終了コード)を書いた E8 か正常の終わり SIGKILL: 書かれない(前提 7) E8

用語

用語 意味
実行の核 入口・ステップが必ず通る共通の実行の仕組み(#2335)
実行の識別子 1 回の入口の実行に実行の核が振る識別子(run_id)
実行の記録 1 回の起動(子の実行を含む)の始まりから終わりまでを、実行の識別子つきで書く JSON Lines のファイル
実行の文脈 実行の核が本体へ渡す 1 つの値。記録の書き口(ctx.log)を持つ
起点の実行 親の実行の識別子を持たない実行。実行の記録のファイル名になる
子の実行 核を通る入口が子プロセスとして起こした、核を通る入口の実行。親の実行の識別子を持つ
診断 スクリプトが人へ向けて標準エラーへ出す文(警告・進み具合・失敗の理由)。結果 JSON とは別の出力
使用量の帳簿 NDF が起動したエージェントの呼び出しごとの使用量を追記する JSON Lines

受け入れ条件

1 つの記録で追える

  • 1. 核を通る入口を正常・例外・SIGTERM・時間切れの 4 つの終わり方で起動すると、どの場合も実行の記録の 1 ファイルに、同じ実行の識別子を持つ行が始まりの行(event: start)から終わりの行(event: end)まで並ぶ。すべての行が run_id・at(UTC の ISO 8601)・event・entry・fields を持つ
  • 1a. 核を通る入口が別の入口を子プロセスで起こすと、子の行は親と同じファイルに書かれ、子の行の parent_id が親の run_id と一致する
  • 1b. 子の実行が親と並行に同じファイルへ書いても、ファイルのすべての行が 1 行ずつ JSON として読める
  • 1c. 本体が下した判断(event: decision)と、止まった理由(例外の理由・stopped の summary と gaps)が行として残る。終わりの行の fields は、標準出力へ出した結果 JSON と終了コードを持つ
  • 1d. 核を通る起動の中で追記された使用量の帳簿の行とプランの状態の記録(progress.jsonl)の行が、その実行の識別子を持つ。核の外の起動で書いた行は、実行の識別子を持たないまま今と同じ形で読める
  • 1e. 核の起動が、30 日を超えた日付のディレクトリを実行の記録の置き場から消す。消せなかったときは診断を書いて本体を続ける
  • 1f. 実行の記録の置き場を書けない場所にして起動すると、標準エラーに書けなかった理由が出て、結果が stopped になる

診断の経路が 1 つ

  • 2. 対象(前提 5)の Python の入口とステップで、標準エラーへの出力が実行の文脈の書き口(ctx.log("diag", ...) に当たる関数)1 つを通る。print(..., file=sys.stderr) と sys.stderr.write の直書きを構造チェック(scripts/check-script-structure.py)が落とす。2026-11-04 の時点の直書きは 131 か所(print(..., file=sys.stderr) 126 か所・63 ファイル、sys.stderr.write 5 か所。下の「調べ方」のコマンド)
  • 2a. 書き口を通した診断は、標準エラーと実行の記録(event: diag)の両方に出る。シェルの本体が標準エラーへ書いた行も、実行の記録に event: diag で残る
  • 2b. 移す途中は、まだ直書きを持つファイルを例外リストに載せ、例外リストに行を足す変更を構造チェックが落とす(異常系・ログ・回復とデータ・入出力の形を 1 つの実行の核へ集め、異常系の課題 103 件を型ごとにまとめて解く #2335 の受け入れ条件 1a と同じ扱い)

止まった理由が切れない

  • 3. 本体が渡した止まった理由の文字列(summary・gaps・例外の理由)が、結果 JSON と実行の記録の終わりの行に、途中で切られずに同じ文字列で残る。2026-11-04 の時点で理由・出力を先頭 N 字で切っている 10 か所(下の「調べ方」のコマンド)を、実行の記録へ全文を書いた後に人へ見せる表示だけを短くする形にする
  • 3a. 外部コマンドの失敗の出力のように 1 行の上限(未決 1)を超える理由は、全文を記録からたどれる(全文の置き場のパスと長さが行に残る)

子の課題

起きてはいけないこと

子の課題の見込み

実装の前の見込みで、判定は受け入れ条件 4 の再現の手順で決める。

課題 内容 見込み
#1687 設計のプランの途中で、作成者 t <a@b>・題 tmp のコミットが入り、worker の報告に出ない 記録から作った操作をたどれるかを確かめる。試しのコミットを止める検査は核の外で、残る見込み
#1839 cross-refactoring の見送った提案が起票されない 見送りの判断が記録に残るかを確かめる。起票の経路は核の外で、残る見込み
#1893 後片付けを終えても工程表の「後片付け」の行が未チェック 進捗記録(projects-sync.sh)は課題の本文への記録で、実行の記録とは別。残る見込み
#1979 使用量の帳簿の数え方の誤り 帳簿の数え方の誤りで、前提 3 により残る見込み
#2059 先に別の警告を出すと、非致命の拒否の件数の警告が出ない 診断が種類ごとに記録へ残るかを確かめる。解消の候補
#2132 開発ループの周回の created[] を書く経路が無い 起票の判断が記録に残るかを確かめる。記録の項目の書き手は核の外で、残る見込み
#2205 cross-refactoring の効果を版ごとに集計するスクリプト 記録が子の実行までつながれば集計の元が 1 つになる。集計のスクリプト自体は残る見込み
#2300 検査の記録の集計で、落ちた工程が「不明」になる 落ちたステップが記録に残るかを確かめる。解消の候補(ほかの 2 点は残る見込み)

非機能の条件

大項目 条件
運用・保守性 1 回の起動を実行の識別子 1 つで、始まりから終わりまで 1 ファイルから追える(受け入れ条件 1)。記録の寿命は 30 日(前提 2)
移行性 移す対象: Python の入口の標準エラーへの直書き。順序: #2335 の部品の順(supervise のプラン・判定・キュー → cross-refactoring → 課題を閉じる処理 → リリース → 外部 CLI の監視 → 使用量の帳簿 → 残り)。並走: 例外リストに載った入口は直書きのまま動く。切り戻し: Pull Request ごとの revert で前の形へ戻る。実行の識別子を持たない既存の帳簿と状態の記録の行は、そのまま読める(受け入れ条件 1d)
セキュリティ 受け入れ条件 5。秘密の扱い(C1)と認証・認可(C2)は変えない
システム環境 Claude Code と Codex の両方の配布で同じ核が動く。シェルの入口は bash で動く

影響

対象 影響
公開インタフェース 変わらない。結果 JSON の鍵は足すだけで、標準エラーの診断の文は変えない(受け入れ条件 6)
データ 実行の記録のファイルを状態の置き場(${NDF_STATE_DIR}/runs/)に新しく書く。使用量の帳簿とプランの状態の記録の行に、任意の鍵として実行の識別子を足す
既存の振る舞い 記録の置き場へ書けない場合は、今は動く入口が stopped で止まる(受け入れ条件 1f)。理由を短く切って表示していた箇所で、記録には全文が残る

検証手段

項目 手段
テスト uv run --frozen --project . --all-extras pytest . -q -n 4(全体テスト)。核の終わり方(正常・例外・SIGTERM・時間切れ)ごとに、1 ファイルに start から end まで同じ run_id が並ぶことを見るテスト。子の実行を並行に起こし、全行が JSON として読めることを見るテスト。置き場を書けない場所にして stopped を見るテスト。秘密を入れた環境変数で起動し、ファイルに値が無いことを見るテスト
静的解析・構造の検査 python3 scripts/check-script-structure.py(CI のジョブ script-structure)。直書きの検査と例外リストの増加の検査をここへ足す
配布物の検証 claude plugin validate .
手動確認(マージ前) 子の課題 8 件の再現の手順を 1 回ずつ通し、結果を受け入れ条件 4 の節へ書く

前提とする取り決め

項目 参照先 / 決めたこと
プロジェクト構造 書き口は plugins/ndf/scripts/lib/run_log.py に置く。既定で働くため実験版に置かない。factory/ を参照しない(AGENTS.md の「マーケットプレイスの構造」、#2335 の設計の「置き場所」)
コーディング規約 AGENTS.md の「最小限のコード実装」と、外部コマンドを書く前に実行して確かめる決まり
テスト戦略 書き口と核の終わり方は単体テストで担保する。入口の移し替えは構造チェックで担保する。子の課題は再現の手順で確かめる。.md の文言を照合するテストは書かない(AGENTS.md の DON'T)

境界

区分 内容
常に行う 実行の記録へ書く前に秘密を伏せる。移した入口を例外リストから消す。柱の Pull Request ごとに全体テストと構造チェックを通す
確認してから行う(承認ゲート 1・2 で人が見る) 標準エラーの診断の文を変える変更。使用量の帳簿とプランの状態の記録の形を変える変更
行わない 子の課題を 1 件ずつ直す Pull Request。hook を核へ移す。例外リストに行を足す。帳簿を実行の記録へ吸い込む

未決

項目 誰が決めるか 期限
1. 実行の記録の 1 行の大きさの上限と、上限を超える理由の全文の置き場(受け入れ条件 3a) この柱の設計 設計 Pull Request
2. 並行に書く子の実行の行が混ざらない書き方(1 行の追記の単位・排他の要否。受け入れ条件 1b) この柱の設計 設計 Pull Request
3. hook が書く問い合わせの記録(ask.jsonl)の行に実行の識別子が入るか。核が起動した claude の hook が環境変数 NDF_RUN_ID を受け取れるかは未確認 この柱の設計(実際に起動して確かめる) 設計 Pull Request
4. 本体の途中で記録へ書けなくなったとき、本体を止めるか、終わりまで続けて結果だけを stopped にするか(前提 6 の具体化) この柱の設計 設計 Pull Request

調べ方

2026-11-04 に、このリポジトリの develop(729dcbe40)で打った。

# 受け入れ条件 2 の数(テストと実験版を除く)
P="plugins/ndf/scripts plugins/ndf/skills"
grep -rEn 'file=sys\.stderr' $P --include=*.py | grep -v '/tests/' | grep -v experimental | wc -l    # 126
grep -rEl 'file=sys\.stderr' $P --include=*.py | grep -v '/tests/' | grep -v experimental | wc -l    # 63
grep -rEn 'sys\.stderr\.write' $P --include=*.py | grep -v '/tests/' | grep -v experimental | wc -l  # 5
grep -rEn '>&2' $P --include=*.sh | grep -v '/tests/' | grep -v experimental | wc -l                 # 203(24 ファイル。前提 4 により対象外)

# 受け入れ条件 3 の数(理由・出力を先頭 N 字で切る箇所)
grep -rEn '(reason|summary|stderr|tail|message|detail|err)[a-z_]*\)?\[ ?:-? ?[0-9]{2,4}\]' $P --include=*.py \
  | grep -v /tests/ | grep -v experimental | wc -l   # 10

切る箇所の 10 件は project-mvv.py:184・supervise_lib/actuals.py:46・supervise_lib/engine.py:422・supervise_lib/slow.py:202・merged_lib/merge.py:317・lib/sprint_loop_gate.py:127・lib/sprint_loop_gate.py:128・lib/monitor_loop.py:120・lib/monitor_loop.py:240・release-steps.py:141(いずれも plugins/ndf/scripts/ の下)。この正規表現に当たらない書き方(変数名が違う・textwrap.shorten など)で切る箇所は数えておらず、未確認として残る。

進行

モード: —

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

設計の承認ゲート 1 での利用者の指摘(直して出し直す)

(書き手: 利用者の答えをオーガナイザーが写した、日付: 2026-11-05。出し直しの設計はこの節を満たすように直す。満たせない項目は理由を設計に書く。この節は上書きしない)

範囲を、実際に起きた不具合 #2059(別の警告に拒否の件数の警告が消される)と #2300(落ちた工程が「不明」になる)に絞る(#2342・#2344 と同じく、起きた不具合に根拠の無い範囲を外す。MVV 版 5 の量の上限・縮めるを既定)。
残すもの: 実行の識別子・1 つの記録(JSON Lines)・子の実行・理由を切らずに記録する・秘密を伏せる仕組みと、lib/monitor_loop.py(#2059)・check-trigger.py(#2300)の移し替え。
外すもの: 60 ファイル・125 か所の移し替え(G1〜G6)、直接書きを落とす構造チェック、関係しない理由の切り詰めの書き換え、使用量の記録への識別子の追加。外した子の課題 6 件は理由を付けて残す。
1 スプリント・PR 1〜2 本に収める。実装の開始は #2337 のマージ後と設計に明記する。PR 本文の要求の検査の記述は再検査の結果に直す。

利用者の追加の指示(2026-11-06、出力先の切り替え)

ログについても同様です。手元はファイル、FactoryならCloudWatch Logsなど、ログの出力方法は統一した上で切り替えられるようにする必要があります

(書き手: 利用者、日付: 2026-11-06。オーガナイザーが写した。「同様」は、NDF の状態を手元はファイル・Factory はサーバー側の DB で持つと決めた #2608 を指す)

この柱への影響: 承認ゲート 1 の答えで絞った範囲(実行の識別子・1 つの記録・子の実行・理由を切らない・秘密を伏せる)の書き口を、出力先を差し替えられる形にする。手元の既定はファイル(JSON Lines)のままとし、出力先は書き口の中で 1 つの設定(環境変数か .ndf/ の宣言)から決める。手順書でなくスクリプトで固定する(#2600 の原則)。CloudWatch Logs への出力の実装は Factory の課題 #2608 が受け持ち、この柱では差し替えの口と既定のファイルの出力だけを作る。

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

Activity

  1. added 8 commits that reference this issue on Nov 4, 2026
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