From 9ff71261cc1c4060c57e31c14292d223790a8a03 Mon Sep 17 00:00:00 2001 From: aloekun Date: Sat, 8 Aug 2026 21:56:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(nightly-todo):=20tee=20=E3=81=AB?= =?UTF-8?q?=E3=82=88=E3=82=8B=E7=94=9F=20summary=20=E3=81=AE=20Actions=20?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E9=9C=B2=E5=87=BA=E3=82=92=E5=A1=9E=E3=81=90?= =?UTF-8?q?=20(ADR-072=20=E6=B1=BA=E5=AE=9A=2014=E3=80=81#369=20post-merge?= =?UTF-8?q?=20feedback)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 決定 14 は「公開面 = PR 本文」と狭く見ており、step ログを見落としていた。 Select task step は exe 出力を tee で selected.txt と画面 (= Actions ログ) の両方へ 出しており、そこに生の summary / target_files / caution 行が含まれていた。 **public repo では step ログも第三者に可視**なので、これは 381 の screening を 迂回する 2 つ目の公開面だった (#369 post-merge feedback Tier 1 #1、High)。 ## 修正 tee をリダイレクト (> selected.txt) に変え、ログへはマーカー行 (rank/branch/ledger のみ = 構造的に安全) と screening 済みの summary_display だけを grep で出す。生の 出力はファイルに留まり $GITHUB_OUTPUT 経由でのみ使われる (後段の許可リスト grep は不変)。 ## 実測 実データで確認: ログに出るのはマーカー行と summary_display (バッククォート置換済み) のみ。grep exit 0 で set -e でも落ちない。生 summary はファイルに残り GITHUB_OUTPUT 用途は保たれる。 ## 教訓 「公開面」は出力先を 1 つ塞ぐたびに次が見つかる (PR 本文 → step ログ)。棚卸しは 「PR 本文」で止めず経路単位で行う、と決定 14 へ追記した。narrow 修正が隣接エッジに 穴を作るパターン (memory dont-trust-takt-fix-output) の公開面版。 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/nightly-todo.yml | 10 +++++++++- docs/adr/adr-072-nightly-todo-loop.md | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly-todo.yml b/.github/workflows/nightly-todo.yml index 57ab3b9d..bc844987 100644 --- a/.github/workflows/nightly-todo.yml +++ b/.github/workflows/nightly-todo.yml @@ -150,10 +150,18 @@ jobs: if: steps.preflight.outcome == 'success' run: | set -euo pipefail + # **tee ではなくリダイレクトにする (ADR-072 決定 14、post-merge feedback #369)。** + # tee は exe の全出力を Actions ログへも出すが、そこには生の自由記述 + # (summary / target_files / caution) が含まれる。**step ログも public repo では + # 第三者に可視**なので、tee は決定 14 の screening を迂回する公開面になっていた。 + # 生の出力はファイルに留め ($GITHUB_OUTPUT 経由でのみ使う)、ログへは安全な行だけを出す。 master-ref/target/release/cli-nightly-task-select \ --ledger master-ref/docs/claude-code-web-tasks.md \ --exclude-ranks "${{ steps.inflight.outputs.exclude_ranks }}" \ - | tee "$RUNNER_TEMP/selected.txt" + > "$RUNNER_TEMP/selected.txt" + # 可観測性: マーカー行 (rank/branch/ledger のみ = 構造的に安全) と screening 済みの + # summary_display だけをログへ出す。生の summary/target_files/caution は出さない。 + grep -E '^\[NIGHTLY_TASK\]|^summary_display=' "$RUNNER_TEMP/selected.txt" # 許可リスト方式 — exe が出す key のうち、ここに列挙したものだけを転送する。 # 想定外の key 注入を防ぐ fail-closed だが、**exe 側に出力を足したらここも足す** # 必要がある (片方だけ変えると新しい出力が黙って捨てられ、参照側は空文字になる)。 diff --git a/docs/adr/adr-072-nightly-todo-loop.md b/docs/adr/adr-072-nightly-todo-loop.md index a4a6a029..466ac6fc 100644 --- a/docs/adr/adr-072-nightly-todo-loop.md +++ b/docs/adr/adr-072-nightly-todo-loop.md @@ -281,7 +281,9 @@ pre-push simplicity review はここを「他の停止点と同様に graceful d **draft PR でも public repository では第三者に可視**であり、台帳の自由記述がそのまま公開面へ出ていた (順位 381)。 -公開面の棚卸し結果、台帳由来で外部可視になるのは **PR 本文の `内容` だけ**だった。`RANK` は `u32` にパース済み、ブランチ名は `format!("claude/nightly-{rank}")` で、どちらも**構造的に安全**である。 +公開面の棚卸し結果、台帳由来で外部可視になるのは **PR 本文の `内容`** と **step ログ**の 2 つだった。`RANK` は `u32` にパース済み、ブランチ名は `format!("claude/nightly-{rank}")` で、どちらも**構造的に安全**である。 + +**初版は「公開面 = PR 本文」と狭く見ており、step ログを見落としていた** (#369 post-merge feedback が指摘)。`Select task` step は exe 出力を `tee` で `selected.txt` と**画面 (= Actions ログ) の両方**へ出しており、そこに生の `summary` / `target_files` / `caution` 行が含まれていた。**public repo では step ログも第三者に可視**なので、これは screening を迂回する 2 つ目の公開面だった。`tee` をリダイレクト (`> selected.txt`) に変え、ログへはマーカー行 (rank/branch/ledger のみ = 安全) と screening 済みの `summary_display` だけを `grep` で出す形にした。生の出力はファイルに留まり `$GITHUB_OUTPUT` 経由でのみ使われる。**「公開面」は出力先を 1 つ塞ぐたびに次が見つかる**ので、棚卸しは「PR 本文」で止めず経路単位で行う。 `cli-nightly-task-select` に `summary_display` 出力を足し、workflow はそれを**インラインコードスパンで囲んで**出す。コードスパンの内側では markdown が描画されず `@mention` の通知も飛ばないため、注入の効果がそこで消える。したがって screening の主眼は **「コードスパンから抜け出せる文字を残さないこと」**に絞り、バッククォートの置換・制御文字の除去・200 文字での切り詰めだけを行う。`@` は書き換えない — 無害化はコードスパンの役目で、`@` を潰すと正当なタスク記述が読めなくなる。 From aea194e7f54cd392b519d4f545799a091bfcd5c2 Mon Sep 17 00:00:00 2001 From: aloekun Date: Sat, 8 Aug 2026 22:48:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(nightly-todo):=20tee=20=E3=81=AB?= =?UTF-8?q?=E3=82=88=E3=82=8B=E7=94=9F=20summary=20=E3=81=AE=20Actions=20?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E9=9C=B2=E5=87=BA=E3=82=92=E5=A1=9E=E3=81=90?= =?UTF-8?q?=20+=20=E5=87=BA=E5=8A=9B=E5=A5=91=E7=B4=84=E6=A4=9C=E8=A8=BC?= =?UTF-8?q?=E3=82=92=E5=88=86=E9=9B=A2=20(ADR-072=20=E6=B1=BA=E5=AE=9A=201?= =?UTF-8?q?4=E3=80=81#369/#370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 決定 14 は「公開面 = PR 本文」と狭く見ており step ログを見落としていた。Select task step は exe 出力を tee で selected.txt と画面 (= Actions ログ) の両方へ出しており、 生の summary/target_files/caution が含まれていた。**public repo では step ログも 第三者に可視**なので、381 の screening を迂回する 2 つ目の公開面だった (#369 post-merge feedback Tier 1 #1、High)。 ## tee → リダイレクト tee を > selected.txt に変え、生の出力はファイルに留めて $GITHUB_OUTPUT 経由でのみ 使う。ログへはマーカー行 (rank/branch/ledger のみ = 構造的に安全) と screening 済みの summary_display だけを出す。 ## 出力契約検証を別 step に分離 (#370 CodeRabbit Major 指摘) 初版は「マーカー欠落なら exit 1」を Select task step の中に置いていたが、同 step は exit 2 (台帳破損) / exit 3 (該当タスク無し) を区別するため continue-on-error が必要で、 その非 success は下段 Stop step が「正常な no-op」として green で終える。検証を同 step に 置くと、**exe は成功したのに出力が壊れているケースの exit 1 も continue-on-error に 飲まれ、出力契約違反が「今夜は何も無かった」に化ける**。 検証を Validate and echo the task-selection output contract step に切り出し、 continue-on-error を付けないことで job の red として顕在化させる (if: steps.select.outcome == 'success' なので exit 2/3 の no-op 経路とは分離)。 ## ADR の保護方法を公開面ごとに明記 (#370 CodeRabbit Minor 指摘) 決定 14 が step ログと PR 本文の保護方法を混ぜて書いていた。実際は screening 処理は 共通だが囲み方が違う (PR 本文 = コードスパン、step ログ = 固定プレフィックス付き 1 行)。 公開面ごとに分けて記述した。 ## 教訓 「公開面」は出力先を 1 つ塞ぐたびに次が見つかる (PR 本文 → step ログ)。棚卸しは 経路単位で行う、と決定 14 へ追記。 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/nightly-todo.yml | 26 +++++++++++++++++++++++--- docs/adr/adr-072-nightly-todo-loop.md | 7 ++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nightly-todo.yml b/.github/workflows/nightly-todo.yml index bc844987..c7594438 100644 --- a/.github/workflows/nightly-todo.yml +++ b/.github/workflows/nightly-todo.yml @@ -159,15 +159,35 @@ jobs: --ledger master-ref/docs/claude-code-web-tasks.md \ --exclude-ranks "${{ steps.inflight.outputs.exclude_ranks }}" \ > "$RUNNER_TEMP/selected.txt" - # 可観測性: マーカー行 (rank/branch/ledger のみ = 構造的に安全) と screening 済みの - # summary_display だけをログへ出す。生の summary/target_files/caution は出さない。 - grep -E '^\[NIGHTLY_TASK\]|^summary_display=' "$RUNNER_TEMP/selected.txt" # 許可リスト方式 — exe が出す key のうち、ここに列挙したものだけを転送する。 # 想定外の key 注入を防ぐ fail-closed だが、**exe 側に出力を足したらここも足す** # 必要がある (片方だけ変えると新しい出力が黙って捨てられ、参照側は空文字になる)。 grep -E '^(rank|branch|target_files|summary|summary_display|caution)=' "$RUNNER_TEMP/selected.txt" \ >> "$GITHUB_OUTPUT" + # 出力契約の検証と可観測ログ。**この step には continue-on-error を付けない** + # (ADR-072 決定 14、#370 CodeRabbit 指摘)。 + # + # Select task は exit 2 (台帳破損) / exit 3 (該当タスク無し) / exit 0 を区別するため + # continue-on-error が必要で、その非 success は下段の Stop step が「正常な no-op」として + # green で終える。ところが検証を Select task の**中**に置くと、exe は成功したのに出力が + # 壊れている (マーカー欠落) ケースの exit 1 も同じ continue-on-error に飲まれ、 + # 「出力契約違反」が「今夜は何も無かった」に化ける。検証を別 step に切り出し + # continue-on-error を付けないことで、この失敗を job の red として顕在化させる。 + # + # 生の summary/target_files/caution はログへ出さない。ログへ出すのはマーカー行 + # (rank/branch/ledger のみ = 構造的に安全) と screening 済みの summary_display だけ。 + - name: Validate and echo the task-selection output contract + if: steps.select.outcome == 'success' + run: | + set -euo pipefail + if ! grep -qE '^\[NIGHTLY_TASK\]' "$RUNNER_TEMP/selected.txt" \ + || ! grep -qE '^summary_display=' "$RUNNER_TEMP/selected.txt"; then + echo "::error::output-contract violation: cli-nightly-task-select の出力に [NIGHTLY_TASK] または summary_display= が見つかりません (出力フォーマットのドリフトの可能性)。台帳選択は成功しているため no-op ではなく job を止めます。" >&2 + exit 1 + fi + grep -E '^\[NIGHTLY_TASK\]|^summary_display=' "$RUNNER_TEMP/selected.txt" + # 本 step は 2 つの経路で発火する — select が非ゼロだった場合と、Pre-flight gate の # 背圧 deny で select 自体が skip された場合。後者では select の [NIGHTLY_SKIP] 行が # 存在しないため、「直前の行を見よ」と一律に案内すると誤誘導になる。経路で出し分ける。 diff --git a/docs/adr/adr-072-nightly-todo-loop.md b/docs/adr/adr-072-nightly-todo-loop.md index 466ac6fc..40f15422 100644 --- a/docs/adr/adr-072-nightly-todo-loop.md +++ b/docs/adr/adr-072-nightly-todo-loop.md @@ -285,7 +285,12 @@ pre-push simplicity review はここを「他の停止点と同様に graceful d **初版は「公開面 = PR 本文」と狭く見ており、step ログを見落としていた** (#369 post-merge feedback が指摘)。`Select task` step は exe 出力を `tee` で `selected.txt` と**画面 (= Actions ログ) の両方**へ出しており、そこに生の `summary` / `target_files` / `caution` 行が含まれていた。**public repo では step ログも第三者に可視**なので、これは screening を迂回する 2 つ目の公開面だった。`tee` をリダイレクト (`> selected.txt`) に変え、ログへはマーカー行 (rank/branch/ledger のみ = 安全) と screening 済みの `summary_display` だけを `grep` で出す形にした。生の出力はファイルに留まり `$GITHUB_OUTPUT` 経由でのみ使われる。**「公開面」は出力先を 1 つ塞ぐたびに次が見つかる**ので、棚卸しは「PR 本文」で止めず経路単位で行う。 -`cli-nightly-task-select` に `summary_display` 出力を足し、workflow はそれを**インラインコードスパンで囲んで**出す。コードスパンの内側では markdown が描画されず `@mention` の通知も飛ばないため、注入の効果がそこで消える。したがって screening の主眼は **「コードスパンから抜け出せる文字を残さないこと」**に絞り、バッククォートの置換・制御文字の除去・200 文字での切り詰めだけを行う。`@` は書き換えない — 無害化はコードスパンの役目で、`@` を潰すと正当なタスク記述が読めなくなる。 +`cli-nightly-task-select` に `summary_display` 出力を足す。**2 つの公開面は保護方法が異なる**: + +- **PR 本文**: `summary_display` を**インラインコードスパンで囲んで**出す。コードスパンの内側では markdown が描画されず `@mention` の通知も飛ばないため、注入の効果がそこで消える。したがって screening の主眼は **「コードスパンから抜け出せる文字を残さないこと」**に絞り、バッククォートの置換・制御文字の除去・200 文字での切り詰めだけを行う。`@` は書き換えない — 無害化はコードスパンの役目で、`@` を潰すと正当なタスク記述が読めなくなる。 +- **step ログ**: コードスパンは使えない (Actions ログは markdown 描画しない plain text)。代わりに **screening 済みの `summary_display` を固定プレフィックス `summary_display=` 付きの 1 行**として出す。screening が制御文字を除去し 1 行化しているため、ログ行を割って別の出力に見せかける経路が塞がる。生の `summary` はここには出さない (§ 実走スモークで塞いだ `tee` 露出)。 + +`summary_display` の screening 処理そのもの (バッククォート置換・制御/不可視文字除去・切り詰め) は両公開面で共通だが、**その値をどう囲むか**が公開面ごとに違う。 **agent プロンプト側はこの screening を通さない。** あちらが必要とするのは完全なタスク記述で、遮断の責務は決定 12 / 13 が持つ。**同じ文字列でも出口ごとに必要な処理が違う**ため、「安全な summary」1 本に統一していない。screening を Rust に置いたのは、順位 382 の injection payload 回帰テストが固定する対象を作るためでもある (shell に置くとテストの場が無い)。