From 17d5d441659901afafd2954b8a7dc3ce8da54e2e Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 22:42:59 +0000 Subject: [PATCH 01/30] =?UTF-8?q?Fix:=20=E5=88=A9=E7=94=A8=E8=80=85?= =?UTF-8?q?=E3=81=94=E3=81=A8=E3=81=AE=20Serena=20=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E8=BF=BD=E8=B7=A1=E3=81=8B=E3=82=89=E5=A4=96?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.serena/serena_config.yml` は利用者ごとに書き換わる設定で、認証の秘密 (`auth_secret`)を持つ。追跡したままだと、手元の変更が `git add -u` や `commit -a` でコミットへ入りうる。 - 無視の設定へ 1 行足し、追跡から外す(`git rm --cached`) - 手元のファイルは消えない。既存の利用者は取り込んだ後も同じ内容を使い続ける - 履歴に秘密は入っていない(コミット済みの版に `auth_secret` の行は無い) Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + .serena/serena_config.yml | 213 -------------------------------------- 2 files changed, 2 insertions(+), 213 deletions(-) delete mode 100644 .serena/serena_config.yml diff --git a/.gitignore b/.gitignore index 283efc389..61359c2b1 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ __pycache__/ ${SERENA_HOME}/ .serena/logs/ .serena/cache/ +# 利用者ごとの設定。認証の秘密を持つため追跡しない +.serena/serena_config.yml .cross_review_pr22_diff.patch # pytest が実行のたびに作る作業領域 diff --git a/.serena/serena_config.yml b/.serena/serena_config.yml deleted file mode 100644 index b9ba4eff7..000000000 --- a/.serena/serena_config.yml +++ /dev/null @@ -1,213 +0,0 @@ -# the language backend to use for code understanding and manipulation. -# Possible values are: -# * LSP: Use the language server protocol (LSP), spawning freely available language servers -# via the SolidLSP library that is part of Serena. -# * JetBrains: Use the Serena plugin in your JetBrains IDE. -# (requires the plugin to be installed and the project being worked on to be open -# in your IDE). -language_backend: LSP - -# whether to open a graphical window with Serena's logs. -# This is mainly supported on Windows and (partly) on Linux; not available on macOS. -# If you prefer a browser-based tool, use the `web_dashboard` option instead. -# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html -# -# Being able to inspect logs is useful both for troubleshooting and for monitoring the tool calls, -# especially when using the agno playground, since the tool calls are not always shown, -# and the input params are never shown in the agno UI. -# When used as MCP server for Claude Desktop, the logs are primarily for troubleshooting. -# Note: unfortunately, the various entities starting the Serena server or agent do so in -# mysterious ways, often starting multiple instances of the process without shutting down -# previous instances. This can lead to multiple log windows being opened, and only the last -# window being updated. Since we can't control how agno or Claude Desktop start Serena, -# we have to live with this limitation for now. -gui_log_window: false - -# whether to start the Serena Dashboard, which provides detailed information on your Serena session, -# the current configuration and furthermore allows some settings to be conveniently modified on the fly. -# We strongly recommend to always enable this option! -# If you want to prevent the Dashboard window from being opened on launch, -# set `web_dashboard_open_on_launch` to false (see below). -# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html -web_dashboard: false - -# the address the web dashboard will listen on (bind address). -web_dashboard_listen_address: 127.0.0.1 - -# whether to open the Dashboard window/browser tab when Serena starts (provided that `web_dashboard` is enabled). -# If set to false, you can still open the dashboard manually: -# * When using an interface that supports a tray icon (see setting `web_dashboard_interface`), -# you can conveniently open the dashboard from the system tray. -# * When using the `browser` interface (no tray icon), so you can only open the dashboard by -# a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by -# b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port -# may be higher if you have multiple instances running; try ports 24283, 24284, etc.) -# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html -web_dashboard_open_on_launch: true -jetbrains_plugin_server_address: 127.0.0.1 - -# the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error) -log_level: 20 - -# whether to trace the communication between Serena and the language servers. -# This is useful for debugging language server issues. -trace_lsp_communication: false - -# advanced configuration option allowing to configure language server-specific options. -# Maps the language key to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. -# No documentation on options means no options are available. -ls_specific_settings: {} - -# timeout, in seconds, after which tool executions are terminated -tool_timeout: 240 - -# list of tools to be globally excluded -excluded_tools: [] - -# list of optional tools (which are disabled by default) to be included -included_optional_tools: [] - -# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. -# This cannot be combined with non-empty excluded_tools or included_optional_tools. -fixed_tools: [] - -# list of mode names to that are always to be included in the set of active modes. -# The full set of modes to be activated is base_modes + default_modes + added_modes, -# where added_modes can be defined by projects/CLI parameters. -# If this is undefined/empty, no base modes are included. -# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes -base_modes: -default_modes: -- interactive -- editing - -# Used as default for tools where the apply method has a default maximal answer length. -# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default -# through the global configuration. -default_max_tool_answer_chars: 150000 - -# the name of the token count estimator to use for tool usage statistics. -# See the `RegisteredTokenCountEstimator` enum for available options. -# -# By default, a very naive character count estimator is used, which simply counts the number of characters. -# You can configure this to TIKTOKEN_GPT4O to use a local tiktoken-based estimator for GPT-4o (will download tiktoken -# data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to -# estimate the token count using the Claude Sonnet 4 tokenizer. -token_count_estimator: CHAR_COUNT - -# the list of registered project paths (updated automatically). -projects: -- /work/ai-plugins - -# list of paths to ignore across all projects. -# Same syntax as gitignore, so you can use * and **. -# These patterns are merged additively with each project's own ignored_paths. -# Quote patterns that start with `*`, e.g. `"**/bin/**"`. -ignored_paths: [] - -# time budget (seconds) per tool call for the retrieval of additional symbol information -# such as docstrings or parameter information. -# (currently only used by LSP-based tools). -# If the budget is exceeded, Serena stops issuing further retrieval requests -# and returns partial info results. -# 0 disables the budget (no early stopping). Negative values are invalid. -# This is an advanced setting that can help alleviate problems with LSP servers -# that have a slow implementation of request_hover (clangd is one of those) -# or with tool calls that find very many symbols. -# Can be overridden in project.yml. -symbol_info_budget: 10.0 - -# list of regex patterns which, when matched, mark a memory entry as read‑only. -# For example, "global/.*" will mark all global memories as read-only. -# You can extend the list on a per-project basis in the project.yml configuration file. -read_only_memory_patterns: [] - -# template for the location of the per-project .serena data folder (memories, caches, etc.). -# Supports the following placeholders: -# $projectDir - the absolute path to the project root directory -# $projectFolderName - the name of the project directory -# Default: "$projectDir/.serena" (data stored inside the project directory) -# Example for a central location: "/projects-metadata/$projectFolderName/.serena" -project_serena_folder_location: $projectDir/.serena - -# list of regex patterns for memories to completely ignore. -# Matching memories will not appear in list_memories or activate_project output -# and cannot be accessed via read_memory or write_memory. -# To access ignored memory files, use the read_file tool on the raw file path. -# This is useful for projects with large numbers of archived memory files. -# You can extend the list on a per-project basis in the project.yml configuration file. -# Example: ["_archive/.*", "_episodes/.*"] -ignored_memory_patterns: [] - -# line ending convention to use when writing source files. -# Possible values: "lf" (Unix), "crlf" (Windows), "native" (platform default). -# Note that Serena's own files (e.g. memories and configuration files) always use native line endings. -# This setting can be overridden on a per-project basis in project.yml files. -line_ending: native - -# defines the interface (application mode) used for the web dashboard (if enabled). -# If empty/null, use platform-dependent default. Otherwise, possible values: -# * browser: the dashboard is opened in the default browser (if `web_dashboard_open_on_launch` is true) -# This is supported on all platforms. -# * app: the dashboard is opened in a separate native-like app window with accompanying tray icon, whose -# lifecycle is tied to the Serena process. -# If `web_dashboard_open_on_launch` is false, the dashboard can be conveniently accessed via the tray icon. -# This is supported on Windows and macOS, but note that on macOS, where tray icons are very visible, -# this may result in too many icons being displayed when using multi-agent setups. -# * tray_manager: use a global tray icon to provide access to the dashboards of all running Serena instances, -# opening the dashboard in browser tabs when selected from the tray menu. -# This is EXPERIMENTAL. It is tested on Windows only. We will establish macOS support, but it is yet untested. -# On Linux, this cannot be universally supported, but it may work in some desktop environments. -# On NixOS, when using the package from flake.nix, both modern AppIndicator trays as well as -# older Xorg-/XEmbed-based trays should be supported. -# See https://oraios.github.io/serena/02-usage/060_dashboard.html -web_dashboard_interface: - -# trusted hosts used to access the web dashboard. -# By default, only allow access via local addresses for security reasons. -# If you want to allow access from remote machines, add the hostname used to access the dashboard to this list. -# If the list is empty/undefined, all hosts are trusted. -web_dashboard_trusted_hosts: -- 127.0.0.1 -- localhost - -# command used to launch a JetBrains IDE on demand (only relevant when using the JetBrains language backend) -# If this is non-empty and, at project activation, the Serena JetBrains Plugin server instance corresponding -# to the project is not found, use this command to spawn a new instance. -# Specifically, if this is , then ` ` is launched. -# Provide the full path to the executable/script (e.g., "/usr/bin/idea" or "C:/Users/bob/AppData/Local/JetBrains/Toolbox/scripts/idea.cmd"). -jetbrains_launch_command: - -# list of glob patterns for project root directories that are considered trusted. -# Some project settings will only be applied if the project is trusted. -# A glob pattern can contain the following: -# * matches any sequence of characters except path separators -# ** matches any sequence of characters including path separators -# ? matches any single character except path separators -# [abc] matches any single character in the set (here: a, b, or c) -# Path separators are normalised internally, so on Windows, both / and \ can be used in the patterns. -# Example: -# trusted_project_path_patterns: -# - /home/user/projects/** -# - C:\Users\Anna\Dev\work\** -# The pattern "**" matches any project path, so it can be used to trust all projects. -trusted_project_path_patterns: [] - -# mapping from language server keys to integer priority values (higher = more preferred), which determine -# language server selection during automatic project creation and are used to break ties when multiple -# language servers match an equal number of source files. -# The priority values override Serena's default priorities: -# * 0 = experimental or secondary language servers, which are never auto-detected -# (e.g. `python_*` and `php_*` are "secondary"; also applies to non-programming languages like `json`) -# * 1 = supersets of other languages, which shall be preferred only if they match more files -# (e.g. `vue` and `svelte`, which are supersets of `typescript`) -# * 2 = default priority -# Example: -# ls_priorities: -# python: 0 -# python_basedpyright: 3 -# This example would achieve that `python_basedpyright` becomes the preferred language server for Python, -# giving it a high priority of 3, while the default Python language server is fully excluded from -# auto-detection with priority 0. -ls_priorities: From 2b35495d31f547e8b2bb0e39cc2cea9fa245d155 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:25:04 +0000 Subject: [PATCH 02/30] =?UTF-8?q?docs:=20#829=20#830=20=E3=81=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E4=BB=95=E6=A7=98=E3=81=A8=E8=A8=AD=E8=A8=88=EF=BC=88?= =?UTF-8?q?=E5=BE=85=E3=81=A1=E6=96=B9=E3=81=AE=20hook=20=E3=81=A8?= =?UTF-8?q?=E4=BC=9A=E8=A9=B1=E3=82=92=E5=88=87=E3=82=8B=20hook=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 410 +++++++++++++++++++++++++++ issues/issue-829-830-requirements.md | 168 +++++++++++ 2 files changed, 578 insertions(+) create mode 100644 issues/issue-829-830-design.md create mode 100644 issues/issue-829-830-requirements.md diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md new file mode 100644 index 000000000..e7e4c17d3 --- /dev/null +++ b/issues/issue-829-830-design.md @@ -0,0 +1,410 @@ +# #829 / #830: 待つ間の問い合わせをやめ、conductor の会話を工程の切れ目で切る — 設計 + +要求と受け入れ条件は [issue-829-830-requirements.md](issue-829-830-requirements.md) にある。 +この文書は「どう作るか」だけを扱う。 + +**実装は 1 本の Pull Request にまとめる**(決定 1)。hook の入口と登録、状態の置き場所、 +拒否の返し方を 2 つの課題で共有するためである。 + +## 何が変わるか(例) + +**#829 の例。** サブエージェントが `codex exec` を背景で起動し、`sleep 60 && tail -5 /tmp/x.log` +を 30 回繰り返すと、30 回とも文脈の全体を読み直す。変更後は、1 回目の `sleep 60 && tail` を +hook が拒否し、理由の欄で「待ちの条件を until ループにして `run_in_background` で起動し、完了通知を待つ」よう案内する。 + +**#830 の例。** conductor の文脈が 41 万のまま `/ndf:design` を起動すると、変更後は hook が +起動を 1 度拒否し、「新しい会話で `/ndf:development-workflow #829` を打つ」と案内する。 +conductor はその 1 行を利用者へ示して止まる。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 待ち方の規約を 1 か所で読む | supervisor / worker / conductor | +| F2 | 前景で `sleep` を使って待つ Bash(ループの待ちと長い `sleep`)を止め、代わりの待ち方を知らせる | Claude Code のエージェント(全層) | +| F3 | 変わっていないファイルの同じ範囲を続けて読み直す Read を止め、代わりの待ち方を知らせる | 同上 | +| F4 | 文脈が上限を超えた conductor の工程 Skill の起動を 1 度止め、新しい会話で打つ 1 行を知らせる | conductor と、それを見る利用者 | +| F5 | 工程を 1 つ終えるたびに、次の工程を始める 1 行を出す | conductor(全ランタイム) | +| F6 | その 1 行から始めた新しい会話で、モード・作業ツリー・現在の工程を戻す | conductor(全ランタイム) | +| F7 | hook を種類ごとに止める・上限を変える | 利用者 | + +## 構成要素 + +| 要素 | 新設 / 変更 | 責務 | +| --- | --- | --- | +| `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(sleep / 連続 Read / 文脈量)へ振り分け、拒否か通過を返す | +| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名)。F4 がこの一覧に無い Skill を見ない | +| `plugins/ndf/hooks/claude.json` | 変更 | PreToolUse に matcher `Bash\|Read\|Skill` で `token-guard.sh` を登録する | +| `development-workflow/references/waiting.md` | 新設 | 待ち方の規約の唯一の置き場所(F1) | +| `development-workflow/references/agent-layers.md` | 変更 | supervisor の規則 4 と worker の規則に、`waiting.md` への参照を 1 行ずつ足す | +| `development-workflow/references/context-window.md` | 変更 | 「前提: 実測ではない」を #827 の実測へ置き換える。hook の上限と引き継ぎの 1 行の節を足す | +| `development-workflow/SKILL.md` | 変更 | 工程を終えるたびに 1 行を出す規約と、新しい会話で戻す手順への参照(F5 / F6) | +| `release/references/completion-check.md` | 変更 | 前景の待ちのループの直前に「Claude Code では `run_in_background` で起動する(`waiting.md`)」の 1 行を足す | +| `plugins/ndf/scripts/tests/test_token_guard.py` | 新設 | F2〜F4 と F7 の判定を、入力 JSON と transcript の見本で確かめる | +| `plugins/ndf/README.md` | 変更 | hook の一覧に `token-guard.sh` を足し、4 ランタイムでの扱いを表で示す | + +```mermaid +graph TB + subgraph CC["Claude Code の会話"] + AG["エージェント
conductor / supervisor / worker"] + end + subgraph HK["hooks/claude.json の PreToolUse"] + WG["worktree-guard.sh
(既存)"] + TG["token-guard.sh"] + end + subgraph ST["状態"] + RS["連続 Read の控え
~/.local/state/ndf/guards/"] + TR["会話の記録
transcript_path"] + SL["token-guard-stages.txt"] + end + subgraph DOC["development-workflow の文書"] + WT["references/waiting.md"] + CW["references/context-window.md"] + AL["references/agent-layers.md"] + SK["SKILL.md"] + end + AG -->|"Bash / Read / Skill"| TG + AG -->|"編集系 / Bash"| WG + TG -->|"Read の判定"| RS + TG -->|"Skill の判定"| TR + TG -->|"Skill の判定"| SL + TG -. "拒否の理由が指す" .-> WT + TG -. "拒否の理由が指す" .-> CW + AL --> WT + SK --> CW +``` + +図にはテスト(`test_token_guard.py`)と `README.md`、`completion-check.md` を含めない。どちらも実行の経路に現れない。 + +### 配置 + +**hook は Claude Code の配布物にだけ登録する**(決定 5)。 + +| ランタイム | 待ち方(#829) | 会話を切る(#830) | 根拠 | +| --- | --- | --- | --- | +| Claude Code | hook(`token-guard.sh`)+ 規約 | hook + 引き継ぎの 1 行 | 代わりの待ち方(`Monitor` / `run_in_background` の通知)と `transcript_path` を持つ | +| Codex | 規約だけ | 引き継ぎの 1 行だけ | `hooks/codex.json` は変えない。背景の起動と完了通知が無く、1 回の前景のループが待ち方になる | +| Kiro | 規約だけ | 引き継ぎの 1 行だけ | 実行前の hook は拒否(exit 2)しか返せず、既存の設計も実行前の hook を置いていない | +| agy | 規約だけ | 引き継ぎの 1 行だけ | 実行前の hook は案内を控えへ積む形で、拒否の口を使っていない | + +### パッケージ構成 + +```text +plugins/ndf/ +├── hooks/claude.json … 登録を 1 つ足す +├── scripts/ +│ ├── token-guard.sh … 新設 +│ ├── lib/token-guard-stages.txt … 新設 +│ └── tests/test_token_guard.py … 新設 +├── skills/development-workflow/ +│ ├── SKILL.md … 引き継ぎの 1 行 +│ └── references/ +│ ├── waiting.md … 新設 +│ ├── agent-layers.md … 参照を 2 行 +│ └── context-window.md … 実測値・上限・引き継ぎ +├── skills/release/references/completion-check.md … waiting.md を指す 1 行 +└── README.md … hook の一覧と 4 ランタイムの表 +``` + +## データ構造 + +**連続 Read の控えを、会話ごとに 1 つの小さなファイルへ持つ。** 置き場所は +`${XDG_STATE_HOME:-$HOME/.local/state}/ndf/guards/read-.json`。既存の通過工程の控え +(`~/.local/state/ndf/stages/`)と同じ親に置く。 + +| キー | 型 | 意味 | +| --- | --- | --- | +| `key` | 文字列 | 直前の Read の `file_path` と `offset` と `limit` を `\t` でつないだもの | +| `size` | 整数 | 直前の Read の時点のファイルの大きさ(バイト)。無いファイルは `-1` | +| `mtime` | 整数 | 同じく更新時刻(秒) | +| `count` | 整数 | `key` と `size` と `mtime` が変わらないまま続いた Read の回数 | + +- **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない +- **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を + hook は受け取らないため +- **文脈量の案内を出した印** は `guards/context-` の空ファイルで持つ(F4 の 2 回目を + 通すため。決定 7) + +## 入出力の契約 + +### hook の入力(Claude Code の PreToolUse) + +| キー | 使う判定 | 無いとき | +| --- | --- | --- | +| `tool_name` | 振り分け(`Bash` / `Read` / `Skill`) | 通す | +| `tool_input.command` / `tool_input.run_in_background` | sleep | 通す | +| `tool_input.file_path` / `offset` / `limit` | 連続 Read | 通す | +| `tool_input.skill` / `tool_input.args` | 文脈量 | 通す | +| `session_id` | 連続 Read の控え・案内の印 | 通す | +| `transcript_path` | 文脈量 | 通す | +| `agent_id`(サブエージェントで付く) | 文脈量(付いていれば見ない) | conductor とみなす。`transcript_path` が `/subagents/` を含めばサブエージェントとみなす | + +### hook の出力 + +**拒否は `permissionDecision: deny` と理由の欄で返し、終了コードは常に 0 にする。** 通すときは +何も出さない。 + +```json +{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", + "permissionDecisionReason":"<下の表の文面>"}} +``` + +| 判定 | 拒否する条件 | 理由の欄の文面(要旨) | +| --- | --- | --- | +| sleep | `run_in_background` が真でなく、`command` が `\bsleep\s+[0-9]` に当たり、かつ次のどちらかに当たる: `\b(while\|until)\b` を含む / `sleep` に渡した秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | +| 連続 Read | 直前の Read と `key`・`size`・`mtime` が同じで、`count + 1` が上限(既定 3)に達する | 「同じファイルの同じ範囲を、変わらないまま 回続けて読もうとした。書き終わりを待つなら `until [ -s <ファイル> ]; do sleep 1; done` を `run_in_background: true` で起動するか、背景の処理の完了通知を待つ。サブエージェントの `tasks/*.output` は読まずに完了通知を待つ。規約: 同上」 | +| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、この会話で案内の印が無い | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill をもう一度起動すると通る。規約: `context-window.md`」 | + +**`<課題>` は次の順で決める。** 先に当たったものを使う。 + +| 順 | 読むもの | 取り出す値 | +| --- | --- | --- | +| 1 | Skill の `args` | `#<数>` と数だけの語 | +| 2 | このリポジトリの通過工程の控え(`~/.local/state/ndf/stages/<所有者>__<リポジトリ>__<番号>.json`) | 更新時刻が最も新しい控えの番号 | +| 3 | どちらも無い | `<課題番号>` の文字のまま | + +### 環境変数 + +| 変数 | 既定 | 意味 | +| --- | --- | --- | +| `NDF_SLEEP_GUARD` | `1` | `0` で sleep の判定を止める | +| `NDF_SLEEP_MAX_SEC` | `5` | ループの外で通す `sleep` の秒数の上限 | +| `NDF_READ_REPEAT_GUARD` | `1` | `0` で連続 Read の判定を止める | +| `NDF_READ_REPEAT_LIMIT` | `3` | 連続 Read を拒否する回数 | +| `NDF_CONTEXT_GUARD` | `1` | `0` で文脈量の判定を止める | +| `NDF_CONTEXT_LIMIT` | `200000` | 文脈量の上限(トークン) | + +### 文脈量の読み方 + +**`transcript_path` の末尾 200 行から、最後の assistant 行の `message.usage` を読み、 +`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` を文脈量とする。** +`transcript_agents.py` の `_input_total` と `statusline.sh` と同じ足し方である。末尾だけを読むのは、 +50 MB の記録でも 1 秒以内に終えるためである(非機能の条件)。 + +### 引き継ぎの 1 行(F5 / F6) + +**形は `/ndf:development-workflow #<課題> [#<課題> ...]` とする**(決定 8)。Claude Code と agy の +起動の書き方である。Codex と Kiro では、それぞれの README が示す Skill の起動の書き方に読み替える。 + +**新しい会話の `development-workflow` は、次の順で状態を戻す。** 手順は `context-window.md` の +新しい節に置き、`SKILL.md` からはそこを指す。 + +| # | 読むもの | 戻すもの | +| --- | --- | --- | +| 1 | 課題の本文の `## 進行` | モード・作業ツリー・計画ファイル・通った工程 | +| 2 | `stage-check.sh report <番号>` | 通過工程の控え(本文と食い違えば控えを正とする) | +| 3 | `gh pr list --search "<番号>" --state all` | 設計・実装の Pull Request と状態 | +| 4 | 1〜3 から、チェックの付いていない最初の必須の工程 | 次に起動する工程 Skill | + +### 文書の中身 + +**`waiting.md`(新設)** は次の 5 つを持つ。他の文書は写さずにここを指す。 + +| 節 | 中身 | +| --- | --- | +| 待ちの費用 | 呼び出し 1 回ごとに文脈の全体を読み直す。#827 の実測(全体の 16%)と決定 3 の表 | +| 許す待ち方 | Claude Code: 条件の until ループを `run_in_background` で起動して完了通知を 1 回受ける / 出来事を 1 つずつ受けるなら `Monitor` / サブエージェントは完了通知。他の 3 ランタイム: 1 回の前景の until ループ(600 秒を超えるなら `bg-wait.sh`) | +| 禁じる待ち方 | `sleep` を挟んだ呼び出しの繰り返し / 出力ファイルの繰り返しの読み直し / サブエージェントの `tasks/*.output` を読むこと | +| 待つ相手ごとの手 | サブエージェント → 完了通知。背景の CLI → CLI そのものを `run_in_background` で起動する。既に起動したプロセス → `until` で終わりを待つループを `run_in_background` で。Pull Request の検査 → `gh pr checks --watch` を `run_in_background` で。新しいコメントを 1 件ずつ → `Monitor` | +| hook | `token-guard.sh` の条件と止め方(環境変数)。4 ランタイムの表 | + +**`context-window.md`(変更)** は 3 か所を変える。 + +| 場所 | 変え方 | +| --- | --- | +| 「前提: 実測ではない」(:41-42) | #827 の実測に置き換える: conductor の最大文脈は 2026-09-20 以降 7 件中 4 件で 20 万超・最大 68 万、ai-plugins の 30 日間で 45 件中 37 件が 20 万超・平均 41 万、工程の開始ごとに切れば再読込量が 58%(30 日間 62%)減る。出典は #827 | +| 新しい節「上限を超えたら hook が止める」 | 上限の既定(200,000)と `NDF_CONTEXT_LIMIT`・1 度だけ止めること・続けたいときの手 | +| 新しい節「新しい会話で戻す」 | 引き継ぎの 1 行の形と、戻す手順の表(上の 4 行) | + +**`SKILL.md`(変更)** は「工程は 1 つの context window で通し切らなくてよい」の段落に 2 文を足す。 +工程を 1 つ終えるたびに引き継ぎの 1 行を出すこと、戻す手順は `context-window.md` にあること。 + +**`agent-layers.md`(変更)** は supervisor の規則 4 の後ろに「待ち方は `waiting.md` に従う」を足し、 +worker の規則に同じ 1 行を 5 番目として足す。 + +## 処理の流れ + +```mermaid +sequenceDiagram + participant A as エージェント + participant H as token-guard.sh + participant S as 控え / 記録 + A->>H: PreToolUse(tool_name, tool_input, session_id, transcript_path) + alt 入力が読めない・jq が無い・該当の NDF_*_GUARD=0 + H-->>A: 何も出さず 0(通す) + else tool_name = Bash + H->>H: 背景か / sleep の秒数 / while・until を含むか + H-->>A: 当たれば拒否(待ち方の案内) + else tool_name = Read + H->>S: 控えを読む・ファイルの size と mtime を取る + H->>S: 控えを置き換える(count を進めるか 1 に戻す) + H-->>A: count が上限に達すれば拒否 + else tool_name = Skill + H->>H: 工程 Skill か / サブエージェントか + H->>S: transcript の末尾から文脈量を読む + H->>S: 案内の印が無ければ作る + H-->>A: 上限超え かつ 印が無かったなら拒否(1 行を示す) + end +``` + +連続 Read の控えの状態: + +```mermaid +stateDiagram-v2 + [*] --> 一回目: 控えが無い / key が違う / size か mtime が変わった + 一回目 --> 繰り返し: 同じ key・size・mtime + 繰り返し --> 繰り返し: 同じ(count が上限未満) + 繰り返し --> 拒否: count が上限に達する + 拒否 --> 拒否: 同じまま読み直す + 繰り返し --> 一回目: 変わった / 別の key + 拒否 --> 一回目: 変わった / 別の key +``` + +**拒否した Read も控えの `count` を進める。** 進めないと、拒否された後に同じ Read を続けても +拒否の回数が増えるだけで、状態が変わらない(どちらでも拒否が続くため振る舞いは同じだが、控えの +値が「試みた回数」を表すようにそろえる)。 + +## 非機能の実現方式 + +| 大項目 | 条件 | 実現方式 | +| --- | --- | --- | +| 性能・拡張性 | 50 MB の記録でも 1 秒以内 | 記録は `tail -n 200` の範囲だけを読む。Bash と Read の判定は記録を読まない。登録の `timeout` は 5 秒 | +| 運用・保守性 | 理由の欄だけで次の手が分かる | 理由の欄に代わりの手段と規約の場所を必ず書く(出力の表) | +| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。登録に `continueOnError: true` | + +## 決定の記録 + +### 決定 1: 2 つの課題を 1 本の実装 Pull Request にまとめる + +hook の入口・登録・状態の置き場所・拒否の返し方が同じで、分けると `hooks/claude.json` と +テストの土台を 2 本が同時に触る。2 本に分ける形は採らない。後に入る側が土台の食い違いを解く +手間が、分けて読みやすくなる利点を上回る。 + +### 決定 2: 待ち方の規約は `development-workflow/references/waiting.md` の新しいファイルに置く + +`agent-layers.md` は #828(持ち場ごとの抜粋)も同時に触る。待ち方の規約は、 +#731(`bg-wait.sh` を共通層へ移す)と external-ai / cross-review の文書からも参照される。 +同じファイルの節にすると、参照するたびに 3 層の規約の全体を読ませる。`agent-layers.md` と `parallel-work.md` の節に置く形は +採らない。`parallel-work.md` は待ち方の道具に触れておらず、そこへ足す理由が無い。 + +### 決定 3: sleep の判定は「前景で、`while` / `until` のループを含むか、5 秒を超える `sleep`」を拒否する + +ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 +(6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 + +| 形 | 件数 | 費用(input 換算) | +| --- | ---: | ---: | +| 前景・ループの中(`while [ $n -lt 40 ]; do ...; sleep ...; done` など) | 1,543 | 67.3M | +| 前景・ループなし(`sleep 30 && tail x` など) | 743 | 9.9M | +| 背景(`run_in_background`) | 441 | 5.3M | + +ループの 1 回の呼び出しは 600 秒で打ち切られ、待ちが長いと呼び直しが続く。同じループを背景へ移せば、 +待つ時間の長さによらず完了通知 1 回で済む。ループなしの形の半数(382 件)は 5 秒以下で、サーバの起動を +待つような短い間であるため通す。`for` のループで 5 秒以下の `sleep` を挟む形(API の照会の +間隔を空ける使い方)も通す。 + +ループの中の `sleep` をすべて通す形は採らない。前景の `sleep` の費用の 87% を占める形が残る。`sleep` を含む +Bash をすべて拒否する形も採らない。短い間と照会の間隔まで止めると、代わりの手段が無い。 +配布物の文書にある前景の待ちのループも拒否に当たる。理由の欄が「同じループを +`run_in_background: true` で」と案内するため、Claude Code ではループを書き換えずに 1 回の回り道で済む。 + +| 文書 | ループ | 書き換える変更 | +| --- | --- | --- | +| `external-ai/references/cli-codex.md` / `cli-agy.md` | `until ! ps -p ...; do sleep 30; done` | #731 | +| `qa-security-scan/03-report-template.md` | `until grep -q ...; do sleep 30; done` | #731 | +| `release/references/completion-check.md` | `while :; do ...; sleep 5; done` | この変更(`waiting.md` を指す 1 行) | + +### 決定 4: 連続 Read は「同じ範囲・変わらないファイル・3 回目」で拒否する + +hook は実行の前に呼ばれ、読んだ中身を知らない。そのため「空ファイル」ではなく「前回から +大きさと更新時刻が変わっていない」で判定する。空ファイルの読み直しはこれに含まれ、書き込みが +進むログの読み直しは含まれない。`offset` と `limit` を鍵に入れるのは、大きなファイルを範囲を +変えて読み進める正当な使い方を止めないためである。 + +3 回目にするのは、2026-08-23 以降の記録の実測による。同じ引数の Read が 3 回以上続いたのは 6 本で、 +うち 5 本が `tasks/*.output` の読み直し(最長 1,168 回)だった。残る 1 本は画像を見直す 3 回である。 +2 回目で止めると、正当な見直しに当たる機会が増える。間に他のツールが挟まったら数え直す形は +採らない。hook は Read の呼び出しにしか登録されず、間のツールを見られない。 + +### 決定 5: hook は Claude Code にだけ登録し、他の 3 ランタイムは規約で守る + +拒否の理由が案内する代わりの手段(`Monitor` / `run_in_background` の通知)は Claude Code にしか +無い。文脈量も Claude Code の `transcript_path` からしか読めない。#827 の実測も Claude Code の +記録だけで、Codex / Kiro / agy の消費は測っていない。3 ランタイムへも登録する形は採らない。 +Kiro は拒否すると代わりの口を持たず、agy は案内を控えへ積む形で、どちらも同じ案内を出せない。 +CLI 側の消費を測った後(#827 の次の手順)に改めて決める。 + +### 決定 6: 文脈量の判定は conductor の工程 Skill の起動だけに掛ける + +supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が途中で +途切れる。supervisor の切れ目は #768 / #773 が測ってから決める。工程でない Skill +(`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 +されるため、切れ目にならない。 + +### 決定 7: 文脈量の案内は会話ごとに 1 度だけ拒否し、2 回目は通す + +利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 +毎回拒否する形は採らない。関門の直前など、切ると判断の材料を失う場面で進めなくなる。 +拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` +に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 + +### 決定 8: 引き継ぎの 1 行は `development-workflow` を起動する形にする + +工程 Skill を直接起動する形(`/ndf:design #829`)は採らない。工程 Skill はモード・作業ツリー・ +承認の状態を戻す手順を持たず、戻す手順を持つのは `development-workflow` の側だからである。 +`development-workflow` を経由すると固定費に 1 回分の読み込み(約 1 万トークン)が足されるが、 +切る前の会話の文脈(#827 で平均 41 万)に比べて小さい。 + +### 決定 9: 上限の既定は 200,000 にし、`skill-stats` の既定と同じ値にする + +`context-window.md` の「遅くとも 20 万」と、`skill-stats.py` の `DEFAULT_WINDOW_LIMIT` が同じ値を +持つ。hook だけ別の値にすると、測る側と止める側の上限が食い違う。10 万(目安の側)にする形は +採らない。#827 で固定費だけで約 4 万あり、1 工程の途中で止まる回数が増える。 + +### 決定 10: 1 回で足りる待ちは `Monitor` ではなく `run_in_background` の until ループにする + +#829 は「`Monitor` の until で 1 回だけ待つ」を挙げた。一方、Claude Code 2.1.280 の `Monitor` の +説明は、道具を次のように使い分ける。 + +| 待ち方 | 使う道具 | +| --- | --- | +| 通知が 1 回で足りる(終わるのを待つ) | `run_in_background` の until ループ | +| 出来事を 1 つずつ受ける | `Monitor` | +`Monitor` は既定 5 分・最長 30 分で打ち切られ、張り直しが要る。 +`Monitor` を 1 回の待ちの既定にする形は採らない。張り直しのたびに呼び出しが増える。 + +## テスト設計 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | +| AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done` で deny と理由の欄に `run_in_background` と `waiting.md` | +| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep`・`tool_name: Monitor` で出力なし | +| AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し | +| AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | +| AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | +| AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える | +| AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | +| AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829` | +| AC13 | 単体: 同じ入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | +| AC14 | 単体: `ndf:markdown-writing` → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と一致することを文書テストで確かめる | +| AC15 | 単体: 同じ `session_id` で 2 回 → 1 回目 deny、2 回目は出力なし。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | +| AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | +| AC17 | AC12〜AC16 のテストが通る | +| AC18 / AC19 | 文書の検査: `SKILL.md` に 1 行を出す規約があり、`context-window.md` に戻す手順の表がある | +| AC20 | 実機: 実装の Pull Request の途中で会話を切り、1 行だけで新しい会話を始め、モード・作業ツリー・次の工程が戻ったことを #830 に残す | +| AC21 / AC22 | 文書の検査: 「実測ではない」の文面が消え、#827 への参照と数値があり、上限の値が `NDF_CONTEXT_LIMIT` の既定と一致する | +| AC23 | 文書の検査: README に 4 ランタイムの表がある | +| AC24 | 既存: `plugins/ndf/skills/worktree/tests/` と `scripts/tests/test_agy_install_hooks.py` が通る | +| AC25 / AC26 | 配布後: `release-verification` で #827 の `measure.py` / `poll.py` / `extra.py` を変更前と同じ引数で回し、変更前の値(全体の 16%、conductor の最大 683k・再読込の削減見込み 58%)と並べて各 issue に残す | + +## 未確認のまま残ること + +| 項目 | 内容 | いつ決まるか | +| --- | --- | --- | +| サブエージェントの hook 入力 | Claude Code 2.1.280 の PreToolUse の入力に `agent_id` が付くか、`transcript_path` がサブエージェントの記録を指すか。どちらも付かなければ conductor とサブエージェントを区別できない | 実装の最初のタスクで、入力を書き出すだけの hook を登録して確かめる | +| 記録の書き込みの時点 | PreToolUse が呼ばれた時点で、その Skill を呼んだ assistant 行が記録に書かれているか。書かれていなければ 1 つ前の呼び出しの文脈量を読む(差は 1 回分の出力と結果) | 同上 | +| Codex / Kiro の起動の書き方 | 引き継ぎの 1 行を、各ランタイムでどう書くか | 実装で各 README の記載に合わせる | +| 通知の届き方 | サブエージェントが背景の処理を残したまま応答を終えたとき、完了通知で再開されるか、親へ完了として返るか(AC11)。Agent の完了通知の注記「背景の子を持たずに止まるたびに通知する」からは再開されると読めるが、Bash の背景の処理で確かめていない | 実装の Pull Request の実機確認 | +| 背景の Bash の上限 | `run_in_background` の Bash に 600 秒の打ち切りが掛かるか。掛かるなら 600 秒を超える待ちは `bg-wait.sh`(#731) | 同上 | +| 効果の数値 | ポーリングの割合と conductor の再読込量がどれだけ下がるか | 配布後の `release-verification` | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md new file mode 100644 index 000000000..2c1f3e30e --- /dev/null +++ b/issues/issue-829-830-requirements.md @@ -0,0 +1,168 @@ +# #829 / #830: 待つ間の問い合わせをやめ、conductor の会話を工程の切れ目で切る — 要求と受け入れ条件 + +設計は [issue-829-830-design.md](issue-829-830-design.md) にある。この文書は「何を満たすか」だけを扱う。 + +親は #827(トークン消費の実測)。マイルストーンは「17 トークン消費の削減」。 + +## 依頼(原文) + +#829: + +> **待つ間の繰り返し問い合わせ(ポーリング)をやめる。** 待ち方は次の 2 つにそろえる。 +> +> - `run_in_background` で起動し、完了の通知を待つ +> - `Monitor` の until で、条件が満たされるまで 1 回だけ待つ +> +> `sleep` を挟んだループと、出力ファイルの繰り返しの読み直しは、規約で禁止し、hook でも止める。 + +> ## 受け入れ条件 +> +> - [ ] 待ち方の規約が 1 か所にあり、supervisor / worker への指示の雛形から参照されている +> - [ ] hook が、空ファイルへの連続 Read と `sleep` ループを拒否し、代わりの待ち方を案内する(テストがある) +> - [ ] 通知が届かない経路(サブエージェントの中から起動した CLI など)で待ちが止まらないことを、実機で確かめている +> - [ ] #827 の計測スクリプトで、変更後の版を同じ条件で回すと、ポーリングの費用の割合が下がっている(数値を本 issue に残す) + +#830: + +> **conductor の会話を、工程の切れ目で切ることを強制する。** `references/context-window.md` は「1 工程あたり 10 万、遅くとも 20 万で切る」と定めているが、実際には守られていない。規定を書くだけでは足りないので、仕組みで切らせる。 + +> ## 受け入れ条件 +> +> - [ ] 文脈が上限を超えた状態で工程 Skill を起動すると、hook が新しい会話へ移るよう案内する(テストがある) +> - [ ] 工程の終わりで出す引き継ぎの 1 行だけで、新しい会話から工程を再開できる(課題・PR・控えから状態を戻せる) +> - [ ] `context-window.md` の「前提: 実測ではない」を、#827 の実測値に書き換えている +> - [ ] #827 の計測スクリプトで、変更後の版を同じ条件で回すと、conductor の最大文脈と再読込量が下がっている(数値を本 issue に残す) + +進行側からの補足(設計の持ち場の指示): + +> 1 本の設計 Pull Request に両方の設計を載せる(hook の設定と待ち方・会話を切る規約を共有するため)。範囲は #829 と #830 だけ。 +> 受け入れ条件の数値比較はリリース後に行う前提で書いてよい。 +> 4 ランタイム(Claude Code / Codex / Kiro / agy)での扱いも設計で触れる。 + +## 目的 + +- **待つ間に文脈を読み直す呼び出しを減らす。** #827 の実測で、ポーリングは全体の費用の 16%(2026-09-20 以降)、ai-plugins の 30 日間では 19%(258M)を占めた +- **conductor の文脈を工程の切れ目で捨てさせる。** 工程の開始ごとに切っていれば、conductor の再読込量は 58%(30 日間では 62%)減る見込みである + +## 前提 + +- 前提 1: 待ちの費用は「呼び出しの回数 × その時点の文脈」で決まる。**背景で待つ(`run_in_background` の完了通知、`Monitor` の出来事の通知)なら、待つ時間の長さは費用を増やさない** +- 前提 2: Claude Code 2.1.280 は、Bash の説明文で前景の `sleep` を禁じると書くが、止めていない。サブエージェントの中で `sleep 12 && echo` を実行すると 12 秒待って成功した(2026-09-23 実測)。**本体の仕組みには頼れない** +- 前提 3: 会話の文脈量は、会話の記録(transcript)の最後の assistant 呼び出しの `usage` から読める。`skill-stats --agents` と同じ読み方をする +- 前提 4: 文脈量の上限の既定は 200,000 トークンとする(`context-window.md` の「遅くとも 20 万」)。利用者が環境変数で変えられる +- 前提 5: 受け入れ条件の数値の比較(#829 の 4 つ目、#830 の 4 つ目)は、変更を配布した後に `release-verification` で行う。設計と実装の Pull Request では、比べる手順と比べる前の値を固定するまでを扱う +- 前提 6: 待ちの道具を共通層へ移す #731 とその子(#656 / #345)は、同じ規約の節を参照先として使う。道具の移動はそちらで行う + +## 対象範囲 + +含む: + +- 待ち方の規約の置き場所を 1 つに決め、supervisor / worker への指示の雛形から参照させる(#829) +- PreToolUse hook で、前景の `sleep` で待つ Bash(ループの待ちと長い `sleep`)と、変わらないファイルの同じ範囲への連続 Read を拒否し、代わりの待ち方を案内する(#829) +- PreToolUse:Skill hook で、会話の文脈量が上限を超えた状態の工程 Skill の起動を止め、新しい会話で始める 1 行を案内する(#830) +- `development-workflow` が工程を 1 つ終えるたびに、次に打つコマンドを 1 行で出す規約(#830) +- `context-window.md` の「前提: 実測ではない」を #827 の実測値へ書き換える(#830) +- 4 ランタイムでの扱い(hook が効くランタイムと、規約だけで守るランタイムの区別) + +含まない: + +- Skill 本文を持ち場ごとの抜粋で渡す #828、仕事を分ける手法の #680 +- 待ちの道具(`bg-wait.sh`)を共通層へ移す #731、サブエージェントが待ちで止まる #656、上限の無い待ち #345 +- supervisor のスクリプト駆動(#827 の「方針」) +- codex / kiro / agy の CLI 側の消費の計測(#827 で対象外) +- supervisor / worker の文脈量の上限(#768 / #773)。この変更の hook は conductor の工程 Skill の起動だけを見る +- 設計の成果物のうちクラス図: 作るのはシェルの hook と文書だけで、型を持たないため対象が無い + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| ポーリング | 待つ間に、状態を確かめるための呼び出しを繰り返すこと。#827 の `poll.py` は `sleep <数字>` を含む Bash、`tasks/*.output` の Read、出力ファイルやログの `tail` / `cat` / `wc` / `grep` を数える | +| 前景の Bash | `run_in_background` を付けずに実行する Bash。終わるまで呼び出しが返らない | +| 文脈量 | 1 回の API 呼び出しで読んだトークン数。`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` | +| 工程 Skill | `development-workflow` の工程表が起動する Skill(`requirements-design` / `design` / `pr` など) | +| 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行 | + +## 受け入れ条件 + +### 待ち方の規約(#829) + +- [ ] AC1: 待ち方の規約が `development-workflow/references/` の 1 か所にあり、他の文書は同じ内容を写さず、その節を指す +- [ ] AC2: supervisor への起動指示の雛形と worker への指示の雛形が、AC1 の節を参照している +- [ ] AC3: 規約が許す待ち方と禁じる待ち方を挙げている。許すのは、`run_in_background` で起動した until ループの完了通知・出来事を 1 つずつ受ける `Monitor`・hook の効かないランタイムでの 1 回の前景の until ループ。禁じるのは、`sleep` を挟んだ呼び出しの繰り返しと、出力ファイルの繰り返しの読み直し +- [ ] AC4: 規約と指示の雛形に、Claude Code で前景の `sleep` ループを勧める文面が無い(Claude Code では `run_in_background` と `Monitor` を指示する) + +### 待ちの hook(#829) + +- [ ] AC5: Claude Code の PreToolUse hook が、前景で `sleep` を使って待つ Bash を拒否する。対象は、`sleep` に数値の秒数を渡し、`while` / `until` のループを含むか秒数が上限(既定 5 秒)を超えるもの。理由の欄に代わりの待ち方(同じループを `run_in_background` で起動して完了通知を待つ / `Monitor`)を示す +- [ ] AC6: `run_in_background: true` の Bash、`Monitor` ツールの中の `sleep`、`sleep` を含まない Bash、ループの外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep` は拒否しない +- [ ] AC7: 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさと更新時刻が変わらないまま、その会話で連続して上限の回数(既定 3)に達すると、hook が拒否し、代わりの待ち方を示す。別の引数の Read が挟まるか、ファイルが変われば数え直す +- [ ] AC8: AC5〜AC7 の判定を、入力 JSON を与えて終了コードと出力を見るテストが確かめている(拒否する例と通す例の両方) +- [ ] AC9: hook の判定が失敗しても(入力 JSON が壊れている・記録を書けない)、ツールの実行を止めない(終了コード 0 で通す) +- [ ] AC10: 利用者が環境変数で hook を止められる(`sleep` の拒否と連続 Read の拒否を別々に)。`sleep` の秒数の上限と連続 Read の回数を変えられる + +### 通知の届かない経路(#829) + +- [ ] AC11: サブエージェントが `run_in_background` で起動した CLI(`codex exec` など)の完了を待つ間、完了として親へ返らず、完了通知で再開される。これを実機で確かめ、手順と結果を本 issue に残している + +### 会話を切る hook(#830) + +- [ ] AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で工程 Skill を起動すると、PreToolUse:Skill hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す +- [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない +- [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` など)の起動は拒否しない +- [ ] AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる) +- [ ] AC16: 文脈量を読めない(transcript が無い・`usage` が無い)ときは拒否しない +- [ ] AC17: AC12〜AC16 の判定を、transcript の見本を与えて終了コードと出力を見るテストが確かめている + +### 引き継ぎの 1 行(#830) + +- [ ] AC18: `development-workflow` は、工程を 1 つ終えるたびに、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す +- [ ] AC19: そのコマンド 1 行だけで始めた新しい会話が、課題の本文の `## 進行`・Pull Request・通過工程の控えから、モード・作業ツリー・現在の工程を戻せる。戻す手順が文書にある +- [ ] AC20: AC19 を、実際の課題 1 件で新しい会話から再開して確かめ、結果を本 issue に残している + +### 文書(#830) + +- [ ] AC21: `context-window.md` の「前提: 実測ではない」が、#827 の実測値(最大文脈・平均文脈・超過件数・再読込の削減見込み)と出典に置き換わっている +- [ ] AC22: `context-window.md` の上限の値と、hook の既定の上限が一致している + +### 4 ランタイム + +- [ ] AC23: hook が効くランタイムと、規約だけで守るランタイムが、設計文書と配布物の README に表で示されている +- [ ] AC24: Codex / Kiro / agy の配布物で、既存の hook の動作が変わらない(既存のテストが通る) + +### 効果の確認(リリース後) + +- [ ] AC25: 変更を配布した後、#827 の `measure.py` / `poll.py` を変更前と同じ条件(全プロジェクト、同じ集計の定義)で回し、ポーリングの費用の割合を #829 に残している +- [ ] AC26: 同じく、conductor の最大文脈と再読込量を #830 に残している + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | hook 1 回の実行は、transcript が 50 MB でも 1 秒以内に終わる | +| 運用・保守性 | 拒否の理由の欄だけで、エージェントが次に何をすればよいか分かる(代わりの手段を具体的に書く) | +| 可用性 | hook の失敗でツールの実行を止めない(AC9 / AC16) | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | Claude Code の hook の定義に PreToolUse の matcher(`Bash` / `Read` / `Skill`)の登録が増える。環境変数が増える(止める・上限を変える) | +| データ | 連続 Read を数える状態を、会話ごとに一時ファイルへ持つ | +| 既存の振る舞い | 前景の `sleep` で待つ Bash(`while` / `until` のループか、5 秒を超える `sleep`)が拒否される。文脈が上限を超えた conductor では工程 Skill の起動が 1 度拒否される | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --with pytest pytest scripts/tests plugins/ndf -q` | +| 静的解析 | `python3 scripts/check-skill-frontmatter.py`、`claude plugin validate .`(終了コードで判定) | +| 手動確認 | AC11 / AC20 は実機で行い、手順と結果を issue に残す。AC25 / AC26 は配布後に `release-verification` で行う | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存の hook のテストを通す。拒否の理由に代わりの手段を書く | +| 確認してから行う | 既存の hook の登録順・matcher の変更。上限の既定値の変更 | +| 行わない | #731 / #656 / #345 / #828 の範囲の変更。Claude Code 本体の設定の書き換え | From a85e0d5bbaff1695cd646c0cc5e51ee4aa2823ff Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:32:45 +0000 Subject: [PATCH 03/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=201=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88sleep=20=E3=81=AE=E8=AA=9E=E3=81=AE?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E3=83=BB=E6=A1=88=E5=86=85=E3=81=AE=E5=8D=B0?= =?UTF-8?q?=E3=83=BBmtime=20=E3=81=AE=E7=B2=BE=E5=BA=A6=E3=83=BB=E6=96=87?= =?UTF-8?q?=E6=9B=B8=E3=81=AE=E9=A0=86=E5=BA=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 73 ++++++++++++++++++---------- issues/issue-829-830-requirements.md | 5 +- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index e7e4c17d3..f8fd10a50 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -39,7 +39,7 @@ conductor はその 1 行を利用者へ示して止まる。 | `development-workflow/references/agent-layers.md` | 変更 | supervisor の規則 4 と worker の規則に、`waiting.md` への参照を 1 行ずつ足す | | `development-workflow/references/context-window.md` | 変更 | 「前提: 実測ではない」を #827 の実測へ置き換える。hook の上限と引き継ぎの 1 行の節を足す | | `development-workflow/SKILL.md` | 変更 | 工程を終えるたびに 1 行を出す規約と、新しい会話で戻す手順への参照(F5 / F6) | -| `release/references/completion-check.md` | 変更 | 前景の待ちのループの直前に「Claude Code では `run_in_background` で起動する(`waiting.md`)」の 1 行を足す | +| `external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` | 変更 | 前景の待ちのループの直前に「Claude Code では、このループを `run_in_background: true` で実行して完了通知を待つ(`development-workflow/references/waiting.md`)」の 1 行を足す(決定 3) | | `plugins/ndf/scripts/tests/test_token_guard.py` | 新設 | F2〜F4 と F7 の判定を、入力 JSON と transcript の見本で確かめる | | `plugins/ndf/README.md` | 変更 | hook の一覧に `token-guard.sh` を足し、4 ランタイムでの扱いを表で示す | @@ -74,7 +74,7 @@ graph TB SK --> CW ``` -図にはテスト(`test_token_guard.py`)と `README.md`、`completion-check.md` を含めない。どちらも実行の経路に現れない。 +図にはテスト(`test_token_guard.py`)と `README.md`、案内の 1 行を足す 4 文書を含めない。いずれも実行の経路に現れない。 ### 配置 @@ -102,6 +102,10 @@ plugins/ndf/ │ ├── waiting.md … 新設 │ ├── agent-layers.md … 参照を 2 行 │ └── context-window.md … 実測値・上限・引き継ぎ +├── skills/external-ai/references/ +│ ├── cli-codex.md … waiting.md を指す 1 行 +│ └── cli-agy.md … waiting.md を指す 1 行 +├── skills/qa-security-scan/03-report-template.md … waiting.md を指す 1 行 ├── skills/release/references/completion-check.md … waiting.md を指す 1 行 └── README.md … hook の一覧と 4 ランタイムの表 ``` @@ -116,14 +120,16 @@ plugins/ndf/ | --- | --- | --- | | `key` | 文字列 | 直前の Read の `file_path` と `offset` と `limit` を `\t` でつないだもの | | `size` | 整数 | 直前の Read の時点のファイルの大きさ(バイト)。無いファイルは `-1` | -| `mtime` | 整数 | 同じく更新時刻(秒) | -| `count` | 整数 | `key` と `size` と `mtime` が変わらないまま続いた Read の回数 | +| `mtime` | 文字列 | 同じく更新時刻(ナノ秒の精度。GNU の `stat -c %.9Y`、BSD の `stat -f %Fm`) | +| `inode` | 整数 | 同じく inode 番号(`stat -c %i`)。無いファイルは `-1` | +| `count` | 整数 | `key`・`size`・`mtime`・`inode` が変わらないまま続いた Read の回数 | - **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない - **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を hook は受け取らないため -- **文脈量の案内を出した印** は `guards/context-` の空ファイルで持つ(F4 の 2 回目を - 通すため。決定 7) +- **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の `skill` と + `args` を持つ。次の起動が同じ `skill`・`args` なら 1 度だけ通し、印を消す。別の工程 Skill の + 起動は、上限を超えていれば再び拒否する。工程の切れ目ごとに 1 度ずつ止まる(決定 7) ## 入出力の契約 @@ -151,9 +157,9 @@ plugins/ndf/ | 判定 | 拒否する条件 | 理由の欄の文面(要旨) | | --- | --- | --- | -| sleep | `run_in_background` が真でなく、`command` が `\bsleep\s+[0-9]` に当たり、かつ次のどちらかに当たる: `\b(while\|until)\b` を含む / `sleep` に渡した秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | -| 連続 Read | 直前の Read と `key`・`size`・`mtime` が同じで、`count + 1` が上限(既定 3)に達する | 「同じファイルの同じ範囲を、変わらないまま 回続けて読もうとした。書き終わりを待つなら `until [ -s <ファイル> ]; do sleep 1; done` を `run_in_background: true` で起動するか、背景の処理の完了通知を待つ。サブエージェントの `tasks/*.output` は読まずに完了通知を待つ。規約: 同上」 | -| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、この会話で案内の印が無い | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill をもう一度起動すると通る。規約: `context-window.md`」 | +| sleep | `run_in_background` が真でない。判定の前に `command` からコメント(引用の外の `#` 以降)・引用文字列(`'…'` と `"…"`)・ヒアドキュメントの本文を取り除く。残りで `sleep <数>` が**コマンドの位置**(行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後)にあり、次のどちらかに当たる: `while` / `until` がコマンドの位置にある / その `sleep` の秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | +| 連続 Read | 直前の Read と `key`・`size`・`mtime`・`inode` が同じで、`count + 1` が上限(既定 3)に達する | 「同じファイルの同じ範囲を、変わらないまま 回続けて読もうとした。書き終わりを待つなら `until [ -s <ファイル> ]; do sleep 1; done` を `run_in_background: true` で起動するか、背景の処理の完了通知を待つ。サブエージェントの `tasks/*.output` は読まずに完了通知を待つ。規約: 同上」 | +| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、案内の印が同じ `skill`・`args` を持たない | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill を同じ引数でもう一度起動すると 1 度だけ通る。規約: `context-window.md`」 | **`<課題>` は次の順で決める。** 先に当たったものを使う。 @@ -213,7 +219,7 @@ plugins/ndf/ | 場所 | 変え方 | | --- | --- | | 「前提: 実測ではない」(:41-42) | #827 の実測に置き換える: conductor の最大文脈は 2026-09-20 以降 7 件中 4 件で 20 万超・最大 68 万、ai-plugins の 30 日間で 45 件中 37 件が 20 万超・平均 41 万、工程の開始ごとに切れば再読込量が 58%(30 日間 62%)減る。出典は #827 | -| 新しい節「上限を超えたら hook が止める」 | 上限の既定(200,000)と `NDF_CONTEXT_LIMIT`・1 度だけ止めること・続けたいときの手 | +| 新しい節「上限を超えたら hook が止める」 | 上限の既定(200,000)と `NDF_CONTEXT_LIMIT`・工程の切れ目ごとに 1 度止めること・続けたいときの手 | | 新しい節「新しい会話で戻す」 | 引き継ぎの 1 行の形と、戻す手順の表(上の 4 行) | **`SKILL.md`(変更)** は「工程は 1 つの context window で通し切らなくてよい」の段落に 2 文を足す。 @@ -233,17 +239,23 @@ sequenceDiagram alt 入力が読めない・jq が無い・該当の NDF_*_GUARD=0 H-->>A: 何も出さず 0(通す) else tool_name = Bash - H->>H: 背景か / sleep の秒数 / while・until を含むか + H->>H: 背景か / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と while・until H-->>A: 当たれば拒否(待ち方の案内) else tool_name = Read - H->>S: 控えを読む・ファイルの size と mtime を取る + H->>S: 控えを読む・ファイルの size と mtime と inode を取る H->>S: 控えを置き換える(count を進めるか 1 に戻す) H-->>A: count が上限に達すれば拒否 else tool_name = Skill H->>H: 工程 Skill か / サブエージェントか H->>S: transcript の末尾から文脈量を読む - H->>S: 案内の印が無ければ作る - H-->>A: 上限超え かつ 印が無かったなら拒否(1 行を示す) + H->>S: 案内の印を読む + alt 印が同じ skill・args を持つ + H->>S: 印を消す + H-->>A: 何も出さず 0(1 度だけ通す) + else 上限超え + H->>S: 印を skill・args で置き換える + H-->>A: 拒否(1 行を示す) + end end ``` @@ -251,8 +263,8 @@ sequenceDiagram ```mermaid stateDiagram-v2 - [*] --> 一回目: 控えが無い / key が違う / size か mtime が変わった - 一回目 --> 繰り返し: 同じ key・size・mtime + [*] --> 一回目: 控えが無い / key が違う / size・mtime・inode のどれかが変わった + 一回目 --> 繰り返し: 同じ key・size・mtime・inode 繰り返し --> 繰り返し: 同じ(count が上限未満) 繰り返し --> 拒否: count が上限に達する 拒否 --> 拒否: 同じまま読み直す @@ -289,6 +301,9 @@ hook の入口・登録・状態の置き場所・拒否の返し方が同じで ### 決定 3: sleep の判定は「前景で、`while` / `until` のループを含むか、5 秒を超える `sleep`」を拒否する +文字列やコメントの中の `sleep` は数えない。`echo sleep 30` や `git commit -m "sleep 60"` を止めないよう、 +引用・コメント・ヒアドキュメントを除いた後の語の位置で判定する。 + ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 (6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 @@ -310,14 +325,20 @@ Bash をすべて拒否する形も採らない。短い間と照会の間隔ま | 文書 | ループ | 書き換える変更 | | --- | --- | --- | -| `external-ai/references/cli-codex.md` / `cli-agy.md` | `until ! ps -p ...; do sleep 30; done` | #731 | -| `qa-security-scan/03-report-template.md` | `until grep -q ...; do sleep 30; done` | #731 | -| `release/references/completion-check.md` | `while :; do ...; sleep 5; done` | この変更(`waiting.md` を指す 1 行) | +| `external-ai/references/cli-codex.md` / `cli-agy.md` | `until ! ps -p ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | +| `qa-security-scan/03-report-template.md` | `until grep -q ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | +| `release/references/completion-check.md` | `while :; do ...; sleep 5; done` | この変更で案内の 1 行。ループの書き換えは #731 | + +案内の 1 行は 4 文書とも同じで、ループの直前に置く: 「Claude Code では、このループを +`run_in_background: true` で実行して完了通知を待つ(`development-workflow/references/waiting.md`)」。 +hook と案内の行を同じ変更で配布するため、拒否と文書の順序が食い違わない。 +ループの書き換え(道具の共通化)は #731 で行う。 ### 決定 4: 連続 Read は「同じ範囲・変わらないファイル・3 回目」で拒否する hook は実行の前に呼ばれ、読んだ中身を知らない。そのため「空ファイル」ではなく「前回から -大きさと更新時刻が変わっていない」で判定する。空ファイルの読み直しはこれに含まれ、書き込みが +大きさ・更新時刻・inode が変わっていない」で判定する。更新時刻はナノ秒の精度で持ち、inode も比べる。 +同じ秒に同じ大きさの内容で置き換えた(`mv`)ファイルを、変わっていないと取り違えないためである。空ファイルの読み直しはこれに含まれ、書き込みが 進むログの読み直しは含まれない。`offset` と `limit` を鍵に入れるのは、大きなファイルを範囲を 変えて読み進める正当な使い方を止めないためである。 @@ -341,10 +362,12 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 (`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 されるため、切れ目にならない。 -### 決定 7: 文脈量の案内は会話ごとに 1 度だけ拒否し、2 回目は通す +### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、直後の同じ起動だけを通す 利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 -毎回拒否する形は採らない。関門の直前など、切ると判断の材料を失う場面で進めなくなる。 +毎回拒否する形は採らない。続けると決めた利用者が、同じ工程をやり直せなくなる。 +会話ごとに 1 度にする形も採らない。1 度通した後は、以後の工程の切れ目で止まらなくなる。 +印に `skill` と `args` を持つのは、通すのを直後の同じ起動に限るためである。 拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 @@ -379,8 +402,8 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | --- | --- | | AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | | AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done` で deny と理由の欄に `run_in_background` と `waiting.md` | -| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep`・`tool_name: Monitor` で出力なし | -| AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し | +| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`tool_name: Monitor` で出力なし | +| AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し | | AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | | AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | | AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える | @@ -388,7 +411,7 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829` | | AC13 | 単体: 同じ入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | | AC14 | 単体: `ndf:markdown-writing` → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と一致することを文書テストで確かめる | -| AC15 | 単体: 同じ `session_id` で 2 回 → 1 回目 deny、2 回目は出力なし。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | +| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | | AC18 / AC19 | 文書の検査: `SKILL.md` に 1 行を出す規約があり、`context-window.md` に戻す手順の表がある | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index 2c1f3e30e..5f63633f1 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -110,7 +110,8 @@ - [ ] AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で工程 Skill を起動すると、PreToolUse:Skill hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す - [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない - [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` など)の起動は拒否しない -- [ ] AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる) +- ~~AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる)~~ → 変更(2026-09-23、PR #843 のレビュー) +- [ ] AC15: 拒否した直後の同じ Skill・同じ args の起動は通す。別の工程 Skill の起動は再び拒否する。環境変数で判定ごと止められる - [ ] AC16: 文脈量を読めない(transcript が無い・`usage` が無い)ときは拒否しない - [ ] AC17: AC12〜AC16 の判定を、transcript の見本を与えて終了コードと出力を見るテストが確かめている @@ -149,7 +150,7 @@ | --- | --- | | 公開インタフェース | Claude Code の hook の定義に PreToolUse の matcher(`Bash` / `Read` / `Skill`)の登録が増える。環境変数が増える(止める・上限を変える) | | データ | 連続 Read を数える状態を、会話ごとに一時ファイルへ持つ | -| 既存の振る舞い | 前景の `sleep` で待つ Bash(`while` / `until` のループか、5 秒を超える `sleep`)が拒否される。文脈が上限を超えた conductor では工程 Skill の起動が 1 度拒否される | +| 既存の振る舞い | 前景の `sleep` で待つ Bash(`while` / `until` のループか、5 秒を超える `sleep`)が拒否される。文脈が上限を超えた conductor では工程 Skill の起動が 1 度拒否される。`external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` の前景の待ちのループの直前に、`run_in_background` で実行する案内の 1 行が足される | ## 検証手段 From 1bc421be67bcc37507802bd44f9493ce060b1ca3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:39:15 +0000 Subject: [PATCH 04/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=202=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88-c=20=E3=81=AE=E4=B8=AD=E8=BA=AB?= =?UTF-8?q?=E3=81=AE=E5=88=A4=E5=AE=9A=E3=83=BB=E5=8D=B0=E3=81=AE=E5=A4=B1?= =?UTF-8?q?=E5=8A=B9=E3=83=BB=E9=96=BE=E5=80=A4=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 31 +++++++++++++++++----------- issues/issue-829-830-requirements.md | 3 ++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index f8fd10a50..bf351eed1 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -128,8 +128,9 @@ plugins/ndf/ - **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を hook は受け取らないため - **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の `skill` と - `args` を持つ。次の起動が同じ `skill`・`args` なら 1 度だけ通し、印を消す。別の工程 Skill の - 起動は、上限を超えていれば再び拒否する。工程の切れ目ごとに 1 度ずつ止まる(決定 7) + `args` を持つ。次の工程 Skill の起動が同じ `skill`・`args` なら 1 度だけ通し、印を消す。 + 間に他のツールや工程でない Skill が挟まっても印は残る。次の工程 Skill が別の `skill` か + `args` なら、上限を超えていれば印を置き換えて再び拒否する。工程の切れ目ごとに 1 度ずつ止まる(決定 7) ## 入出力の契約 @@ -157,9 +158,9 @@ plugins/ndf/ | 判定 | 拒否する条件 | 理由の欄の文面(要旨) | | --- | --- | --- | -| sleep | `run_in_background` が真でない。判定の前に `command` からコメント(引用の外の `#` 以降)・引用文字列(`'…'` と `"…"`)・ヒアドキュメントの本文を取り除く。残りで `sleep <数>` が**コマンドの位置**(行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後)にあり、次のどちらかに当たる: `while` / `until` がコマンドの位置にある / その `sleep` の秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | +| sleep | `run_in_background` が真でない。判定の前に `command` からコメント(引用の外の `#` 以降)・引用文字列(`'…'` と `"…"`)・ヒアドキュメントの本文を取り除く。取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数(`timeout <秒> bash -c` のように前に `timeout` / `nohup` / `env` が付く形も含む)を取り出し、その中身へ同じ判定を当てる(入れ子も同じ規則で 1 段ずつ)。残りで `sleep <数>` が**コマンドの位置**(行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後)にあり、次のどちらかに当たる: `while` / `until` がコマンドの位置にある / その `sleep` の秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | | 連続 Read | 直前の Read と `key`・`size`・`mtime`・`inode` が同じで、`count + 1` が上限(既定 3)に達する | 「同じファイルの同じ範囲を、変わらないまま 回続けて読もうとした。書き終わりを待つなら `until [ -s <ファイル> ]; do sleep 1; done` を `run_in_background: true` で起動するか、背景の処理の完了通知を待つ。サブエージェントの `tasks/*.output` は読まずに完了通知を待つ。規約: 同上」 | -| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、案内の印が同じ `skill`・`args` を持たない | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill を同じ引数でもう一度起動すると 1 度だけ通る。規約: `context-window.md`」 | +| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、案内の印が同じ `skill`・`args` を持たない(印は次の工程 Skill の起動まで残り、間の他のツールでは消えない) | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill を同じ引数でもう一度起動すると 1 度だけ通る。規約: `context-window.md`」 | **`<課題>` は次の順で決める。** 先に当たったものを使う。 @@ -239,7 +240,7 @@ sequenceDiagram alt 入力が読めない・jq が無い・該当の NDF_*_GUARD=0 H-->>A: 何も出さず 0(通す) else tool_name = Bash - H->>H: 背景か / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と while・until + H->>H: 背景か / -c・eval の中身を取り出し同じ判定 / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と while・until H-->>A: 当たれば拒否(待ち方の案内) else tool_name = Read H->>S: 控えを読む・ファイルの size と mtime と inode を取る @@ -248,7 +249,7 @@ sequenceDiagram else tool_name = Skill H->>H: 工程 Skill か / サブエージェントか H->>S: transcript の末尾から文脈量を読む - H->>S: 案内の印を読む + H->>S: 案内の印を読む(間の他のツールでは消えない) alt 印が同じ skill・args を持つ H->>S: 印を消す H-->>A: 何も出さず 0(1 度だけ通す) @@ -304,6 +305,11 @@ hook の入口・登録・状態の置き場所・拒否の返し方が同じで 文字列やコメントの中の `sleep` は数えない。`echo sleep 30` や `git commit -m "sleep 60"` を止めないよう、 引用・コメント・ヒアドキュメントを除いた後の語の位置で判定する。 +ただし引用を取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数を取り出す。 +前に `timeout` / `nohup` / `env` が付く形(`timeout 590 bash -c "..."`)も含める。 +取り出した中身へ同じ判定を当て、入れ子も同じ規則で 1 段ずつ見る。 +引用の中身は実行されるため、除くだけだと `bash -c 'sleep 30'` を見逃す。 + ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 (6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 @@ -362,12 +368,13 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 (`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 されるため、切れ目にならない。 -### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、直後の同じ起動だけを通す +### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、次の同じ起動だけを通す 利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 毎回拒否する形は採らない。続けると決めた利用者が、同じ工程をやり直せなくなる。 会話ごとに 1 度にする形も採らない。1 度通した後は、以後の工程の切れ目で止まらなくなる。 -印に `skill` と `args` を持つのは、通すのを直後の同じ起動に限るためである。 +印に `skill` と `args` を持つのは、通すのを次の工程 Skill の同じ起動に限るためである。 +間のツールで印を失効させる形は採らない。hook は Edit などを見ないため、失効の条件を一貫して判定できない。 拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 @@ -401,17 +408,17 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | 受け入れ条件 | 何で確かめるか | | --- | --- | | AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | -| AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done` で deny と理由の欄に `run_in_background` と `waiting.md` | -| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`tool_name: Monitor` で出力なし | +| AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done`・`bash -c 'sleep 30'`・`timeout 590 bash -c "until [ -s f ]; do sleep 5; done"`・`sh -c 'until test -s x; do sleep 1; done'` で deny と理由の欄に `run_in_background` と `waiting.md` | +| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | | AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し | | AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | | AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | -| AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える | +| AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える。閾値: `NDF_SLEEP_MAX_SEC=30` で `sleep 10` は出力なし・`sleep 40` は deny、`NDF_READ_REPEAT_LIMIT=2` で 2 回目に deny、`NDF_CONTEXT_LIMIT=300000` で文脈量 250,000 は出力なし | | AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | | AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829` | | AC13 | 単体: 同じ入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | | AC14 | 単体: `ndf:markdown-writing` → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と一致することを文書テストで確かめる | -| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | +| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | | AC18 / AC19 | 文書の検査: `SKILL.md` に 1 行を出す規約があり、`context-window.md` に戻す手順の表がある | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index 5f63633f1..7842afac8 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -111,7 +111,8 @@ - [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない - [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` など)の起動は拒否しない - ~~AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる)~~ → 変更(2026-09-23、PR #843 のレビュー) -- [ ] AC15: 拒否した直後の同じ Skill・同じ args の起動は通す。別の工程 Skill の起動は再び拒否する。環境変数で判定ごと止められる +- ~~AC15: 拒否した直後の同じ Skill・同じ args の起動は通す。別の工程 Skill の起動は再び拒否する。環境変数で判定ごと止められる~~ → 変更(2026-09-23、PR #843 のレビュー 2 回目。hook は Bash / Read / Skill しか見ないため『直後』を判定できない) +- [ ] AC15: 拒否の後、次に起動した工程 Skill が拒否したものと同じ skill・args なら 1 度だけ通す(間に他のツールや工程でない Skill が挟まってもよい)。次の工程 Skill の起動が別の skill か args なら、印を置き換えて再び拒否する。環境変数で判定ごと止められる - [ ] AC16: 文脈量を読めない(transcript が無い・`usage` が無い)ときは拒否しない - [ ] AC17: AC12〜AC16 の判定を、transcript の見本を与えて終了コードと出力を見るテストが確かめている From bdb8871457ed61613369674462a082f606d8272a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:47:58 +0000 Subject: [PATCH 05/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=203=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88=E5=BC=95=E3=81=8D=E7=B6=99=E3=81=8E?= =?UTF-8?q?=E3=81=AE=E5=A2=83=E3=83=BBPR=20=E3=81=AE=E5=BC=95=E3=81=8D?= =?UTF-8?q?=E6=96=B9=E3=83=BB=E6=8E=A7=E3=81=88=E3=81=AE=E7=BD=AE=E3=81=8D?= =?UTF-8?q?=E5=A0=B4=E6=89=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 43 +++++++++++++++++++--------- issues/issue-829-830-requirements.md | 7 +++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index bf351eed1..6af6f8f90 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -24,7 +24,7 @@ conductor はその 1 行を利用者へ示して止まる。 | F2 | 前景で `sleep` を使って待つ Bash(ループの待ちと長い `sleep`)を止め、代わりの待ち方を知らせる | Claude Code のエージェント(全層) | | F3 | 変わっていないファイルの同じ範囲を続けて読み直す Read を止め、代わりの待ち方を知らせる | 同上 | | F4 | 文脈が上限を超えた conductor の工程 Skill の起動を 1 度止め、新しい会話で打つ 1 行を知らせる | conductor と、それを見る利用者 | -| F5 | 工程を 1 つ終えるたびに、次の工程を始める 1 行を出す | conductor(全ランタイム) | +| F5 | `context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)と、文脈量の hook が拒否したときに、次の工程を始める 1 行を出す | conductor(全ランタイム) | | F6 | その 1 行から始めた新しい会話で、モード・作業ツリー・現在の工程を戻す | conductor(全ランタイム) | | F7 | hook を種類ごとに止める・上限を変える | 利用者 | @@ -38,7 +38,7 @@ conductor はその 1 行を利用者へ示して止まる。 | `development-workflow/references/waiting.md` | 新設 | 待ち方の規約の唯一の置き場所(F1) | | `development-workflow/references/agent-layers.md` | 変更 | supervisor の規則 4 と worker の規則に、`waiting.md` への参照を 1 行ずつ足す | | `development-workflow/references/context-window.md` | 変更 | 「前提: 実測ではない」を #827 の実測へ置き換える。hook の上限と引き継ぎの 1 行の節を足す | -| `development-workflow/SKILL.md` | 変更 | 工程を終えるたびに 1 行を出す規約と、新しい会話で戻す手順への参照(F5 / F6) | +| `development-workflow/SKILL.md` | 変更 | `context-window.md` の 4 つの切れ目で conductor が 1 行を出す規約と、新しい会話で戻す手順への参照(F5 / F6) | | `external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` | 変更 | 前景の待ちのループの直前に「Claude Code では、このループを `run_in_background: true` で実行して完了通知を待つ(`development-workflow/references/waiting.md`)」の 1 行を足す(決定 3) | | `plugins/ndf/scripts/tests/test_token_guard.py` | 新設 | F2〜F4 と F7 の判定を、入力 JSON と transcript の見本で確かめる | | `plugins/ndf/README.md` | 変更 | hook の一覧に `token-guard.sh` を足し、4 ランタイムでの扱いを表で示す | @@ -53,7 +53,7 @@ graph TB TG["token-guard.sh"] end subgraph ST["状態"] - RS["連続 Read の控え
~/.local/state/ndf/guards/"] + RS["連続 Read の控え
#lt;wf_state_dir の親#gt;/guards/"] TR["会話の記録
transcript_path"] SL["token-guard-stages.txt"] end @@ -112,9 +112,19 @@ plugins/ndf/ ## データ構造 -**連続 Read の控えを、会話ごとに 1 つの小さなファイルへ持つ。** 置き場所は -`${XDG_STATE_HOME:-$HOME/.local/state}/ndf/guards/read-.json`。既存の通過工程の控え -(`~/.local/state/ndf/stages/`)と同じ親に置く。 +**連続 Read の控えを、会話ごとに 1 つの小さなファイルへ持つ。** 置き場所は `guards/read-.json`。 + +**`guards/` の場所は、通過工程の控えの場所から決める。** `token-guard.sh` は +`development-workflow/scripts/lib/workflow-common.sh` を読み込み、その `wf_state_dir` で +通過工程の控えの場所を得る。解決順は次のとおりで、先に使えたものを採る。 + +1. `$CLAUDE_PLUGIN_DATA/stages` +2. `$XDG_STATE_HOME/ndf/stages` +3. `$HOME/.local/state/ndf/stages` +4. `${TMPDIR:-/tmp}/ndf-stages` + +`guards/` は得たディレクトリと同じ親に置く。4 番目のときは `${TMPDIR:-/tmp}/ndf-guards` に置く。 +**読み込めないときは Read と文脈量の判定を通す。** sleep の判定は状態を持たないので続ける。 | キー | 型 | 意味 | | --- | --- | --- | @@ -167,7 +177,7 @@ plugins/ndf/ | 順 | 読むもの | 取り出す値 | | --- | --- | --- | | 1 | Skill の `args` | `#<数>` と数だけの語 | -| 2 | このリポジトリの通過工程の控え(`~/.local/state/ndf/stages/<所有者>__<リポジトリ>__<番号>.json`) | 更新時刻が最も新しい控えの番号 | +| 2 | このリポジトリの通過工程の控え(`wf_state_dir` が返すディレクトリの `<所有者>__<リポジトリ>__<番号>.json`) | 更新時刻が最も新しい控えの番号 | | 3 | どちらも無い | `<課題番号>` の文字のまま | ### 環境変数 @@ -200,9 +210,12 @@ plugins/ndf/ | --- | --- | --- | | 1 | 課題の本文の `## 進行` | モード・作業ツリー・計画ファイル・通った工程 | | 2 | `stage-check.sh report <番号>` | 通過工程の控え(本文と食い違えば控えを正とする) | -| 3 | `gh pr list --search "<番号>" --state all` | 設計・実装の Pull Request と状態 | +| 3 | 1 の作業ツリー(`.worktrees/<ブランチ名>`)のブランチ名で `gh pr list --head <ブランチ名> --state all`。実装の Pull Request は `gh issue view <番号> --json closedByPullRequestsReferences` でも引く | 設計・実装の Pull Request と状態 | | 4 | 1〜3 から、チェックの付いていない最初の必須の工程 | 次に起動する工程 Skill | +**Pull Request は番号の全文検索で引かない。** 同じ番号に触れただけの別の Pull Request も返すためである。 +設計の Pull Request は閉じる語を持たないため、課題との結び付きでは引けず、ブランチ名で引く。 + ### 文書の中身 **`waiting.md`(新設)** は次の 5 つを持つ。他の文書は写さずにここを指す。 @@ -215,16 +228,20 @@ plugins/ndf/ | 待つ相手ごとの手 | サブエージェント → 完了通知。背景の CLI → CLI そのものを `run_in_background` で起動する。既に起動したプロセス → `until` で終わりを待つループを `run_in_background` で。Pull Request の検査 → `gh pr checks --watch` を `run_in_background` で。新しいコメントを 1 件ずつ → `Monitor` | | hook | `token-guard.sh` の条件と止め方(環境変数)。4 ランタイムの表 | -**`context-window.md`(変更)** は 3 か所を変える。 +**`context-window.md`(変更)** は 4 か所を変える。 | 場所 | 変え方 | | --- | --- | | 「前提: 実測ではない」(:41-42) | #827 の実測に置き換える: conductor の最大文脈は 2026-09-20 以降 7 件中 4 件で 20 万超・最大 68 万、ai-plugins の 30 日間で 45 件中 37 件が 20 万超・平均 41 万、工程の開始ごとに切れば再読込量が 58%(30 日間 62%)減る。出典は #827 | | 新しい節「上限を超えたら hook が止める」 | 上限の既定(200,000)と `NDF_CONTEXT_LIMIT`・工程の切れ目ごとに 1 度止めること・続けたいときの手 | | 新しい節「新しい会話で戻す」 | 引き継ぎの 1 行の形と、戻す手順の表(上の 4 行) | +| 「復元の手順を持つのは各工程の Skill であって、この文書ではない」(:46-47) | 「新しい会話で戻す手順は、この文書の「新しい会話で戻す」節が持つ」へ改める | -**`SKILL.md`(変更)** は「工程は 1 つの context window で通し切らなくてよい」の段落に 2 文を足す。 -工程を 1 つ終えるたびに引き継ぎの 1 行を出すこと、戻す手順は `context-window.md` にあること。 +**`SKILL.md`(変更)** は「工程は 1 つの context window で通し切らなくてよい」の段落に 3 文を足す。 +1 つ目は、`context-window.md` の 4 つの切れ目で conductor が引き継ぎの 1 行を出すこと。 +2 つ目は、3 層では conductor が `## 持ち場の報告` を受け取った時点で出し、supervisor は出さないこと。 +supervisor の持ち場の境がこの切れ目に当たるためである。文脈量の hook が拒否したときも出す。 +3 つ目は、戻す手順が `context-window.md` にあること。 **`agent-layers.md`(変更)** は supervisor の規則 4 の後ろに「待ち方は `waiting.md` に従う」を足し、 worker の規則に同じ 1 行を 5 番目として足す。 @@ -283,7 +300,7 @@ stateDiagram-v2 | --- | --- | --- | | 性能・拡張性 | 50 MB の記録でも 1 秒以内 | 記録は `tail -n 200` の範囲だけを読む。Bash と Read の判定は記録を読まない。登録の `timeout` は 5 秒 | | 運用・保守性 | 理由の欄だけで次の手が分かる | 理由の欄に代わりの手段と規約の場所を必ず書く(出力の表) | -| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。登録に `continueOnError: true` | +| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。`workflow-common.sh` を読み込めないときは Read と文脈量の判定を通し、sleep の判定だけを続ける。登録に `continueOnError: true` | ## 決定の記録 @@ -421,7 +438,7 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | -| AC18 / AC19 | 文書の検査: `SKILL.md` に 1 行を出す規約があり、`context-window.md` に戻す手順の表がある | +| AC18 / AC19 | 文書の検査: `SKILL.md` に 4 つの切れ目と hook の拒否で conductor が 1 行を出す規約(3 層では `## 持ち場の報告` を受け取った時点)があり、`context-window.md` に戻す手順の表がある | | AC20 | 実機: 実装の Pull Request の途中で会話を切り、1 行だけで新しい会話を始め、モード・作業ツリー・次の工程が戻ったことを #830 に残す | | AC21 / AC22 | 文書の検査: 「実測ではない」の文面が消え、#827 への参照と数値があり、上限の値が `NDF_CONTEXT_LIMIT` の既定と一致する | | AC23 | 文書の検査: README に 4 ランタイムの表がある | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index 7842afac8..fdd68f926 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -60,7 +60,7 @@ - 待ち方の規約の置き場所を 1 つに決め、supervisor / worker への指示の雛形から参照させる(#829) - PreToolUse hook で、前景の `sleep` で待つ Bash(ループの待ちと長い `sleep`)と、変わらないファイルの同じ範囲への連続 Read を拒否し、代わりの待ち方を案内する(#829) - PreToolUse:Skill hook で、会話の文脈量が上限を超えた状態の工程 Skill の起動を止め、新しい会話で始める 1 行を案内する(#830) -- `development-workflow` が工程を 1 つ終えるたびに、次に打つコマンドを 1 行で出す規約(#830) +- `context-window.md` の 4 つの切れ目と文脈量の hook の拒否で、conductor が次に打つコマンドを 1 行で出す規約(#830) - `context-window.md` の「前提: 実測ではない」を #827 の実測値へ書き換える(#830) - 4 ランタイムでの扱い(hook が効くランタイムと、規約だけで守るランタイムの区別) @@ -81,7 +81,7 @@ | 前景の Bash | `run_in_background` を付けずに実行する Bash。終わるまで呼び出しが返らない | | 文脈量 | 1 回の API 呼び出しで読んだトークン数。`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` | | 工程 Skill | `development-workflow` の工程表が起動する Skill(`requirements-design` / `design` / `pr` など) | -| 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行 | +| 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行。`context-window.md` の 4 つの切れ目と文脈量の hook の拒否で conductor が出す | ## 受け入れ条件 @@ -118,7 +118,8 @@ ### 引き継ぎの 1 行(#830) -- [ ] AC18: `development-workflow` は、工程を 1 つ終えるたびに、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す +- ~~AC18: `development-workflow` は、工程を 1 つ終えるたびに、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す~~ → 変更(2026-09-23、PR #843 のレビュー 3 回目。3 層では conductor が工程の終わりを観測しないため、既存の切れ目にそろえた) +- [ ] AC18: conductor は、`context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)で、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す。3 層では supervisor の持ち場の境がこの切れ目に当たるため、conductor が `## 持ち場の報告` を受け取った時点で出す(supervisor は出さない)。文脈量の hook が拒否したときにも出す - [ ] AC19: そのコマンド 1 行だけで始めた新しい会話が、課題の本文の `## 進行`・Pull Request・通過工程の控えから、モード・作業ツリー・現在の工程を戻せる。戻す手順が文書にある - [ ] AC20: AC19 を、実際の課題 1 件で新しい会話から再開して確かめ、結果を本 issue に残している From 747f19104d08fdec8f3bae02a02260d6bffc31c9 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:56:46 +0000 Subject: [PATCH 06/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=204=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88=E8=AA=B2=E9=A1=8C=E7=95=AA=E5=8F=B7?= =?UTF-8?q?=E3=81=AE=E6=8E=A8=E6=B8=AC=E3=82=92=E3=82=84=E3=82=81=E3=82=8B?= =?UTF-8?q?=E3=83=BB=E3=83=AB=E3=83=BC=E3=83=97=E6=9C=AC=E4=BD=93=E3=81=AE?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E3=83=BB3=20=E5=B1=A4=E3=81=AE=E7=B5=8C?= =?UTF-8?q?=E8=B7=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 83 +++++++++++++++++----------- issues/issue-829-830-requirements.md | 19 ++++--- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index 6af6f8f90..daa25ace1 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -12,8 +12,8 @@ を 30 回繰り返すと、30 回とも文脈の全体を読み直す。変更後は、1 回目の `sleep 60 && tail` を hook が拒否し、理由の欄で「待ちの条件を until ループにして `run_in_background` で起動し、完了通知を待つ」よう案内する。 -**#830 の例。** conductor の文脈が 41 万のまま `/ndf:design` を起動すると、変更後は hook が -起動を 1 度拒否し、「新しい会話で `/ndf:development-workflow #829` を打つ」と案内する。 +**#830 の例。** conductor の文脈が 41 万のまま `/ndf:design` を起動する(3 層では `設計:` の +supervisor を起動する)と、変更後は hook が起動を 1 度拒否し、「新しい会話で `/ndf:development-workflow #829` を打つ」と案内する。 conductor はその 1 行を利用者へ示して止まる。 ## 機能一覧 @@ -23,7 +23,7 @@ conductor はその 1 行を利用者へ示して止まる。 | F1 | 待ち方の規約を 1 か所で読む | supervisor / worker / conductor | | F2 | 前景で `sleep` を使って待つ Bash(ループの待ちと長い `sleep`)を止め、代わりの待ち方を知らせる | Claude Code のエージェント(全層) | | F3 | 変わっていないファイルの同じ範囲を続けて読み直す Read を止め、代わりの待ち方を知らせる | 同上 | -| F4 | 文脈が上限を超えた conductor の工程 Skill の起動を 1 度止め、新しい会話で打つ 1 行を知らせる | conductor と、それを見る利用者 | +| F4 | 文脈が上限を超えた conductor が工程へ入る起動(対話の経路は工程 Skill、3 層の経路は持ち場の supervisor の Agent)を 1 度止め、新しい会話で打つ 1 行を知らせる | conductor と、それを見る利用者 | | F5 | `context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)と、文脈量の hook が拒否したときに、次の工程を始める 1 行を出す | conductor(全ランタイム) | | F6 | その 1 行から始めた新しい会話で、モード・作業ツリー・現在の工程を戻す | conductor(全ランタイム) | | F7 | hook を種類ごとに止める・上限を変える | 利用者 | @@ -32,9 +32,9 @@ conductor はその 1 行を利用者へ示して止まる。 | 要素 | 新設 / 変更 | 責務 | | --- | --- | --- | -| `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(sleep / 連続 Read / 文脈量)へ振り分け、拒否か通過を返す | -| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名)。F4 がこの一覧に無い Skill を見ない | -| `plugins/ndf/hooks/claude.json` | 変更 | PreToolUse に matcher `Bash\|Read\|Skill` で `token-guard.sh` を登録する | +| `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(`Bash` → sleep / `Read` → 連続 Read / `Skill`・`Agent`・`Task` → 文脈量)へ振り分け、拒否か通過を返す | +| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名。入口の `development-workflow` と `issue-plan-strategy` を含む)。F4 がこの一覧に無い Skill を見ない | +| `plugins/ndf/hooks/claude.json` | 変更 | PreToolUse に matcher `Bash\|Read\|Skill\|Agent\|Task` で `token-guard.sh` を登録する | | `development-workflow/references/waiting.md` | 新設 | 待ち方の規約の唯一の置き場所(F1) | | `development-workflow/references/agent-layers.md` | 変更 | supervisor の規則 4 と worker の規則に、`waiting.md` への参照を 1 行ずつ足す | | `development-workflow/references/context-window.md` | 変更 | 「前提: 実測ではない」を #827 の実測へ置き換える。hook の上限と引き継ぎの 1 行の節を足す | @@ -63,10 +63,10 @@ graph TB AL["references/agent-layers.md"] SK["SKILL.md"] end - AG -->|"Bash / Read / Skill"| TG + AG -->|"Bash / Read / Skill / Agent・Task"| TG AG -->|"編集系 / Bash"| WG TG -->|"Read の判定"| RS - TG -->|"Skill の判定"| TR + TG -->|"Skill・Agent の判定"| TR TG -->|"Skill の判定"| SL TG -. "拒否の理由が指す" .-> WT TG -. "拒否の理由が指す" .-> CW @@ -116,7 +116,7 @@ plugins/ndf/ **`guards/` の場所は、通過工程の控えの場所から決める。** `token-guard.sh` は `development-workflow/scripts/lib/workflow-common.sh` を読み込み、その `wf_state_dir` で -通過工程の控えの場所を得る。解決順は次のとおりで、先に使えたものを採る。 +通過工程の控えの場所を得る。使うのは `guards/` の置き場所を決めることだけで、控えの番号は読まない。解決順は次のとおりで、先に使えたものを採る。 1. `$CLAUDE_PLUGIN_DATA/stages` 2. `$XDG_STATE_HOME/ndf/stages` @@ -137,10 +137,11 @@ plugins/ndf/ - **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない - **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を hook は受け取らないため -- **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の `skill` と - `args` を持つ。次の工程 Skill の起動が同じ `skill`・`args` なら 1 度だけ通し、印を消す。 - 間に他のツールや工程でない Skill が挟まっても印は残る。次の工程 Skill が別の `skill` か - `args` なら、上限を超えていれば印を置き換えて再び拒否する。工程の切れ目ごとに 1 度ずつ止まる(決定 7) +- **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の鍵を持つ。 + 鍵は Skill なら `skill` と `args`、Agent なら `description` である(決定 6)。 + 工程へ入る次の起動が同じ鍵なら 1 度だけ通し、印を消す。 + 間に他のツールや工程でない Skill・Agent が挟まっても印は残る。次の起動が別の鍵なら、 + 上限を超えていれば印を置き換えて再び拒否する。工程の切れ目ごとに 1 度ずつ止まる(決定 7) ## 入出力の契約 @@ -148,10 +149,11 @@ plugins/ndf/ | キー | 使う判定 | 無いとき | | --- | --- | --- | -| `tool_name` | 振り分け(`Bash` / `Read` / `Skill`) | 通す | +| `tool_name` | 振り分け(`Bash` / `Read` / `Skill` / `Agent`・旧名 `Task`) | 通す | | `tool_input.command` / `tool_input.run_in_background` | sleep | 通す | | `tool_input.file_path` / `offset` / `limit` | 連続 Read | 通す | -| `tool_input.skill` / `tool_input.args` | 文脈量 | 通す | +| `tool_input.skill` / `tool_input.args` | 文脈量(対話の経路) | 通す | +| `tool_input.description` | 文脈量(3 層の経路。先頭語が持ち場の語彙か) | 通す | | `session_id` | 連続 Read の控え・案内の印 | 通す | | `transcript_path` | 文脈量 | 通す | | `agent_id`(サブエージェントで付く) | 文脈量(付いていれば見ない) | conductor とみなす。`transcript_path` が `/subagents/` を含めばサブエージェントとみなす | @@ -168,24 +170,25 @@ plugins/ndf/ | 判定 | 拒否する条件 | 理由の欄の文面(要旨) | | --- | --- | --- | -| sleep | `run_in_background` が真でない。判定の前に `command` からコメント(引用の外の `#` 以降)・引用文字列(`'…'` と `"…"`)・ヒアドキュメントの本文を取り除く。取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数(`timeout <秒> bash -c` のように前に `timeout` / `nohup` / `env` が付く形も含む)を取り出し、その中身へ同じ判定を当てる(入れ子も同じ規則で 1 段ずつ)。残りで `sleep <数>` が**コマンドの位置**(行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後)にあり、次のどちらかに当たる: `while` / `until` がコマンドの位置にある / その `sleep` の秒数のどれかが上限(既定 5)を超える | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | +| sleep | `run_in_background` が真でない。判定の前に `command` からコメント(引用の外の `#` 以降)・引用文字列(`'…'` と `"…"`)・ヒアドキュメントの本文を取り除く。取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数(`timeout <秒> bash -c` のように前に `timeout` / `nohup` / `env` が付く形も含む)を取り出し、その中身へ同じ判定を当てる(入れ子も同じ規則で 1 段ずつ)。残りで `sleep <数>` が**コマンドの位置**(行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後)にあり、次のどちらかに当たる: その `sleep` が `while` / `until` のループの本体(`do` と対応する `done` の間)にある / その秒数が上限(既定 5)を超える。本体の外の `sleep` は秒数の上限だけで見る | 「前景で `sleep` を使って待つと、待つ呼び出しのたびに文脈を読み直す。同じ条件の until ループを `run_in_background: true` で起動し、完了通知を待つ(通知は 1 回)。出来事を 1 つずつ受けるなら `Monitor`。規約: `development-workflow/references/waiting.md`」 | | 連続 Read | 直前の Read と `key`・`size`・`mtime`・`inode` が同じで、`count + 1` が上限(既定 3)に達する | 「同じファイルの同じ範囲を、変わらないまま 回続けて読もうとした。書き終わりを待つなら `until [ -s <ファイル> ]; do sleep 1; done` を `run_in_background: true` で起動するか、背景の処理の完了通知を待つ。サブエージェントの `tasks/*.output` は読まずに完了通知を待つ。規約: 同上」 | -| 文脈量 | 工程 Skill で、conductor で、文脈量が上限(既定 200,000)を超え、案内の印が同じ `skill`・`args` を持たない(印は次の工程 Skill の起動まで残り、間の他のツールでは消えない) | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。このまま続けると利用者が決めたら、同じ Skill を同じ引数でもう一度起動すると 1 度だけ通る。規約: `context-window.md`」 | +| 文脈量 | conductor が工程へ入る起動(対話の経路: 工程 Skill の `Skill`。3 層の経路: `description` の先頭語が `設計` / `実装` / `検査` / `取り込み` / `仕上げ` の `Agent`)で、文脈量が上限(既定 200,000)を超え、案内の印が同じ鍵を持たない(印は工程へ入る次の起動まで残り、間の他のツールでは消えない) | 「文脈が で上限 を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: `/ndf:development-workflow <課題>`。3 層の経路では、新しい会話で `/goal` に同じ 1 行を渡す。`<課題>` が `<課題番号>` のままなら、進めている課題の番号を補って示す。このまま続けると利用者が決めたら、同じ起動をもう一度行うと 1 度だけ通る。規約: `context-window.md`」 | **`<課題>` は次の順で決める。** 先に当たったものを使う。 | 順 | 読むもの | 取り出す値 | | --- | --- | --- | -| 1 | Skill の `args` | `#<数>` と数だけの語 | -| 2 | このリポジトリの通過工程の控え(`wf_state_dir` が返すディレクトリの `<所有者>__<リポジトリ>__<番号>.json`) | 更新時刻が最も新しい控えの番号 | -| 3 | どちらも無い | `<課題番号>` の文字のまま | +| 1 | Skill の `args`(3 層の経路では Agent の `description`) | `#<数>` と数だけの語 | +| 2 | 1 に番号が無い | `<課題番号>` の文字のまま(推測しない) | + +**通過工程の控えから番号を推測しない。** 並行して別の課題を進めていると、最新の控えは別の課題を指す。 ### 環境変数 | 変数 | 既定 | 意味 | | --- | --- | --- | | `NDF_SLEEP_GUARD` | `1` | `0` で sleep の判定を止める | -| `NDF_SLEEP_MAX_SEC` | `5` | ループの外で通す `sleep` の秒数の上限 | +| `NDF_SLEEP_MAX_SEC` | `5` | `sleep` の秒数の上限(ループの本体の外ではこれだけで見る) | | `NDF_READ_REPEAT_GUARD` | `1` | `0` で連続 Read の判定を止める | | `NDF_READ_REPEAT_LIMIT` | `3` | 連続 Read を拒否する回数 | | `NDF_CONTEXT_GUARD` | `1` | `0` で文脈量の判定を止める | @@ -257,21 +260,21 @@ sequenceDiagram alt 入力が読めない・jq が無い・該当の NDF_*_GUARD=0 H-->>A: 何も出さず 0(通す) else tool_name = Bash - H->>H: 背景か / -c・eval の中身を取り出し同じ判定 / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と while・until + H->>H: 背景か / -c・eval の中身を取り出し同じ判定 / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と、while・until の本体にあるか H-->>A: 当たれば拒否(待ち方の案内) else tool_name = Read H->>S: 控えを読む・ファイルの size と mtime と inode を取る H->>S: 控えを置き換える(count を進めるか 1 に戻す) H-->>A: count が上限に達すれば拒否 - else tool_name = Skill - H->>H: 工程 Skill か / サブエージェントか + else tool_name = Skill / Agent / Task + H->>H: 工程 Skill か・先頭語が持ち場の Agent か / サブエージェントか H->>S: transcript の末尾から文脈量を読む H->>S: 案内の印を読む(間の他のツールでは消えない) - alt 印が同じ skill・args を持つ + alt 印が同じ鍵(skill・args か description)を持つ H->>S: 印を消す H-->>A: 何も出さず 0(1 度だけ通す) else 上限超え - H->>S: 印を skill・args で置き換える + H->>S: 印をこの起動の鍵で置き換える H-->>A: 拒否(1 行を示す) end end @@ -317,7 +320,7 @@ hook の入口・登録・状態の置き場所・拒否の返し方が同じで 同じファイルの節にすると、参照するたびに 3 層の規約の全体を読ませる。`agent-layers.md` と `parallel-work.md` の節に置く形は 採らない。`parallel-work.md` は待ち方の道具に触れておらず、そこへ足す理由が無い。 -### 決定 3: sleep の判定は「前景で、`while` / `until` のループを含むか、5 秒を超える `sleep`」を拒否する +### 決定 3: sleep の判定は「前景で、`while` / `until` のループの本体にあるか、5 秒を超える `sleep`」を拒否する 文字列やコメントの中の `sleep` は数えない。`echo sleep 30` や `git commit -m "sleep 60"` を止めないよう、 引用・コメント・ヒアドキュメントを除いた後の語の位置で判定する。 @@ -327,6 +330,10 @@ hook の入口・登録・状態の置き場所・拒否の返し方が同じで 取り出した中身へ同じ判定を当て、入れ子も同じ規則で 1 段ずつ見る。 引用の中身は実行されるため、除くだけだと `bash -c 'sleep 30'` を見逃す。 +ループとして数えるのは、`sleep` が `while` / `until` の `do` と対応する `done` の間にあるときだけである。 +本体の外の `sleep`(`while read l; do ...; done < f; sleep 1`)は秒数の上限だけで見る。 +`while` と `sleep` が同じコマンドにあるだけで拒否すると、ループの後の短い間まで止める。 + ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 (6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 @@ -378,7 +385,17 @@ hook は実行の前に呼ばれ、読んだ中身を知らない。そのため Kiro は拒否すると代わりの口を持たず、agy は案内を控えへ積む形で、どちらも同じ案内を出せない。 CLI 側の消費を測った後(#827 の次の手順)に改めて決める。 -### 決定 6: 文脈量の判定は conductor の工程 Skill の起動だけに掛ける +### 決定 6: 文脈量の判定は conductor が工程へ入る起動だけに掛ける + +**conductor が工程へ入る起動は、経路によって違うツールに現れる。** hook は両方を捕まえる。 + +| 経路 | conductor が起動するもの | 捕まえる入力 | 印の鍵 | +| --- | --- | --- | --- | +| 対話 | 工程 Skill(例 `ndf:design`)。対話では 3 層へ出さない(`development-workflow/SKILL.md` の「`/goal` の引数として呼ばれたとき」の末尾) | `Skill`。名前が `token-guard-stages.txt` にある(入口の `development-workflow` と `issue-plan-strategy` を含む) | `skill`・`args` | +| 3 層 | 持ち場ごとの supervisor。conductor は `development-workflow` と `issue-plan-strategy` 以外を起動しない(`agent-layers.md` の「3 層の責務」) | `Agent`(旧名 `Task`)。`agent_id` が無く、`description` の先頭語が持ち場の語彙(`agent-layers.md` の「起動の指示」) | `description` | + +3 層の経路の `Skill` だけを見ると、捕まるのは入口の起動だけで、持ち場の切れ目で止まらない。 +先頭語が作業の種類(`調査` など)の Agent は持ち場でないため見ない。 supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が途中で 途切れる。supervisor の切れ目は #768 / #773 が測ってから決める。工程でない Skill @@ -390,7 +407,7 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 毎回拒否する形は採らない。続けると決めた利用者が、同じ工程をやり直せなくなる。 会話ごとに 1 度にする形も採らない。1 度通した後は、以後の工程の切れ目で止まらなくなる。 -印に `skill` と `args` を持つのは、通すのを次の工程 Skill の同じ起動に限るためである。 +印に鍵(`skill` と `args`、Agent では `description`)を持つのは、通すのを工程へ入る次の同じ起動に限るためである。 間のツールで印を失効させる形は採らない。hook は Edit などを見ないため、失効の条件を一貫して判定できない。 拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 @@ -426,15 +443,15 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | --- | --- | | AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | | AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done`・`bash -c 'sleep 30'`・`timeout 590 bash -c "until [ -s f ]; do sleep 5; done"`・`sh -c 'until test -s x; do sleep 1; done'` で deny と理由の欄に `run_in_background` と `waiting.md` | -| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | +| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`while read l; do echo "$l"; done < f; sleep 1`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | | AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し | | AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | | AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | | AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える。閾値: `NDF_SLEEP_MAX_SEC=30` で `sleep 10` は出力なし・`sleep 40` は deny、`NDF_READ_REPEAT_LIMIT=2` で 2 回目に deny、`NDF_CONTEXT_LIMIT=300000` で文脈量 250,000 は出力なし | | AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | -| AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829` | -| AC13 | 単体: 同じ入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | -| AC14 | 単体: `ndf:markdown-writing` → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と一致することを文書テストで確かめる | +| AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829`。3 層: `tool_name: Agent`、`description: "設計: #829 #830"` で deny と `/ndf:development-workflow #829 #830`。args に番号が無ければ `<課題番号>` のまま(控えが複数あっても推測しない) | +| AC13 | 単体: AC12 の Skill と Agent の入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | +| AC14 | 単体: `ndf:markdown-writing` → 出力なし。`description` の先頭語が `調査:` の Agent → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と入口の 2 つに一致することを文書テストで確かめる | | AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index fdd68f926..1a8de1cdf 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -59,7 +59,7 @@ - 待ち方の規約の置き場所を 1 つに決め、supervisor / worker への指示の雛形から参照させる(#829) - PreToolUse hook で、前景の `sleep` で待つ Bash(ループの待ちと長い `sleep`)と、変わらないファイルの同じ範囲への連続 Read を拒否し、代わりの待ち方を案内する(#829) -- PreToolUse:Skill hook で、会話の文脈量が上限を超えた状態の工程 Skill の起動を止め、新しい会話で始める 1 行を案内する(#830) +- PreToolUse の `Skill` と `Agent` の hook で、会話の文脈量が上限を超えた conductor が工程へ入る起動(工程 Skill か持ち場の supervisor)を止め、新しい会話で始める 1 行を案内する(#830) - `context-window.md` の 4 つの切れ目と文脈量の hook の拒否で、conductor が次に打つコマンドを 1 行で出す規約(#830) - `context-window.md` の「前提: 実測ではない」を #827 の実測値へ書き換える(#830) - 4 ランタイムでの扱い(hook が効くランタイムと、規約だけで守るランタイムの区別) @@ -70,7 +70,7 @@ - 待ちの道具(`bg-wait.sh`)を共通層へ移す #731、サブエージェントが待ちで止まる #656、上限の無い待ち #345 - supervisor のスクリプト駆動(#827 の「方針」) - codex / kiro / agy の CLI 側の消費の計測(#827 で対象外) -- supervisor / worker の文脈量の上限(#768 / #773)。この変更の hook は conductor の工程 Skill の起動だけを見る +- supervisor / worker の文脈量の上限(#768 / #773)。この変更の hook は conductor が工程へ入る起動だけを見る - 設計の成果物のうちクラス図: 作るのはシェルの hook と文書だけで、型を持たないため対象が無い ## 用語 @@ -94,8 +94,9 @@ ### 待ちの hook(#829) -- [ ] AC5: Claude Code の PreToolUse hook が、前景で `sleep` を使って待つ Bash を拒否する。対象は、`sleep` に数値の秒数を渡し、`while` / `until` のループを含むか秒数が上限(既定 5 秒)を超えるもの。理由の欄に代わりの待ち方(同じループを `run_in_background` で起動して完了通知を待つ / `Monitor`)を示す -- [ ] AC6: `run_in_background: true` の Bash、`Monitor` ツールの中の `sleep`、`sleep` を含まない Bash、ループの外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep` は拒否しない +- ~~AC5: Claude Code の PreToolUse hook が、前景で `sleep` を使って待つ Bash を拒否する。対象は、`sleep` に数値の秒数を渡し、`while` / `until` のループを含むか秒数が上限(既定 5 秒)を超えるもの。理由の欄に代わりの待ち方(同じループを `run_in_background` で起動して完了通知を待つ / `Monitor`)を示す~~ → 変更(2026-09-23、PR #843 のレビュー 4 回目。ループの後の短い `sleep` まで止めないため) +- [ ] AC5: Claude Code の PreToolUse hook が、前景で `sleep` を使って待つ Bash を拒否する。対象は、`sleep` に数値の秒数を渡し、`while` / `until` のループの本体(`do` と対応する `done` の間)にあるか秒数が上限(既定 5 秒)を超えるもの。理由の欄に代わりの待ち方(同じループを `run_in_background` で起動して完了通知を待つ / `Monitor`)を示す +- [ ] AC6: `run_in_background: true` の Bash、`Monitor` ツールの中の `sleep`、`sleep` を含まない Bash、ループの本体の外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep` は拒否しない - [ ] AC7: 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさと更新時刻が変わらないまま、その会話で連続して上限の回数(既定 3)に達すると、hook が拒否し、代わりの待ち方を示す。別の引数の Read が挟まるか、ファイルが変われば数え直す - [ ] AC8: AC5〜AC7 の判定を、入力 JSON を与えて終了コードと出力を見るテストが確かめている(拒否する例と通す例の両方) - [ ] AC9: hook の判定が失敗しても(入力 JSON が壊れている・記録を書けない)、ツールの実行を止めない(終了コード 0 で通す) @@ -107,8 +108,10 @@ ### 会話を切る hook(#830) -- [ ] AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で工程 Skill を起動すると、PreToolUse:Skill hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す -- [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない +- ~~AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で工程 Skill を起動すると、PreToolUse:Skill hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す~~ → 変更(2026-09-23、PR #843 のレビュー 4 回目。3 層では conductor が工程 Skill を起動しないため) +- [ ] AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で、conductor が工程 Skill を起動する(対話の経路)か、持ち場の supervisor を起動する(3 層の経路)と、hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す +- ~~AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない~~ → 変更(2026-09-23、PR #843 のレビュー 4 回目。3 層の経路で Agent の起動も見るため) +- [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill と Agent の起動は拒否しない - [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` など)の起動は拒否しない - ~~AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる)~~ → 変更(2026-09-23、PR #843 のレビュー) - ~~AC15: 拒否した直後の同じ Skill・同じ args の起動は通す。別の工程 Skill の起動は再び拒否する。環境変数で判定ごと止められる~~ → 変更(2026-09-23、PR #843 のレビュー 2 回目。hook は Bash / Read / Skill しか見ないため『直後』を判定できない) @@ -150,9 +153,9 @@ | 対象 | 影響 | | --- | --- | -| 公開インタフェース | Claude Code の hook の定義に PreToolUse の matcher(`Bash` / `Read` / `Skill`)の登録が増える。環境変数が増える(止める・上限を変える) | +| 公開インタフェース | Claude Code の hook の定義に PreToolUse の matcher(`Bash` / `Read` / `Skill` / `Agent` / `Task`)の登録が増える。環境変数が増える(止める・上限を変える) | | データ | 連続 Read を数える状態を、会話ごとに一時ファイルへ持つ | -| 既存の振る舞い | 前景の `sleep` で待つ Bash(`while` / `until` のループか、5 秒を超える `sleep`)が拒否される。文脈が上限を超えた conductor では工程 Skill の起動が 1 度拒否される。`external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` の前景の待ちのループの直前に、`run_in_background` で実行する案内の 1 行が足される | +| 既存の振る舞い | 前景の `sleep` で待つ Bash(`while` / `until` のループの本体にあるか、5 秒を超える `sleep`)が拒否される。文脈が上限を超えた conductor では、工程 Skill と持ち場の supervisor の起動が 1 度拒否される。`external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` の前景の待ちのループの直前に、`run_in_background` で実行する案内の 1 行が足される | ## 検証手段 From bf097fdce0e7781ee34ddbc768232841a4d7c711 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 00:57:37 +0000 Subject: [PATCH 07/30] =?UTF-8?q?docs:=20#843=20=E7=94=A8=E8=AA=9E?= =?UTF-8?q?=E8=A1=A8=E3=81=AE=E5=B7=A5=E7=A8=8B=20Skill=20=E3=81=AB?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E3=81=AE=202=20=E3=81=A4=E3=82=92=E5=90=AB?= =?UTF-8?q?=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index 1a8de1cdf..c9fb03a1f 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -80,7 +80,7 @@ | ポーリング | 待つ間に、状態を確かめるための呼び出しを繰り返すこと。#827 の `poll.py` は `sleep <数字>` を含む Bash、`tasks/*.output` の Read、出力ファイルやログの `tail` / `cat` / `wc` / `grep` を数える | | 前景の Bash | `run_in_background` を付けずに実行する Bash。終わるまで呼び出しが返らない | | 文脈量 | 1 回の API 呼び出しで読んだトークン数。`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` | -| 工程 Skill | `development-workflow` の工程表が起動する Skill(`requirements-design` / `design` / `pr` など) | +| 工程 Skill | `development-workflow` の工程表が起動する Skill(`requirements-design` / `design` / `pr` など)と、工程へ入る入口の `development-workflow` / `issue-plan-strategy` | | 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行。`context-window.md` の 4 つの切れ目と文脈量の hook の拒否で conductor が出す | ## 受け入れ条件 From 0ee39c1360ab5e8c32aea2c4e7a1aa28b4e56aff Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 01:05:02 +0000 Subject: [PATCH 08/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=205=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88=E9=96=A2=E9=96=80=E3=81=AE=E5=BE=8C?= =?UTF-8?q?=E3=81=AB=E5=88=87=E3=82=8B=E3=83=BBAgent=20=E3=81=AE=E5=86=8D?= =?UTF-8?q?=E5=AE=9F=E8=A1=8C=E3=83=BB=E5=B7=A5=E7=A8=8B=20Skill=20?= =?UTF-8?q?=E3=81=AE=E4=B8=80=E8=A6=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 38 ++++++++++++++++++++-------- issues/issue-829-830-requirements.md | 10 +++++--- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index daa25ace1..e5d70c51b 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -12,7 +12,7 @@ を 30 回繰り返すと、30 回とも文脈の全体を読み直す。変更後は、1 回目の `sleep 60 && tail` を hook が拒否し、理由の欄で「待ちの条件を until ループにして `run_in_background` で起動し、完了通知を待つ」よう案内する。 -**#830 の例。** conductor の文脈が 41 万のまま `/ndf:design` を起動する(3 層では `設計:` の +**#830 の例。** conductor の文脈が 41 万のまま `/ndf:implementation-plan` を起動する(3 層では `実装:` の supervisor を起動する)と、変更後は hook が起動を 1 度拒否し、「新しい会話で `/ndf:development-workflow #829` を打つ」と案内する。 conductor はその 1 行を利用者へ示して止まる。 @@ -24,7 +24,7 @@ conductor はその 1 行を利用者へ示して止まる。 | F2 | 前景で `sleep` を使って待つ Bash(ループの待ちと長い `sleep`)を止め、代わりの待ち方を知らせる | Claude Code のエージェント(全層) | | F3 | 変わっていないファイルの同じ範囲を続けて読み直す Read を止め、代わりの待ち方を知らせる | 同上 | | F4 | 文脈が上限を超えた conductor が工程へ入る起動(対話の経路は工程 Skill、3 層の経路は持ち場の supervisor の Agent)を 1 度止め、新しい会話で打つ 1 行を知らせる | conductor と、それを見る利用者 | -| F5 | `context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)と、文脈量の hook が拒否したときに、次の工程を始める 1 行を出す | conductor(全ランタイム) | +| F5 | `context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)と、文脈量の hook が拒否したときに、次の工程を始める 1 行を出す。持ち場の報告が `結果: 関門` なら、関門の承認と取り込みの後に出す | conductor(全ランタイム) | | F6 | その 1 行から始めた新しい会話で、モード・作業ツリー・現在の工程を戻す | conductor(全ランタイム) | | F7 | hook を種類ごとに止める・上限を変える | 利用者 | @@ -33,7 +33,7 @@ conductor はその 1 行を利用者へ示して止まる。 | 要素 | 新設 / 変更 | 責務 | | --- | --- | --- | | `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(`Bash` → sleep / `Read` → 連続 Read / `Skill`・`Agent`・`Task` → 文脈量)へ振り分け、拒否か通過を返す | -| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名。入口の `development-workflow` と `issue-plan-strategy` を含む)。F4 がこの一覧に無い Skill を見ない | +| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名)。下の「工程 Skill の一覧」の表の 13 個を正とする。F4 がこの一覧に無い Skill を見ない | | `plugins/ndf/hooks/claude.json` | 変更 | PreToolUse に matcher `Bash\|Read\|Skill\|Agent\|Task` で `token-guard.sh` を登録する | | `development-workflow/references/waiting.md` | 新設 | 待ち方の規約の唯一の置き場所(F1) | | `development-workflow/references/agent-layers.md` | 変更 | supervisor の規則 4 と worker の規則に、`waiting.md` への参照を 1 行ずつ足す | @@ -43,6 +43,22 @@ conductor はその 1 行を利用者へ示して止まる。 | `plugins/ndf/scripts/tests/test_token_guard.py` | 新設 | F2〜F4 と F7 の判定を、入力 JSON と transcript の見本で確かめる | | `plugins/ndf/README.md` | 変更 | hook の一覧に `token-guard.sh` を足し、4 ランタイムでの扱いを表で示す | +### 工程 Skill の一覧 + +`token-guard-stages.txt` は工程表から機械的に抽出しない。この表を正とする。 +`context-window.md` の 4 つの切れ目の直後に始まる工程の Skill と、入口の 2 つだけを載せる(合計 13 個)。 + +| 切れ目 | 直後に始まる工程の Skill | +| --- | --- | +| 1 ドキュメントレビューのマージの後 | `implementation-plan` / `document-drafting` | +| 2 構造改善と実装レビューの前後 | `cross-refactoring` / `cross-review` / `pr-review` / `quality-gates` | +| 3 Pull Request を出した後 | `plan-to-spec` / `merged` | +| 4 配布の後 | `layout-review` / `release-verification` / `retrospective` | +| 入口 | `development-workflow` / `issue-plan-strategy` | + +`worktree` など切れ目の内側の工程は含めない(理由は決定 6)。 +`cross-review` は切れ目 1 の前(ドキュメントレビュー)でも起動される。その時点で上限を超えていれば止めてよいので含める。 + ```mermaid graph TB subgraph CC["Claude Code の会話"] @@ -244,6 +260,8 @@ plugins/ndf/ 1 つ目は、`context-window.md` の 4 つの切れ目で conductor が引き継ぎの 1 行を出すこと。 2 つ目は、3 層では conductor が `## 持ち場の報告` を受け取った時点で出し、supervisor は出さないこと。 supervisor の持ち場の境がこの切れ目に当たるためである。文脈量の hook が拒否したときも出す。 +ただし報告が `結果: 関門` なら受け取った時点では出さず、関門の承認と取り込み(設計 Pull Request のマージなど)の後に出す。 +関門の前に会話を切らないためで、切れ目 1 はこの形で満たす。 3 つ目は、戻す手順が `context-window.md` にあること。 **`agent-layers.md`(変更)** は supervisor の規則 4 の後ろに「待ち方は `waiting.md` に従う」を足し、 @@ -391,7 +409,7 @@ CLI 側の消費を測った後(#827 の次の手順)に改めて決める | 経路 | conductor が起動するもの | 捕まえる入力 | 印の鍵 | | --- | --- | --- | --- | -| 対話 | 工程 Skill(例 `ndf:design`)。対話では 3 層へ出さない(`development-workflow/SKILL.md` の「`/goal` の引数として呼ばれたとき」の末尾) | `Skill`。名前が `token-guard-stages.txt` にある(入口の `development-workflow` と `issue-plan-strategy` を含む) | `skill`・`args` | +| 対話 | 工程 Skill(例 `ndf:implementation-plan`)。対話では 3 層へ出さない(`development-workflow/SKILL.md` の「`/goal` の引数として呼ばれたとき」の末尾) | `Skill`。名前が `token-guard-stages.txt` にある(上の「工程 Skill の一覧」の 13 個) | `skill`・`args` | | 3 層 | 持ち場ごとの supervisor。conductor は `development-workflow` と `issue-plan-strategy` 以外を起動しない(`agent-layers.md` の「3 層の責務」) | `Agent`(旧名 `Task`)。`agent_id` が無く、`description` の先頭語が持ち場の語彙(`agent-layers.md` の「起動の指示」) | `description` | 3 層の経路の `Skill` だけを見ると、捕まるのは入口の起動だけで、持ち場の切れ目で止まらない。 @@ -400,7 +418,7 @@ CLI 側の消費を測った後(#827 の次の手順)に改めて決める supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が途中で 途切れる。supervisor の切れ目は #768 / #773 が測ってから決める。工程でない Skill (`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 -されるため、切れ目にならない。 +されるため、切れ目にならない。`worktree` など切れ目の内側の工程も、同じ理由で一覧から外す。 ### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、次の同じ起動だけを通す @@ -414,7 +432,7 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 ### 決定 8: 引き継ぎの 1 行は `development-workflow` を起動する形にする -工程 Skill を直接起動する形(`/ndf:design #829`)は採らない。工程 Skill はモード・作業ツリー・ +工程 Skill を直接起動する形(`/ndf:implementation-plan #829`)は採らない。工程 Skill はモード・作業ツリー・ 承認の状態を戻す手順を持たず、戻す手順を持つのは `development-workflow` の側だからである。 `development-workflow` を経由すると固定費に 1 回分の読み込み(約 1 万トークン)が足されるが、 切る前の会話の文脈(#827 で平均 41 万)に比べて小さい。 @@ -449,13 +467,13 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | | AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える。閾値: `NDF_SLEEP_MAX_SEC=30` で `sleep 10` は出力なし・`sleep 40` は deny、`NDF_READ_REPEAT_LIMIT=2` で 2 回目に deny、`NDF_CONTEXT_LIMIT=300000` で文脈量 250,000 は出力なし | | AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | -| AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:design"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829`。3 層: `tool_name: Agent`、`description: "設計: #829 #830"` で deny と `/ndf:development-workflow #829 #830`。args に番号が無ければ `<課題番号>` のまま(控えが複数あっても推測しない) | +| AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:implementation-plan"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829`。3 層: `tool_name: Agent`、`description: "設計: #829 #830"` で deny と `/ndf:development-workflow #829 #830`。args に番号が無ければ `<課題番号>` のまま(控えが複数あっても推測しない) | | AC13 | 単体: AC12 の Skill と Agent の入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | -| AC14 | 単体: `ndf:markdown-writing` → 出力なし。`description` の先頭語が `調査:` の Agent → 出力なし。`token-guard-stages.txt` の名前が `SKILL.md` の工程表の Skill の列と入口の 2 つに一致することを文書テストで確かめる | -| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:pr`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | +| AC14 | 単体: `ndf:markdown-writing` と `ndf:worktree` → 出力なし。`description` の先頭語が `調査:` の Agent → 出力なし。文書テスト: `token-guard-stages.txt` の名前が設計の「工程 Skill の一覧」の 13 個と一致し、どれも `plugins/ndf/manifests/` の Skill 一覧にある | +| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:merged`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。Agent: `description: "設計: #829"` で 1 回目 deny → 同じ description で 2 回目は出力なし → 3 回目の `実装: #829` で deny。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | -| AC18 / AC19 | 文書の検査: `SKILL.md` に 4 つの切れ目と hook の拒否で conductor が 1 行を出す規約(3 層では `## 持ち場の報告` を受け取った時点)があり、`context-window.md` に戻す手順の表がある | +| AC18 / AC19 | 文書の検査: `SKILL.md` に 4 つの切れ目と hook の拒否で conductor が 1 行を出す規約(3 層では `## 持ち場の報告` を受け取った時点。`結果: 関門` なら関門の承認と取り込みの後)があり、`context-window.md` に戻す手順の表がある | | AC20 | 実機: 実装の Pull Request の途中で会話を切り、1 行だけで新しい会話を始め、モード・作業ツリー・次の工程が戻ったことを #830 に残す | | AC21 / AC22 | 文書の検査: 「実測ではない」の文面が消え、#827 への参照と数値があり、上限の値が `NDF_CONTEXT_LIMIT` の既定と一致する | | AC23 | 文書の検査: README に 4 ランタイムの表がある | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index c9fb03a1f..dbcb53dd0 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -80,7 +80,7 @@ | ポーリング | 待つ間に、状態を確かめるための呼び出しを繰り返すこと。#827 の `poll.py` は `sleep <数字>` を含む Bash、`tasks/*.output` の Read、出力ファイルやログの `tail` / `cat` / `wc` / `grep` を数える | | 前景の Bash | `run_in_background` を付けずに実行する Bash。終わるまで呼び出しが返らない | | 文脈量 | 1 回の API 呼び出しで読んだトークン数。`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` | -| 工程 Skill | `development-workflow` の工程表が起動する Skill(`requirements-design` / `design` / `pr` など)と、工程へ入る入口の `development-workflow` / `issue-plan-strategy` | +| 工程 Skill | `context-window.md` の 4 つの切れ目の直後に始まる工程の Skill と、入口の `development-workflow` / `issue-plan-strategy`(一覧は設計文書) | | 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行。`context-window.md` の 4 つの切れ目と文脈量の hook の拒否で conductor が出す | ## 受け入れ条件 @@ -112,17 +112,19 @@ - [ ] AC12: Claude Code で、会話の文脈量が上限(既定 200,000)を超えた状態で、conductor が工程 Skill を起動する(対話の経路)か、持ち場の supervisor を起動する(3 層の経路)と、hook が起動を拒否し、理由の欄に「新しい会話で打つ 1 行」を示す - ~~AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill の起動は拒否しない~~ → 変更(2026-09-23、PR #843 のレビュー 4 回目。3 層の経路で Agent の起動も見るため) - [ ] AC13: 上限の判定は conductor(本体の会話)だけに掛かる。サブエージェントの中の Skill と Agent の起動は拒否しない -- [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` など)の起動は拒否しない +- [ ] AC14: 工程 Skill でない Skill(`markdown-writing` / `progress-tracking` / `out-of-scope` / `worktree` など)の起動は拒否しない。`worktree` は切れ目の内側の工程のため拒否しない - ~~AC15: 同じ会話で案内を 1 度出した後、利用者がそのまま続けると決めたときに続けられる(2 回目の同じ起動は通す、または環境変数で止められる)~~ → 変更(2026-09-23、PR #843 のレビュー) - ~~AC15: 拒否した直後の同じ Skill・同じ args の起動は通す。別の工程 Skill の起動は再び拒否する。環境変数で判定ごと止められる~~ → 変更(2026-09-23、PR #843 のレビュー 2 回目。hook は Bash / Read / Skill しか見ないため『直後』を判定できない) -- [ ] AC15: 拒否の後、次に起動した工程 Skill が拒否したものと同じ skill・args なら 1 度だけ通す(間に他のツールや工程でない Skill が挟まってもよい)。次の工程 Skill の起動が別の skill か args なら、印を置き換えて再び拒否する。環境変数で判定ごと止められる +- ~~AC15: 拒否の後、次に起動した工程 Skill が拒否したものと同じ skill・args なら 1 度だけ通す(間に他のツールや工程でない Skill が挟まってもよい)。次の工程 Skill の起動が別の skill か args なら、印を置き換えて再び拒否する。環境変数で判定ごと止められる~~ → 変更(2026-09-23、PR #843 のレビュー 5 回目。3 層の Agent の再実行を規定していなかったため) +- [ ] AC15: 拒否の後、次に起動した工程 Skill が拒否したものと同じ skill・args なら 1 度だけ通す(間に他のツールや工程でない Skill が挟まってもよい)。次の工程 Skill の起動が別の skill か args なら、印を置き換えて再び拒否する。環境変数で判定ごと止められる。3 層の経路では、同じ description の次の持ち場の起動を 1 度だけ通す。別の description は再び拒否する - [ ] AC16: 文脈量を読めない(transcript が無い・`usage` が無い)ときは拒否しない - [ ] AC17: AC12〜AC16 の判定を、transcript の見本を与えて終了コードと出力を見るテストが確かめている ### 引き継ぎの 1 行(#830) - ~~AC18: `development-workflow` は、工程を 1 つ終えるたびに、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す~~ → 変更(2026-09-23、PR #843 のレビュー 3 回目。3 層では conductor が工程の終わりを観測しないため、既存の切れ目にそろえた) -- [ ] AC18: conductor は、`context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)で、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す。3 層では supervisor の持ち場の境がこの切れ目に当たるため、conductor が `## 持ち場の報告` を受け取った時点で出す(supervisor は出さない)。文脈量の hook が拒否したときにも出す +- ~~AC18: conductor は、`context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)で、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す。3 層では supervisor の持ち場の境がこの切れ目に当たるため、conductor が `## 持ち場の報告` を受け取った時点で出す(supervisor は出さない)。文脈量の hook が拒否したときにも出す~~ → 変更(2026-09-23、PR #843 のレビュー 5 回目。関門の前に会話を切らないため) +- [ ] AC18: conductor は、`context-window.md` の 4 つの切れ目(ドキュメントレビューのマージの後 / 構造改善と実装レビューの前後 / Pull Request を出した後 / 配布の後)で、次の工程を始めるコマンド 1 行(`/ndf:development-workflow #` の形。工程 Skill はモードと作業ツリーを戻す手順を持たないため)を出す。3 層では supervisor の持ち場の境がこの切れ目に当たるため、conductor が `## 持ち場の報告` を受け取った時点で出す(supervisor は出さない)。ただし報告が `結果: 関門` のときは受け取った時点では出さず、関門の承認と取り込み(設計 Pull Request のマージなど)が済んだ後に出す。切れ目 1(ドキュメントレビューのマージの後)はこの形で満たす。文脈量の hook が拒否したときにも出す - [ ] AC19: そのコマンド 1 行だけで始めた新しい会話が、課題の本文の `## 進行`・Pull Request・通過工程の控えから、モード・作業ツリー・現在の工程を戻せる。戻す手順が文書にある - [ ] AC20: AC19 を、実際の課題 1 件で新しい会話から再開して確かめ、結果を本 issue に残している From 9f9daa575f3f08b4b0fbd649edb6759120a1b6a9 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 01:13:33 +0000 Subject: [PATCH 09/30] =?UTF-8?q?docs:=20#843=20=E3=83=A9=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=89=206=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88session=20=E3=81=94=E3=81=A8?= =?UTF-8?q?=E3=81=AE=E6=8E=92=E4=BB=96=E3=83=BBinode=20=E3=81=AE=20BSD=20?= =?UTF-8?q?=E5=BD=A2=E3=83=BB=E7=BD=AE=E3=81=8D=E5=A0=B4=E6=89=80=E3=81=AE?= =?UTF-8?q?=E8=87=AA=E5=89=8D=E3=81=AE=E8=A7=A3=E6=B1=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design.md | 46 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index e5d70c51b..8685dfa28 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -32,7 +32,7 @@ conductor はその 1 行を利用者へ示して止まる。 | 要素 | 新設 / 変更 | 責務 | | --- | --- | --- | -| `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(`Bash` → sleep / `Read` → 連続 Read / `Skill`・`Agent`・`Task` → 文脈量)へ振り分け、拒否か通過を返す | +| `plugins/ndf/scripts/token-guard.sh` | 新設 | PreToolUse の入口。`tool_name` で 3 つの判定(`Bash` → sleep / `Read` → 連続 Read / `Skill`・`Agent`・`Task` → 文脈量)へ振り分け、拒否か通過を返す。排他は既存の `scripts/lib/lock-common.sh` を読み込んで使う | | `plugins/ndf/scripts/lib/token-guard-stages.txt` | 新設 | 工程 Skill の名前の一覧(1 行 1 名)。下の「工程 Skill の一覧」の表の 13 個を正とする。F4 がこの一覧に無い Skill を見ない | | `plugins/ndf/hooks/claude.json` | 変更 | PreToolUse に matcher `Bash\|Read\|Skill\|Agent\|Task` で `token-guard.sh` を登録する | | `development-workflow/references/waiting.md` | 新設 | 待ち方の規約の唯一の置き場所(F1) | @@ -69,7 +69,7 @@ graph TB TG["token-guard.sh"] end subgraph ST["状態"] - RS["連続 Read の控え
#lt;wf_state_dir の親#gt;/guards/"] + RS["連続 Read の控えと印
#lt;自前で解決した親#gt;/guards/"] TR["会話の記録
transcript_path"] SL["token-guard-stages.txt"] end @@ -130,27 +130,30 @@ plugins/ndf/ **連続 Read の控えを、会話ごとに 1 つの小さなファイルへ持つ。** 置き場所は `guards/read-.json`。 -**`guards/` の場所は、通過工程の控えの場所から決める。** `token-guard.sh` は -`development-workflow/scripts/lib/workflow-common.sh` を読み込み、その `wf_state_dir` で -通過工程の控えの場所を得る。使うのは `guards/` の置き場所を決めることだけで、控えの番号は読まない。解決順は次のとおりで、先に使えたものを採る。 +**`guards/` の場所は `token-guard.sh` が自前で解決する。** 次の順で先に使えたものの下に置く。 +順は `wf_state_dir` と同じで、テストが確かめる(AC9)。`workflow-common.sh` は読み込まない。末尾で通信の層まで読み込むため毎回の hook には重く、その層の変更が hook へ波及する。 -1. `$CLAUDE_PLUGIN_DATA/stages` -2. `$XDG_STATE_HOME/ndf/stages` -3. `$HOME/.local/state/ndf/stages` -4. `${TMPDIR:-/tmp}/ndf-stages` +1. `$CLAUDE_PLUGIN_DATA` +2. `$XDG_STATE_HOME/ndf` +3. `$HOME/.local/state/ndf` +4. `${TMPDIR:-/tmp}`(このときだけ `ndf-guards` の名前で置く) -`guards/` は得たディレクトリと同じ親に置く。4 番目のときは `${TMPDIR:-/tmp}/ndf-guards` に置く。 -**読み込めないときは Read と文脈量の判定を通す。** sleep の判定は状態を持たないので続ける。 +**`guards/` を作れないときは Read と文脈量の判定を通す。** sleep の判定は状態を持たないので続ける。 | キー | 型 | 意味 | | --- | --- | --- | | `key` | 文字列 | 直前の Read の `file_path` と `offset` と `limit` を `\t` でつないだもの | | `size` | 整数 | 直前の Read の時点のファイルの大きさ(バイト)。無いファイルは `-1` | | `mtime` | 文字列 | 同じく更新時刻(ナノ秒の精度。GNU の `stat -c %.9Y`、BSD の `stat -f %Fm`) | -| `inode` | 整数 | 同じく inode 番号(`stat -c %i`)。無いファイルは `-1` | +| `inode` | 整数 | 同じく inode 番号(GNU の `stat -c %i`、BSD の `stat -f %i`)。無いファイルは `-1` | | `count` | 整数 | `key`・`size`・`mtime`・`inode` が変わらないまま続いた Read の回数 | - **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない +- **読み・判定・書き込みは session ごとのロック `guards/.lock` の中で行う。** 同じ session の + hook が並列に走ると、置き換えだけでは `count` の更新や印が失われる。控えと印の両方に当てる +- ロックは `lock-common.sh` の `ndf_lock_acquire 2` / `ndf_lock_release` で取る。`flock` を + 使わない仕組みで、排他の手順はリポジトリでそこ 1 か所にある。標準出力へ書かず hook の JSON に混ざらない +- **2 秒で取れなければ判定せず通す**(可用性。AC9 と同じ扱い)。sleep の判定はロックを取らない - **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を hook は受け取らないため - **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の鍵を持つ。 @@ -281,19 +284,24 @@ sequenceDiagram H->>H: 背景か / -c・eval の中身を取り出し同じ判定 / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と、while・until の本体にあるか H-->>A: 当たれば拒否(待ち方の案内) else tool_name = Read + H->>S: ロックを取る(2 秒で取れなければ通す) H->>S: 控えを読む・ファイルの size と mtime と inode を取る - H->>S: 控えを置き換える(count を進めるか 1 に戻す) + H->>S: 控えを置き換え(count を進めるか 1 に戻す)、ロックを放す H-->>A: count が上限に達すれば拒否 else tool_name = Skill / Agent / Task H->>H: 工程 Skill か・先頭語が持ち場の Agent か / サブエージェントか H->>S: transcript の末尾から文脈量を読む + H->>S: ロックを取る(2 秒で取れなければ通す) H->>S: 案内の印を読む(間の他のツールでは消えない) alt 印が同じ鍵(skill・args か description)を持つ - H->>S: 印を消す + H->>S: 印を消す・ロックを放す H-->>A: 何も出さず 0(1 度だけ通す) else 上限超え - H->>S: 印をこの起動の鍵で置き換える + H->>S: 印をこの起動の鍵で置き換える・ロックを放す H-->>A: 拒否(1 行を示す) + else 上限以内 + H->>S: ロックを放す + H-->>A: 何も出さず 0 end end ``` @@ -321,7 +329,7 @@ stateDiagram-v2 | --- | --- | --- | | 性能・拡張性 | 50 MB の記録でも 1 秒以内 | 記録は `tail -n 200` の範囲だけを読む。Bash と Read の判定は記録を読まない。登録の `timeout` は 5 秒 | | 運用・保守性 | 理由の欄だけで次の手が分かる | 理由の欄に代わりの手段と規約の場所を必ず書く(出力の表) | -| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。`workflow-common.sh` を読み込めないときは Read と文脈量の判定を通し、sleep の判定だけを続ける。登録に `continueOnError: true` | +| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。`guards/` を作れない・ロックを 2 秒で取れないときは Read と文脈量の判定を通し、sleep の判定だけを続ける。登録に `continueOnError: true` | ## 決定の記録 @@ -462,15 +470,15 @@ supervisor は 1 つの持ち場の中で複数の工程を通すため、工程 | AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | | AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done`・`bash -c 'sleep 30'`・`timeout 590 bash -c "until [ -s f ]; do sleep 5; done"`・`sh -c 'until test -s x; do sleep 1; done'` で deny と理由の欄に `run_in_background` と `waiting.md` | | AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`while read l; do echo "$l"; done < f; sleep 1`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | -| AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し | +| AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し。同じ session で Read の hook を 2 本並列に起動しても count が 2 進む(更新が失われない) | | AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | -| AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0 | +| AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0。ロックを他が持ったまま 2 秒を超えると出力なしで 0。同じ環境変数の下で `guards/` の親が `wf_state_dir` の親と一致する(4 段それぞれ) | | AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える。閾値: `NDF_SLEEP_MAX_SEC=30` で `sleep 10` は出力なし・`sleep 40` は deny、`NDF_READ_REPEAT_LIMIT=2` で 2 回目に deny、`NDF_CONTEXT_LIMIT=300000` で文脈量 250,000 は出力なし | | AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | | AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:implementation-plan"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829`。3 層: `tool_name: Agent`、`description: "設計: #829 #830"` で deny と `/ndf:development-workflow #829 #830`。args に番号が無ければ `<課題番号>` のまま(控えが複数あっても推測しない) | | AC13 | 単体: AC12 の Skill と Agent の入力に `agent_id` を足す、または `transcript_path` を `/subagents/` の下にする → 出力なし | | AC14 | 単体: `ndf:markdown-writing` と `ndf:worktree` → 出力なし。`description` の先頭語が `調査:` の Agent → 出力なし。文書テスト: `token-guard-stages.txt` の名前が設計の「工程 Skill の一覧」の 13 個と一致し、どれも `plugins/ndf/manifests/` の Skill 一覧にある | -| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:merged`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。Agent: `description: "設計: #829"` で 1 回目 deny → 同じ description で 2 回目は出力なし → 3 回目の `実装: #829` で deny。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし | +| AC15 | 単体: 同じ `session_id` で 1 回目 deny → 同じ skill・args で 2 回目は出力なし → 3 回目の別の工程 Skill(例 `ndf:merged`)で再び deny。拒否の後に Bash と Read を挟んでも、次の同じ起動は通る。Agent: `description: "設計: #829"` で 1 回目 deny → 同じ description で 2 回目は出力なし → 3 回目の `実装: #829` で deny。`NDF_CONTEXT_GUARD=0` で 1 回目も出力なし。同じ起動の 2 回目の hook を 2 本並列に起動すると、通るのは 1 本だけ | | AC16 | 単体: `transcript_path` が無い・`usage` の無い記録 → 出力なし | | AC17 | AC12〜AC16 のテストが通る | | AC18 / AC19 | 文書の検査: `SKILL.md` に 4 つの切れ目と hook の拒否で conductor が 1 行を出す規約(3 層では `## 持ち場の報告` を受け取った時点。`結果: 関門` なら関門の承認と取り込みの後)があり、`context-window.md` に戻す手順の表がある | From 3882e1119d03b9b1c387001906fecf923b8628d6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 01:21:01 +0000 Subject: [PATCH 10/30] =?UTF-8?q?docs:=20#843=20=E6=9C=80=E7=B5=82?= =?UTF-8?q?=E3=82=B9=E3=82=A4=E3=83=BC=E3=83=97=EF=BC=88=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E5=BE=85=E3=81=A1=E3=81=AE=E4=B8=8A=E9=99=90=E3=83=BB?= =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=97=E5=88=A4=E5=AE=9A=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=BB=E6=B1=BA=E5=AE=9A=E3=81=AE=E8=A8=98?= =?UTF-8?q?=E9=8C=B2=E3=82=92=E5=88=86=E5=89=B2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0188ycp9tV288qHTswhMxQ7K --- issues/issue-829-830-design-decisions.md | 135 ++++++++++++++++++++ issues/issue-829-830-design.md | 151 ++--------------------- issues/issue-829-830-requirements.md | 4 +- 3 files changed, 148 insertions(+), 142 deletions(-) create mode 100644 issues/issue-829-830-design-decisions.md diff --git a/issues/issue-829-830-design-decisions.md b/issues/issue-829-830-design-decisions.md new file mode 100644 index 000000000..d76cb7e75 --- /dev/null +++ b/issues/issue-829-830-design-decisions.md @@ -0,0 +1,135 @@ +# #829 / #830: 決定の記録 + +設計は [issue-829-830-design.md](issue-829-830-design.md) にある。 + +## 決定の記録 + +### 決定 1: 2 つの課題を 1 本の実装 Pull Request にまとめる + +hook の入口・登録・状態の置き場所・拒否の返し方が同じで、分けると `hooks/claude.json` と +テストの土台を 2 本が同時に触る。2 本に分ける形は採らない。後に入る側が土台の食い違いを解く +手間が、分けて読みやすくなる利点を上回る。 + +### 決定 2: 待ち方の規約は `development-workflow/references/waiting.md` の新しいファイルに置く + +`agent-layers.md` は #828(持ち場ごとの抜粋)も同時に触る。待ち方の規約は、 +#731(`bg-wait.sh` を共通層へ移す)と external-ai / cross-review の文書からも参照される。 +同じファイルの節にすると、参照するたびに 3 層の規約の全体を読ませる。`agent-layers.md` と `parallel-work.md` の節に置く形は +採らない。`parallel-work.md` は待ち方の道具に触れておらず、そこへ足す理由が無い。 + +### 決定 3: sleep の判定は「前景で、`while` / `until` のループの本体にあるか、5 秒を超える `sleep`」を拒否する + +文字列やコメントの中の `sleep` は数えない。`echo sleep 30` や `git commit -m "sleep 60"` を止めないよう、 +引用・コメント・ヒアドキュメントを除いた後の語の位置で判定する。 + +ただし引用を取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数を取り出す。 +前に `timeout` / `nohup` / `env` が付く形(`timeout 590 bash -c "..."`)も含める。 +取り出した中身へ同じ判定を当て、入れ子も同じ規則で 1 段ずつ見る。 +引用の中身は実行されるため、除くだけだと `bash -c 'sleep 30'` を見逃す。 + +ループとして数えるのは、`sleep` が `while` / `until` の `do` と対応する `done` の間にあるときだけである。 +本体の外の `sleep`(`while read l; do ...; done < f; sleep 1`)は秒数の上限だけで見る。 +`while` と `sleep` が同じコマンドにあるだけで拒否すると、ループの後の短い間まで止める。 + +ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 +(6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 + +| 形 | 件数 | 費用(input 換算) | +| --- | ---: | ---: | +| 前景・ループの中(`while [ $n -lt 40 ]; do ...; sleep ...; done` など) | 1,543 | 67.3M | +| 前景・ループなし(`sleep 30 && tail x` など) | 743 | 9.9M | +| 背景(`run_in_background`) | 441 | 5.3M | + +ループの 1 回の呼び出しは 600 秒で打ち切られ、待ちが長いと呼び直しが続く。同じループを背景へ移せば、 +待つ時間の長さによらず完了通知 1 回で済む。ループなしの形の半数(382 件)は 5 秒以下で、サーバの起動を +待つような短い間であるため通す。`for` のループで 5 秒以下の `sleep` を挟む形(API の照会の +間隔を空ける使い方)も通す。 + +ループの中の `sleep` をすべて通す形は採らない。前景の `sleep` の費用の 87% を占める形が残る。`sleep` を含む +Bash をすべて拒否する形も採らない。短い間と照会の間隔まで止めると、代わりの手段が無い。 +配布物の文書にある前景の待ちのループも拒否に当たる。理由の欄が「同じループを +`run_in_background: true` で」と案内するため、Claude Code ではループを書き換えずに 1 回の回り道で済む。 + +| 文書 | ループ | 書き換える変更 | +| --- | --- | --- | +| `external-ai/references/cli-codex.md` / `cli-agy.md` | `until ! ps -p ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | +| `qa-security-scan/03-report-template.md` | `until grep -q ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | +| `release/references/completion-check.md` | `while :; do ...; sleep 5; done` | この変更で案内の 1 行。ループの書き換えは #731 | + +案内の 1 行は 4 文書とも同じで、ループの直前に置く: 「Claude Code では、このループを +`run_in_background: true` で実行して完了通知を待つ(`development-workflow/references/waiting.md`)」。 +hook と案内の行を同じ変更で配布するため、拒否と文書の順序が食い違わない。 +ループの書き換え(道具の共通化)は #731 で行う。 + +### 決定 4: 連続 Read は「同じ範囲・変わらないファイル・3 回目」で拒否する + +hook は実行の前に呼ばれ、読んだ中身を知らない。そのため「空ファイル」ではなく「前回から +大きさ・更新時刻・inode が変わっていない」で判定する。更新時刻はナノ秒の精度で持ち、inode も比べる。 +同じ秒に同じ大きさの内容で置き換えた(`mv`)ファイルを、変わっていないと取り違えないためである。空ファイルの読み直しはこれに含まれ、書き込みが +進むログの読み直しは含まれない。`offset` と `limit` を鍵に入れるのは、大きなファイルを範囲を +変えて読み進める正当な使い方を止めないためである。 + +3 回目にするのは、2026-08-23 以降の記録の実測による。同じ引数の Read が 3 回以上続いたのは 6 本で、 +うち 5 本が `tasks/*.output` の読み直し(最長 1,168 回)だった。残る 1 本は画像を見直す 3 回である。 +2 回目で止めると、正当な見直しに当たる機会が増える。間に他のツールが挟まったら数え直す形は +採らない。hook は Read の呼び出しにしか登録されず、間のツールを見られない。 + +### 決定 5: hook は Claude Code にだけ登録し、他の 3 ランタイムは規約で守る + +拒否の理由が案内する代わりの手段(`Monitor` / `run_in_background` の通知)は Claude Code にしか +無い。文脈量も Claude Code の `transcript_path` からしか読めない。#827 の実測も Claude Code の +記録だけで、Codex / Kiro / agy の消費は測っていない。3 ランタイムへも登録する形は採らない。 +Kiro は拒否すると代わりの口を持たず、agy は案内を控えへ積む形で、どちらも同じ案内を出せない。 +CLI 側の消費を測った後(#827 の次の手順)に改めて決める。 + +### 決定 6: 文脈量の判定は conductor が工程へ入る起動だけに掛ける + +**conductor が工程へ入る起動は、経路によって違うツールに現れる。** hook は両方を捕まえる。 + +| 経路 | conductor が起動するもの | 捕まえる入力 | 印の鍵 | +| --- | --- | --- | --- | +| 対話 | 工程 Skill(例 `ndf:implementation-plan`)。対話では 3 層へ出さない(`development-workflow/SKILL.md` の「`/goal` の引数として呼ばれたとき」の末尾) | `Skill`。名前が `token-guard-stages.txt` にある(上の「工程 Skill の一覧」の 13 個) | `skill`・`args` | +| 3 層 | 持ち場ごとの supervisor。conductor は `development-workflow` と `issue-plan-strategy` 以外を起動しない(`agent-layers.md` の「3 層の責務」) | `Agent`(旧名 `Task`)。`agent_id` が無く、`description` の先頭語が持ち場の語彙(`agent-layers.md` の「起動の指示」) | `description` | + +3 層の経路の `Skill` だけを見ると、捕まるのは入口の起動だけで、持ち場の切れ目で止まらない。 +先頭語が作業の種類(`調査` など)の Agent は持ち場でないため見ない。 + +supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が途中で +途切れる。supervisor の切れ目は #768 / #773 が測ってから決める。工程でない Skill +(`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 +されるため、切れ目にならない。`worktree` など切れ目の内側の工程も、同じ理由で一覧から外す。 + +### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、次の同じ起動だけを通す + +利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 +毎回拒否する形は採らない。続けると決めた利用者が、同じ工程をやり直せなくなる。 +会話ごとに 1 度にする形も採らない。1 度通した後は、以後の工程の切れ目で止まらなくなる。 +印に鍵(`skill` と `args`、Agent では `description`)を持つのは、通すのを工程へ入る次の同じ起動に限るためである。 +間のツールで印を失効させる形は採らない。hook は Edit などを見ないため、失効の条件を一貫して判定できない。 +拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` +に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 + +### 決定 8: 引き継ぎの 1 行は `development-workflow` を起動する形にする + +工程 Skill を直接起動する形(`/ndf:implementation-plan #829`)は採らない。工程 Skill はモード・作業ツリー・ +承認の状態を戻す手順を持たず、戻す手順を持つのは `development-workflow` の側だからである。 +`development-workflow` を経由すると固定費に 1 回分の読み込み(約 1 万トークン)が足されるが、 +切る前の会話の文脈(#827 で平均 41 万)に比べて小さい。 + +### 決定 9: 上限の既定は 200,000 にし、`skill-stats` の既定と同じ値にする + +`context-window.md` の「遅くとも 20 万」と、`skill-stats.py` の `DEFAULT_WINDOW_LIMIT` が同じ値を +持つ。hook だけ別の値にすると、測る側と止める側の上限が食い違う。10 万(目安の側)にする形は +採らない。#827 で固定費だけで約 4 万あり、1 工程の途中で止まる回数が増える。 + +### 決定 10: 1 回で足りる待ちは `Monitor` ではなく `run_in_background` の until ループにする + +#829 は「`Monitor` の until で 1 回だけ待つ」を挙げた。一方、Claude Code 2.1.280 の `Monitor` の +説明は、道具を次のように使い分ける。 + +| 待ち方 | 使う道具 | +| --- | --- | +| 通知が 1 回で足りる(終わるのを待つ) | `run_in_background` の until ループ | +| 出来事を 1 つずつ受ける | `Monitor` | +`Monitor` は既定 5 分・最長 30 分で打ち切られ、張り直しが要る。 +`Monitor` を 1 回の待ちの既定にする形は採らない。張り直しのたびに呼び出しが増える。 diff --git a/issues/issue-829-830-design.md b/issues/issue-829-830-design.md index 8685dfa28..e5368422b 100644 --- a/issues/issue-829-830-design.md +++ b/issues/issue-829-830-design.md @@ -2,6 +2,7 @@ 要求と受け入れ条件は [issue-829-830-requirements.md](issue-829-830-requirements.md) にある。 この文書は「どう作るか」だけを扱う。 +決定の記録は [issue-829-830-design-decisions.md](issue-829-830-design-decisions.md) にある。 **実装は 1 本の Pull Request にまとめる**(決定 1)。hook の入口と登録、状態の置き場所、 拒否の返し方を 2 つの課題で共有するためである。 @@ -151,9 +152,9 @@ plugins/ndf/ - **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない - **読み・判定・書き込みは session ごとのロック `guards/.lock` の中で行う。** 同じ session の hook が並列に走ると、置き換えだけでは `count` の更新や印が失われる。控えと印の両方に当てる -- ロックは `lock-common.sh` の `ndf_lock_acquire 2` / `ndf_lock_release` で取る。`flock` を +- ロックは `lock-common.sh` の `ndf_lock_acquire 1` / `ndf_lock_release` で取る。`flock` を 使わない仕組みで、排他の手順はリポジトリでそこ 1 か所にある。標準出力へ書かず hook の JSON に混ざらない -- **2 秒で取れなければ判定せず通す**(可用性。AC9 と同じ扱い)。sleep の判定はロックを取らない +- **1 秒で取れなければ判定せず通す**(可用性。AC9 と同じ扱い。待ちの上限を 1 秒にして非機能の 2 秒に収める)。sleep の判定はロックを取らない - **7 日より古い控えは、書き込みのついでに消す**(`find -mtime +7 -delete`)。会話が終わった合図を hook は受け取らないため - **文脈量の案内を出した印** は `guards/context-.json` に、拒否した起動の鍵を持つ。 @@ -284,14 +285,14 @@ sequenceDiagram H->>H: 背景か / -c・eval の中身を取り出し同じ判定 / コメント・引用・ヒアドキュメントを除く / コマンドの位置の sleep の秒数と、while・until の本体にあるか H-->>A: 当たれば拒否(待ち方の案内) else tool_name = Read - H->>S: ロックを取る(2 秒で取れなければ通す) + H->>S: ロックを取る(1 秒で取れなければ通す) H->>S: 控えを読む・ファイルの size と mtime と inode を取る H->>S: 控えを置き換え(count を進めるか 1 に戻す)、ロックを放す H-->>A: count が上限に達すれば拒否 else tool_name = Skill / Agent / Task H->>H: 工程 Skill か・先頭語が持ち場の Agent か / サブエージェントか H->>S: transcript の末尾から文脈量を読む - H->>S: ロックを取る(2 秒で取れなければ通す) + H->>S: ロックを取る(1 秒で取れなければ通す) H->>S: 案内の印を読む(間の他のツールでは消えない) alt 印が同じ鍵(skill・args か description)を持つ H->>S: 印を消す・ロックを放す @@ -327,152 +328,22 @@ stateDiagram-v2 | 大項目 | 条件 | 実現方式 | | --- | --- | --- | -| 性能・拡張性 | 50 MB の記録でも 1 秒以内 | 記録は `tail -n 200` の範囲だけを読む。Bash と Read の判定は記録を読まない。登録の `timeout` は 5 秒 | +| 性能・拡張性 | 50 MB の記録でも、競合しないとき 1 回 1 秒以内。ロックを待つときは待ちの上限 1 秒を足した 2 秒以内 | 記録は `tail -n 200` の範囲だけを読む。Bash と Read の判定は記録を読まない。ロック待ちの上限は 1 秒(`ndf_lock_acquire 1`)。登録の `timeout` は 5 秒のままでよい(最長の 2 秒に余裕がある) | | 運用・保守性 | 理由の欄だけで次の手が分かる | 理由の欄に代わりの手段と規約の場所を必ず書く(出力の表) | -| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。`guards/` を作れない・ロックを 2 秒で取れないときは Read と文脈量の判定を通し、sleep の判定だけを続ける。登録に `continueOnError: true` | +| 可用性 | hook の失敗で実行を止めない | 入力が読めない・`jq` が無い・控えが書けない・記録が読めないときは何も出さず 0。`guards/` を作れない・ロックを 1 秒で取れないときは Read と文脈量の判定を通し、sleep の判定だけを続ける。登録に `continueOnError: true` | -## 決定の記録 - -### 決定 1: 2 つの課題を 1 本の実装 Pull Request にまとめる - -hook の入口・登録・状態の置き場所・拒否の返し方が同じで、分けると `hooks/claude.json` と -テストの土台を 2 本が同時に触る。2 本に分ける形は採らない。後に入る側が土台の食い違いを解く -手間が、分けて読みやすくなる利点を上回る。 - -### 決定 2: 待ち方の規約は `development-workflow/references/waiting.md` の新しいファイルに置く - -`agent-layers.md` は #828(持ち場ごとの抜粋)も同時に触る。待ち方の規約は、 -#731(`bg-wait.sh` を共通層へ移す)と external-ai / cross-review の文書からも参照される。 -同じファイルの節にすると、参照するたびに 3 層の規約の全体を読ませる。`agent-layers.md` と `parallel-work.md` の節に置く形は -採らない。`parallel-work.md` は待ち方の道具に触れておらず、そこへ足す理由が無い。 - -### 決定 3: sleep の判定は「前景で、`while` / `until` のループの本体にあるか、5 秒を超える `sleep`」を拒否する - -文字列やコメントの中の `sleep` は数えない。`echo sleep 30` や `git commit -m "sleep 60"` を止めないよう、 -引用・コメント・ヒアドキュメントを除いた後の語の位置で判定する。 - -ただし引用を取り除く前に、`bash -c` / `sh -c` / `zsh -c` / `eval` の実行される引数を取り出す。 -前に `timeout` / `nohup` / `env` が付く形(`timeout 590 bash -c "..."`)も含める。 -取り出した中身へ同じ判定を当て、入れ子も同じ規則で 1 段ずつ見る。 -引用の中身は実行されるため、除くだけだと `bash -c 'sleep 30'` を見逃す。 - -ループとして数えるのは、`sleep` が `while` / `until` の `do` と対応する `done` の間にあるときだけである。 -本体の外の `sleep`(`while read l; do ...; done < f; sleep 1`)は秒数の上限だけで見る。 -`while` と `sleep` が同じコマンドにあるだけで拒否すると、ループの後の短い間まで止める。 - -ループの中の `sleep` を通すと、費用の大半を見逃す。2026-08-23 以降の全プロジェクトの記録 -(6,713 本、Bash 74,223 件)では、`sleep <数>` を含む前景の Bash は次のとおりだった。 - -| 形 | 件数 | 費用(input 換算) | -| --- | ---: | ---: | -| 前景・ループの中(`while [ $n -lt 40 ]; do ...; sleep ...; done` など) | 1,543 | 67.3M | -| 前景・ループなし(`sleep 30 && tail x` など) | 743 | 9.9M | -| 背景(`run_in_background`) | 441 | 5.3M | - -ループの 1 回の呼び出しは 600 秒で打ち切られ、待ちが長いと呼び直しが続く。同じループを背景へ移せば、 -待つ時間の長さによらず完了通知 1 回で済む。ループなしの形の半数(382 件)は 5 秒以下で、サーバの起動を -待つような短い間であるため通す。`for` のループで 5 秒以下の `sleep` を挟む形(API の照会の -間隔を空ける使い方)も通す。 - -ループの中の `sleep` をすべて通す形は採らない。前景の `sleep` の費用の 87% を占める形が残る。`sleep` を含む -Bash をすべて拒否する形も採らない。短い間と照会の間隔まで止めると、代わりの手段が無い。 -配布物の文書にある前景の待ちのループも拒否に当たる。理由の欄が「同じループを -`run_in_background: true` で」と案内するため、Claude Code ではループを書き換えずに 1 回の回り道で済む。 - -| 文書 | ループ | 書き換える変更 | -| --- | --- | --- | -| `external-ai/references/cli-codex.md` / `cli-agy.md` | `until ! ps -p ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | -| `qa-security-scan/03-report-template.md` | `until grep -q ...; do sleep 30; done` | この変更で案内の 1 行。ループの書き換えは #731 | -| `release/references/completion-check.md` | `while :; do ...; sleep 5; done` | この変更で案内の 1 行。ループの書き換えは #731 | - -案内の 1 行は 4 文書とも同じで、ループの直前に置く: 「Claude Code では、このループを -`run_in_background: true` で実行して完了通知を待つ(`development-workflow/references/waiting.md`)」。 -hook と案内の行を同じ変更で配布するため、拒否と文書の順序が食い違わない。 -ループの書き換え(道具の共通化)は #731 で行う。 - -### 決定 4: 連続 Read は「同じ範囲・変わらないファイル・3 回目」で拒否する - -hook は実行の前に呼ばれ、読んだ中身を知らない。そのため「空ファイル」ではなく「前回から -大きさ・更新時刻・inode が変わっていない」で判定する。更新時刻はナノ秒の精度で持ち、inode も比べる。 -同じ秒に同じ大きさの内容で置き換えた(`mv`)ファイルを、変わっていないと取り違えないためである。空ファイルの読み直しはこれに含まれ、書き込みが -進むログの読み直しは含まれない。`offset` と `limit` を鍵に入れるのは、大きなファイルを範囲を -変えて読み進める正当な使い方を止めないためである。 - -3 回目にするのは、2026-08-23 以降の記録の実測による。同じ引数の Read が 3 回以上続いたのは 6 本で、 -うち 5 本が `tasks/*.output` の読み直し(最長 1,168 回)だった。残る 1 本は画像を見直す 3 回である。 -2 回目で止めると、正当な見直しに当たる機会が増える。間に他のツールが挟まったら数え直す形は -採らない。hook は Read の呼び出しにしか登録されず、間のツールを見られない。 - -### 決定 5: hook は Claude Code にだけ登録し、他の 3 ランタイムは規約で守る - -拒否の理由が案内する代わりの手段(`Monitor` / `run_in_background` の通知)は Claude Code にしか -無い。文脈量も Claude Code の `transcript_path` からしか読めない。#827 の実測も Claude Code の -記録だけで、Codex / Kiro / agy の消費は測っていない。3 ランタイムへも登録する形は採らない。 -Kiro は拒否すると代わりの口を持たず、agy は案内を控えへ積む形で、どちらも同じ案内を出せない。 -CLI 側の消費を測った後(#827 の次の手順)に改めて決める。 - -### 決定 6: 文脈量の判定は conductor が工程へ入る起動だけに掛ける - -**conductor が工程へ入る起動は、経路によって違うツールに現れる。** hook は両方を捕まえる。 - -| 経路 | conductor が起動するもの | 捕まえる入力 | 印の鍵 | -| --- | --- | --- | --- | -| 対話 | 工程 Skill(例 `ndf:implementation-plan`)。対話では 3 層へ出さない(`development-workflow/SKILL.md` の「`/goal` の引数として呼ばれたとき」の末尾) | `Skill`。名前が `token-guard-stages.txt` にある(上の「工程 Skill の一覧」の 13 個) | `skill`・`args` | -| 3 層 | 持ち場ごとの supervisor。conductor は `development-workflow` と `issue-plan-strategy` 以外を起動しない(`agent-layers.md` の「3 層の責務」) | `Agent`(旧名 `Task`)。`agent_id` が無く、`description` の先頭語が持ち場の語彙(`agent-layers.md` の「起動の指示」) | `description` | - -3 層の経路の `Skill` だけを見ると、捕まるのは入口の起動だけで、持ち場の切れ目で止まらない。 -先頭語が作業の種類(`調査` など)の Agent は持ち場でないため見ない。 - -supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が途中で -途切れる。supervisor の切れ目は #768 / #773 が測ってから決める。工程でない Skill -(`markdown-writing` / `progress-tracking` など)の起動で止める形も採らない。工程の途中で起動 -されるため、切れ目にならない。`worktree` など切れ目の内側の工程も、同じ理由で一覧から外す。 - -### 決定 7: 文脈量の案内は工程 Skill の起動ごとに 1 度拒否し、次の同じ起動だけを通す - -利用者が「このまま続ける」と決めたときに、環境変数を設定し直さずに続けられるようにする。 -毎回拒否する形は採らない。続けると決めた利用者が、同じ工程をやり直せなくなる。 -会話ごとに 1 度にする形も採らない。1 度通した後は、以後の工程の切れ目で止まらなくなる。 -印に鍵(`skill` と `args`、Agent では `description`)を持つのは、通すのを工程へ入る次の同じ起動に限るためである。 -間のツールで印を失効させる形は採らない。hook は Edit などを見ないため、失効の条件を一貫して判定できない。 -拒否せずに案内だけを足す形(`additionalContext`)も採らない。#827 の実測で、`context-window.md` -に書いた規定は守られていなかった。1 度は止めないと、案内は読み流される。 - -### 決定 8: 引き継ぎの 1 行は `development-workflow` を起動する形にする - -工程 Skill を直接起動する形(`/ndf:implementation-plan #829`)は採らない。工程 Skill はモード・作業ツリー・ -承認の状態を戻す手順を持たず、戻す手順を持つのは `development-workflow` の側だからである。 -`development-workflow` を経由すると固定費に 1 回分の読み込み(約 1 万トークン)が足されるが、 -切る前の会話の文脈(#827 で平均 41 万)に比べて小さい。 - -### 決定 9: 上限の既定は 200,000 にし、`skill-stats` の既定と同じ値にする - -`context-window.md` の「遅くとも 20 万」と、`skill-stats.py` の `DEFAULT_WINDOW_LIMIT` が同じ値を -持つ。hook だけ別の値にすると、測る側と止める側の上限が食い違う。10 万(目安の側)にする形は -採らない。#827 で固定費だけで約 4 万あり、1 工程の途中で止まる回数が増える。 - -### 決定 10: 1 回で足りる待ちは `Monitor` ではなく `run_in_background` の until ループにする - -#829 は「`Monitor` の until で 1 回だけ待つ」を挙げた。一方、Claude Code 2.1.280 の `Monitor` の -説明は、道具を次のように使い分ける。 - -| 待ち方 | 使う道具 | -| --- | --- | -| 通知が 1 回で足りる(終わるのを待つ) | `run_in_background` の until ループ | -| 出来事を 1 つずつ受ける | `Monitor` | -`Monitor` は既定 5 分・最長 30 分で打ち切られ、張り直しが要る。 -`Monitor` を 1 回の待ちの既定にする形は採らない。張り直しのたびに呼び出しが増える。 +決定の記録は [issue-829-830-design-decisions.md](issue-829-830-design-decisions.md) にある。 ## テスト設計 | 受け入れ条件 | 何で確かめるか | | --- | --- | | AC1〜AC4 | 文書の検査(`test_token_guard.py`): `waiting.md` があり、許す待ち方の節が `Monitor` と `run_in_background` を挙げる。`agent-layers.md` の supervisor と worker の規則が `waiting.md` を参照する。`waiting.md` と `agent-layers.md` のコード例に、前景の `while` / `until` と `sleep` を組み合わせた Claude Code 向けの例が無い | -| AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done`・`bash -c 'sleep 30'`・`timeout 590 bash -c "until [ -s f ]; do sleep 5; done"`・`sh -c 'until test -s x; do sleep 1; done'` で deny と理由の欄に `run_in_background` と `waiting.md` | -| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`while read l; do echo "$l"; done < f; sleep 1`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | +| AC5 | 単体: `sleep 30 && tail -5 x.log`・`while ! test -s x; do sleep 5; done`・`until ...; do sleep 1; done`・`bash -c 'sleep 30'`・`timeout 590 bash -c "until [ -s f ]; do sleep 5; done"`・`sh -c 'until test -s x; do sleep 1; done'`・`for i in 1 2; do sleep 10; done`(`for` の本体は秒数だけで見るので 10 秒で拒否)・`while a; do while b; do sleep 1; done; done`(内側の `while` の本体)で deny と理由の欄に `run_in_background` と `waiting.md` | +| AC6 | 単体: `run_in_background: true` の `sleep 30 && tail`・`while read l; do echo "$l"; done < f; sleep 1`・`python3 -m http.server & sleep 2`・`for p in 1 2; do gh api ...; sleep 1; done`・`for i in 1 2; do sleep 3; done`・`echo sleep 30`・`git commit -m "sleep 60"`・`# sleep 30` のコメント行・`echo "while x; do sleep 9; done"`・`cat <<'EOF'`〜`sleep 60`〜`EOF` のヒアドキュメント・`tool_name: Monitor` で出力なし | | AC7 | 単体: 一時ファイルに対し Read を 3 回 → 3 回目で deny。2 回目の後にファイルへ追記 → 数え直し。`offset` を変える → 数え直し。同じ大きさの内容で置き換えた(`mv`)ファイル → 数え直し。同じ session で Read の hook を 2 本並列に起動しても count が 2 進む(更新が失われない) | | AC8 | AC5〜AC7 のテストが `uv run --with pytest pytest plugins/ndf/scripts/tests/test_token_guard.py -q` で通る | -| AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0。ロックを他が持ったまま 2 秒を超えると出力なしで 0。同じ環境変数の下で `guards/` の親が `wf_state_dir` の親と一致する(4 段それぞれ) | +| AC9 | 単体: 壊れた JSON・`jq` を外した `PATH`・書けない `XDG_STATE_HOME` で、出力なしと終了コード 0。ロックを他が持ったまま 1 秒を超えると出力なしで 0。同じ環境変数の下で `guards/` の親が `wf_state_dir` の親と一致する(4 段それぞれ) | | AC10 | 単体: `NDF_SLEEP_GUARD=0` と `NDF_READ_REPEAT_GUARD=0` で、それぞれの拒否だけが消える。閾値: `NDF_SLEEP_MAX_SEC=30` で `sleep 10` は出力なし・`sleep 40` は deny、`NDF_READ_REPEAT_LIMIT=2` で 2 回目に deny、`NDF_CONTEXT_LIMIT=300000` で文脈量 250,000 は出力なし | | AC11 | 実機: サブエージェントの中で `codex exec` を `run_in_background` で起動し、他の作業が無いまま応答を終える。ターンを終えずに次の段へ進んだことを、そのサブエージェントの記録で確かめて #829 に残す | | AC12 | 単体: 文脈量 250,000 の transcript の見本と `tool_input.skill: "ndf:implementation-plan"`、`args: "#829"` で deny と理由の欄に `/ndf:development-workflow #829`。3 層: `tool_name: Agent`、`description: "設計: #829 #830"` で deny と `/ndf:development-workflow #829 #830`。args に番号が無ければ `<課題番号>` のまま(控えが複数あっても推測しない) | diff --git a/issues/issue-829-830-requirements.md b/issues/issue-829-830-requirements.md index dbcb53dd0..d7c6f74be 100644 --- a/issues/issue-829-830-requirements.md +++ b/issues/issue-829-830-requirements.md @@ -1,6 +1,6 @@ # #829 / #830: 待つ間の問い合わせをやめ、conductor の会話を工程の切れ目で切る — 要求と受け入れ条件 -設計は [issue-829-830-design.md](issue-829-830-design.md) にある。この文書は「何を満たすか」だけを扱う。 +設計は [issue-829-830-design.md](issue-829-830-design.md) に、決定の記録は [issue-829-830-design-decisions.md](issue-829-830-design-decisions.md) にある。この文書は「何を満たすか」だけを扱う。 親は #827(トークン消費の実測)。マイルストーンは「17 トークン消費の削減」。 @@ -147,7 +147,7 @@ | 大項目 | 条件 | | --- | --- | -| 性能・拡張性 | hook 1 回の実行は、transcript が 50 MB でも 1 秒以内に終わる | +| 性能・拡張性 | hook 1 回の実行は、transcript が 50 MB でも、競合しないとき 1 秒以内に終わる。ロックを待つときは待ちの上限 1 秒を足した 2 秒以内に終わる。hook 登録の `timeout` は 5 秒のままでよい(2 秒に余裕がある) | | 運用・保守性 | 拒否の理由の欄だけで、エージェントが次に何をすればよいか分かる(代わりの手段を具体的に書く) | | 可用性 | hook の失敗でツールの実行を止めない(AC9 / AC16) | From 07f097455d0545267f4ab4fab0f2aa63fc0289f0 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:08:26 +0000 Subject: [PATCH 11/30] =?UTF-8?q?Add:=20=E5=BE=85=E3=81=A1=E3=81=AE?= =?UTF-8?q?=E5=95=8F=E3=81=84=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A8=E9=95=B7?= =?UTF-8?q?=E3=81=84=20conductor=20=E3=81=AE=E5=B7=A5=E7=A8=8B=E3=81=AE?= =?UTF-8?q?=E8=B5=B7=E5=8B=95=E3=82=92=20hook=20=E3=81=A7=E6=AD=A2?= =?UTF-8?q?=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - token-guard.sh(PreToolUse の Bash / Read / Skill / Agent)を新設し、Claude Code にだけ登録する - 前景の sleep の待ち(while / until の本体、または 5 秒を超える秒数)を止める - 変わらないファイルの同じ範囲を 3 回続けて読む Read を止める - 文脈が 200,000 を超えた conductor が工程へ入る起動を 1 度止め、新しい会話で打つ 1 行を示す - 待ち方の規約 waiting.md を新設し、agent-layers.md と前景の待ちのループを持つ 4 文書から指す - context-window.md の「実測ではない」を #827 の実測へ置き換え、hook と新しい会話で戻す手順の節を足す - development-workflow/SKILL.md に、切れ目で conductor が引き継ぎの 1 行を出す規約を足す - README に hook と 4 ランタイムの扱いの表を足す - 実装計画 issues/issue-829-830-implementation-plan.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- issues/issue-829-830-implementation-plan.md | 132 +++++ plugins/ndf/README.md | 21 + plugins/ndf/hooks/claude.json | 13 + .../ndf/scripts/lib/token-guard-stages.txt | 16 + plugins/ndf/scripts/lib/token_guard_sleep.py | 117 ++++ plugins/ndf/scripts/tests/test_token_guard.py | 547 ++++++++++++++++++ plugins/ndf/scripts/token-guard.sh | 174 ++++++ .../ndf/skills/development-workflow/SKILL.md | 8 + .../references/agent-layers.md | 2 + .../references/context-window.md | 68 ++- .../references/waiting.md | 83 +++ .../skills/external-ai/references/cli-agy.md | 2 + .../external-ai/references/cli-codex.md | 2 + .../qa-security-scan/03-report-template.md | 2 + .../release/references/completion-check.md | 2 + 15 files changed, 1185 insertions(+), 4 deletions(-) create mode 100644 issues/issue-829-830-implementation-plan.md create mode 100644 plugins/ndf/scripts/lib/token-guard-stages.txt create mode 100644 plugins/ndf/scripts/lib/token_guard_sleep.py create mode 100644 plugins/ndf/scripts/tests/test_token_guard.py create mode 100755 plugins/ndf/scripts/token-guard.sh create mode 100644 plugins/ndf/skills/development-workflow/references/waiting.md diff --git a/issues/issue-829-830-implementation-plan.md b/issues/issue-829-830-implementation-plan.md new file mode 100644 index 000000000..17fe97982 --- /dev/null +++ b/issues/issue-829-830-implementation-plan.md @@ -0,0 +1,132 @@ +# #829 / #830: 待つ間の問い合わせをやめ、conductor の会話を工程の切れ目で切る — 実装計画 + +## 関連リンク + +- 要求と受け入れ条件: [issue-829-830-requirements.md](issue-829-830-requirements.md)(AC1〜AC26) +- 設計: [issue-829-830-design.md](issue-829-830-design.md) +- 決定の記録: [issue-829-830-design-decisions.md](issue-829-830-design-decisions.md)(決定 1〜10) +- 設計 Pull Request: https://github.com/devbasex/ai-plugins/pull/843 (マージ済み) +- 課題: #829 / #830(親は #827、マイルストーン 26「17 トークン消費の削減」) + +## モード + +standard(hook の新設と複数の Skill 文書の変更。本番の系へ届く操作を含まず、配布は別の工程)。 + +## 目的と非目的 + +達成したい状態: + +- Claude Code で、前景の `sleep` の待ちと変わらないファイルの読み直しを hook が止め、代わりの待ち方を示す +- 文脈が上限を超えた conductor が工程へ入る起動を 1 度止め、新しい会話で打つ 1 行を示す +- 待ち方の規約と、新しい会話で状態を戻す手順が 1 か所ずつにある + +やらないこと: + +- **AC25 / AC26(効果の数値)はこの持ち場では確かめない。** 本番へ配布した後に `release-verification` で #827 の `measure.py` / `poll.py` / `extra.py` を回して確かめる(要求の前提 5) +- #731 / #656 / #345 / #828 / #680 の範囲、supervisor / worker の文脈量の上限(#768 / #773) +- Codex / Kiro / agy の hook の登録(決定 5) + +## 前提 + +- 前提 1: 設計の「未確認」5 件のうち、hook の入力(`agent_id` の有無・`transcript_path` の指す先・記録の書き込みの時点)は Task 0 で実測して決める。実測できなければ設計の既定(`agent_id` が無く `/subagents/` を含まなければ conductor とみなす)で進める +- 前提 2: 「Codex / Kiro の起動の書き方」は各ランタイムの README の記載に合わせる(Task 6) +- 前提 3: 「通知の届き方」(AC11)と「背景の Bash の上限」は Task 7 の実機確認で決める + +## 受け入れ条件 + +要求の AC1〜AC24 をこの Pull Request で満たす。AC11 と AC20 は実機の確認で、手順と結果を各 issue に残す。 +AC25 / AC26 は配布後(上の「やらないこと」)。条件ごとの検証手段は設計の「テスト設計」の表に従う。 + +## 代替案と採否 + +設計の決定 1〜10 のとおり。実装で新たに選ぶものは次の 1 つ。 + +| 案 | 内容 | 採否 | 理由 | +| --- | --- | --- | --- | +| A | `sleep` の判定(引用・コメント・ヒアドキュメントの除去、`-c` / `eval` の中身の取り出し、ループの本体の対応)を hook の中の `python3` で書く | 採用 | bash の正規表現では入れ子の `do` / `done` の対応と引用の除去を読める形で書けない。`python3` は既存のスクリプト(`progress-record.sh` など)が既に使っている | +| B | すべて bash と `jq` で書く | 不採用 | 上記。`python3` が無いときは判定を通す(AC9 の扱い)ため可用性は落ちない | + +## 修正対象 + +- 新設: `plugins/ndf/scripts/token-guard.sh`、`plugins/ndf/scripts/lib/token_guard_sleep.py`(sleep の判定。案 A)、`plugins/ndf/scripts/lib/token-guard-stages.txt`、`plugins/ndf/scripts/tests/test_token_guard.py`、`plugins/ndf/skills/development-workflow/references/waiting.md` +- 変更: `plugins/ndf/hooks/claude.json`、`development-workflow/SKILL.md`、`development-workflow/references/agent-layers.md`、`development-workflow/references/context-window.md`、`external-ai/references/cli-codex.md`、`external-ai/references/cli-agy.md`、`qa-security-scan/03-report-template.md`、`release/references/completion-check.md`、`plugins/ndf/README.md` + +## タスク分解 + +### Task 0: hook の入力を実測する(未確認 1・2) + +- **変更内容:** 入力を書き出すだけの hook を `claude -p --settings` で一時的に登録し、本体とサブエージェントの PreToolUse の入力(`agent_id`・`transcript_path`)と、その時点で記録に呼び出しの assistant 行が書かれているかを見る。利用者の設定は書き換えない +- **満たす受け入れ条件:** AC13 の判定方法の根拠 +- **進め方:** 調査(テスト駆動の対象外) +- **結果(2026-09-23、Claude Code 2.1.280、`claude -p --settings` に入力を書き出す hook を登録):** + - サブエージェントの中の PreToolUse の入力には `agent_id` と `agent_type` が付く。本体の入力には付かない + - サブエージェントの `transcript_path` は**親の記録を指す**(`/subagents/` を含まない)。区別は `agent_id` で行う + - PreToolUse の時点で、その呼び出しを出した assistant 行はまだ記録に書かれていない。hook は 1 つ以上前の呼び出しの文脈量を読む(設計の既定どおり) + +### Task 1: sleep の判定 + +- **対象ファイル:** `token-guard.sh`、`test_token_guard.py` +- **変更内容:** `Bash` の入力で、背景でない・`-c` / `eval` の中身も含め・コメントと引用とヒアドキュメントを除いた残りで、コマンドの位置の `sleep <数>` がループの本体にあるか上限を超えれば拒否する +- **満たす受け入れ条件:** AC5 / AC6 / AC8 / AC9 / AC10(sleep の分) +- **進め方:** 設計の AC5 / AC6 の例を失敗するテストとして書く → 最小実装 → 整理 + +### Task 2: 連続 Read の判定 + +- **対象ファイル:** 同上 +- **変更内容:** `guards/` の解決(`wf_state_dir` と同じ順)、session ごとのロック、`read-.json` の控えの置き換え、7 日より古い控えの削除 +- **満たす受け入れ条件:** AC7 / AC8 / AC9 / AC10(Read の分) +- **進め方:** テスト先行 + +### Task 3: 文脈量の判定と工程 Skill の一覧 + +- **対象ファイル:** `token-guard.sh`、`token-guard-stages.txt`、`test_token_guard.py` +- **変更内容:** `Skill`(一覧にある工程 Skill)と `Agent` / `Task`(先頭語が持ち場の語彙)で、conductor の文脈量が上限を超えれば拒否し、印で次の同じ起動を 1 度通す +- **満たす受け入れ条件:** AC12〜AC17、AC22(既定値) +- **進め方:** テスト先行 + +### Task 4: hook の登録 + +- **対象ファイル:** `hooks/claude.json` +- **変更内容:** PreToolUse に matcher `Bash|Read|Skill|Agent|Task` で `token-guard.sh` を足す。既存の `worktree-guard.sh` の登録と順序は変えない +- **満たす受け入れ条件:** AC5 / AC7 / AC12 の実行経路、AC24 +- **進め方:** 登録の形を確かめるテスト → 変更 → `claude plugin validate .` + +### Task 5: 待ち方の規約 + +- **対象ファイル:** `waiting.md`、`agent-layers.md`、external-ai の 2 文書、`qa-security-scan/03-report-template.md`、`release/references/completion-check.md` +- **満たす受け入れ条件:** AC1〜AC4 +- **進め方:** 文書の検査を先に書く → 文書を書く + +### Task 6: 会話を切る規約と README + +- **対象ファイル:** `context-window.md`、`development-workflow/SKILL.md`、`plugins/ndf/README.md` +- **満たす受け入れ条件:** AC18 / AC19 / AC21 / AC22 / AC23 +- **進め方:** 文書の検査を先に書く → 文書を書く + +### Task 7: 実機の確認 + +- **変更内容:** AC11(サブエージェントが背景の処理を残して応答を終えたとき、完了通知で再開されるか)と AC20(1 行だけで新しい会話から戻せるか)を実機で確かめ、手順と結果を #829 / #830 に残す +- **進め方:** 実機(テスト駆動の対象外) + +## 影響範囲 + +- Claude Code の全層の Bash / Read / Skill / Agent の起動の前に hook が 1 本増える +- Codex / Kiro / agy の配布物の hook は変わらない(AC24) + +## リスクと対処 + +| リスク | 対処 | +| --- | --- | +| sleep の判定の誤検知で通常の Bash が止まる | タスクごとにテストを通す。通す例(AC6)を拒否の例と同数以上そろえ、環境変数で種類ごとに止められる(AC10) | +| hook の失敗でツールが止まる | 判定の失敗は常に 0 で通す(AC9)。登録に `continueOnError: true` | +| 実装が触る対象の構造 | 新設のスクリプトが中心で、既存の構造に手を入れない。実装の後の構造改善で足りる | + +## 切り戻し手順 + +- `hooks/claude.json` の登録を 1 つ外せば hook は動かなくなる。利用者は環境変数(`NDF_SLEEP_GUARD=0` など)で種類ごとに止められる。データの移行は無い + +## 完了の定義 + +- [ ] AC1〜AC24 を満たし、条件ごとに検証手段と結果が対応している(AC11 / AC20 は issue に記録) +- [ ] `uv run --with pytest pytest scripts/tests plugins/ndf -q`、`python3 scripts/check-skill-frontmatter.py`、`claude plugin validate .` が終了コード 0 +- [ ] AC25 / AC26 は配布後の `release-verification` へ引き継ぐことを Pull Request の本文に書く diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index e13d1e946..689102404 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -222,6 +222,27 @@ bash <プラグインのパス>/scripts/worktree-setup.sh init 手順は `/ndf:worktree` にあります。 +### 待ちの問い合わせと長い会話を止める(Claude Code だけ) + +`scripts/token-guard.sh` が PreToolUse の `Bash` / `Read` / `Skill` / `Agent` で動き、3 つを +止めます。止めたときは、代わりの手段を理由の欄に出します。 + +| 止めるもの | 止め方 | 上限 | +| --- | --- | --- | +| 前景の `sleep` の待ち(`while` / `until` のループの本体、または上限を超える秒数) | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | +| 変わらないファイルの同じ範囲を続けて読む Read | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | +| 文脈が上限を超えた conductor が工程へ入る起動(1 度だけ止め、新しい会話で打つ 1 行を示す) | `NDF_CONTEXT_GUARD=0` | `NDF_CONTEXT_LIMIT`(既定 200000) | + +| ランタイム | 待ち方 | 会話を切る | +| --- | --- | --- | +| Claude Code | hook + 規約 | hook + 引き継ぎの 1 行 | +| Codex | 規約だけ | 引き継ぎの 1 行だけ | +| Kiro CLI | 規約だけ | 引き継ぎの 1 行だけ | +| agy | 規約だけ | 引き継ぎの 1 行だけ | + +規約は `skills/development-workflow/references/waiting.md`(待ち方)と +`skills/development-workflow/references/context-window.md`(会話を切る)にあります。 + ### その他 Claude Code の SessionStart hook(`hooks/claude.json`)は上記に加えて次を行います。 diff --git a/plugins/ndf/hooks/claude.json b/plugins/ndf/hooks/claude.json index f526d9e84..eb379eb4b 100644 --- a/plugins/ndf/hooks/claude.json +++ b/plugins/ndf/hooks/claude.json @@ -13,6 +13,19 @@ "suppressOutput": false } ] + }, + { + "matcher": "Bash|Read|Skill|Agent|Task", + "hooks": [ + { + "type": "command", + "command": "bash ${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/token-guard.sh", + "description": "NDF: stop polling waits and ask a long conductor to continue in a new session", + "timeout": 5, + "continueOnError": true, + "suppressOutput": false + } + ] } ], "SessionStart": [ diff --git a/plugins/ndf/scripts/lib/token-guard-stages.txt b/plugins/ndf/scripts/lib/token-guard-stages.txt new file mode 100644 index 000000000..716a51231 --- /dev/null +++ b/plugins/ndf/scripts/lib/token-guard-stages.txt @@ -0,0 +1,16 @@ +# 文脈量の hook(token-guard.sh)が見る工程 Skill の一覧(#830)。1 行 1 名。 +# 正は issues/issue-829-830-design.md の「工程 Skill の一覧」(context-window.md の 4 つの切れ目の直後の工程と入口)。 +# worktree など切れ目の内側の工程は載せない(決定 6)。 +implementation-plan +document-drafting +cross-refactoring +cross-review +pr-review +quality-gates +plan-to-spec +merged +layout-review +release-verification +retrospective +development-workflow +issue-plan-strategy diff --git a/plugins/ndf/scripts/lib/token_guard_sleep.py b/plugins/ndf/scripts/lib/token_guard_sleep.py new file mode 100644 index 000000000..23bb07e39 --- /dev/null +++ b/plugins/ndf/scripts/lib/token_guard_sleep.py @@ -0,0 +1,117 @@ +"""前景の `sleep` で待つ Bash を見分ける(#829)。`token-guard.sh` から呼ぶ。 + +標準入力にコマンドの文字列を受け、第 1 引数に秒数の上限を受ける。拒否するなら終了コード 1、 +通すなら 0 で終わる。**読めないコマンドは通す**(hook の失敗でツールを止めない)。 + +拒否するのは、コマンドの位置にある `sleep <数>` が次のどちらかに当たるときだけである。 + +- `while` / `until` のループの本体(`do` と対応する `done` の間)にある +- 秒数が上限を超える + +コメント・引用の中・ヒアドキュメントの本文は見ない。`bash -c` / `sh -c` / `zsh -c` / `eval` の +実行される引数は、取り出して同じ規則で見る。 +""" +from __future__ import annotations + +import re +import shlex +import sys + +SHELLS = {"bash", "sh", "zsh", "dash"} +# コマンドの位置を作る語。この後ろの語はコマンドとして読む +OPENERS = {"do", "then", "else", "elif", "if", "while", "until", "{", "!", "time"} +UNIT = {"": 1, "s": 1, "m": 60, "h": 3600, "d": 86400} +HEREDOC = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2") + + +def strip_heredocs(text: str) -> str: + """ヒアドキュメントの本文を取り除く。開始の行は残す。""" + out, pending = [], [] + for line in text.split("\n"): + if pending: + dash, word = pending[0] + if (line.lstrip("\t") if dash else line) == word: + pending.pop(0) + continue + out.append(line) + for m in HEREDOC.finditer(line.replace("<<<", " ")): + pending.append((m.group(1) == "-", m.group(3))) + return "\n".join(out) + + +def tokens(text: str) -> list[str]: + lex = shlex.shlex(text, posix=True, punctuation_chars=";&|()\n") + lex.whitespace = " \t\r" + lex.commenters = "#" + lex.wordchars += "$:@%+,[]{}!^=-/.~*?" + return list(lex) + + +def seconds(word: str) -> float | None: + m = re.fullmatch(r"(\d+(?:\.\d+)?)([smhd]?)", word) + return float(m.group(1)) * UNIT[m.group(2)] if m else None + + +def is_separator(tok: str) -> bool: + return bool(tok) and all(c in ";&|()\n" for c in tok) + + +def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) -> bool: + if depth > 5: + return False + toks = tokens(strip_heredocs(text)) + # 各要素は "cond"(while/until の条件)/ "body"(while/until の本体)/ "for" / "forbody" + stack: list[str] = [] + cmd_pos = True + i = 0 + while i < len(toks): + tok = toks[i] + looping = in_loop or "body" in stack + if is_separator(tok): + cmd_pos = True + i += 1 + continue + if cmd_pos: + if tok in ("while", "until"): + stack.append("cond") + elif tok in ("for", "select"): + stack.append("for") + elif tok == "do" and stack: + stack[-1] = "body" if stack[-1] == "cond" else "forbody" + elif tok == "done" and stack: + stack.pop() + elif tok == "sleep" and i + 1 < len(toks): + sec = seconds(toks[i + 1]) + if sec is not None and (looping or sec > limit): + return True + cmd_pos = tok in OPENERS + # 実行される引数を取り出して同じ規則で見る + if tok in SHELLS: + j = i + 1 + while j < len(toks) and toks[j].startswith("-") and not is_separator(toks[j]): + if "c" in toks[j].lstrip("-") and not toks[j].startswith("--"): + if j + 1 < len(toks) and should_deny(toks[j + 1], limit, looping, depth + 1): + return True + break + j += 1 + elif tok == "eval": + j, words = i + 1, [] + while j < len(toks) and not is_separator(toks[j]): + words.append(toks[j]) + j += 1 + if should_deny(" ".join(words), limit, looping, depth + 1): + return True + i += 1 + return False + + +def main() -> int: + try: + limit = float(sys.argv[1]) if len(sys.argv) > 1 else 5.0 + return 1 if should_deny(sys.stdin.read(), limit) else 0 + except Exception: # 読めないコマンドは通す + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py new file mode 100644 index 000000000..3443b0ba4 --- /dev/null +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -0,0 +1,547 @@ +"""待ちの hook と文脈量の hook(#829 / #830)。 + +`token-guard.sh` は Claude Code の PreToolUse で動き、3 つを判定する。 + +- 前景の `sleep` で待つ Bash(ループの本体にあるか、秒数が上限を超える) +- 変わらないファイルの同じ範囲を続けて読み直す Read +- 文脈が上限を超えた conductor が工程へ入る起動(工程 Skill・持ち場の supervisor) + +**判定が失敗してもツールを止めない。** 拒否は `permissionDecision: deny` で返し、終了コードは +常に 0 にする。 +""" +from __future__ import annotations + +import json +import os +import pathlib +import shutil +import subprocess +import threading + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "token-guard.sh" +STAGES = ROOT / "scripts" / "lib" / "token-guard-stages.txt" +WF_DOCS = ROOT / "skills" / "development-workflow" +WAITING = WF_DOCS / "references" / "waiting.md" +LAYERS = WF_DOCS / "references" / "agent-layers.md" +CONTEXT = WF_DOCS / "references" / "context-window.md" +WF_SKILL = WF_DOCS / "SKILL.md" +README = ROOT / "README.md" +HOOKS = ROOT / "hooks" / "claude.json" +MANIFESTS = ROOT / "manifests" + +STAGE_SKILLS = { + "implementation-plan", "document-drafting", + "cross-refactoring", "cross-review", "pr-review", "quality-gates", + "plan-to-spec", "merged", + "layout-review", "release-verification", "retrospective", + "development-workflow", "issue-plan-strategy", +} + + +@pytest.fixture() +def state(tmp_path, monkeypatch): + """状態の置き場所をテストごとに分ける。""" + base = tmp_path / "plugin-data" + return base + + +def run(payload, state_dir, env=None, raw=None): + e = {k: v for k, v in os.environ.items() if not k.startswith("NDF_")} + e.pop("CLAUDE_PLUGIN_DATA", None) + e["CLAUDE_PLUGIN_DATA"] = str(state_dir) + if env: + e.update(env) + data = raw if raw is not None else json.dumps(payload) + return subprocess.run(["bash", str(SCRIPT)], input=data, capture_output=True, + text=True, env=e, timeout=20) + + +def denied(proc): + assert proc.returncode == 0, proc.stderr + if not proc.stdout.strip(): + return None + out = json.loads(proc.stdout) + spec = out["hookSpecificOutput"] + assert spec["hookEventName"] == "PreToolUse" + assert spec["permissionDecision"] == "deny" + return spec["permissionDecisionReason"] + + +def bash(cmd, **extra): + ti = {"command": cmd} + ti.update(extra) + return {"tool_name": "Bash", "tool_input": ti, "session_id": "s1"} + + +# ---------------------------------------------------------------- sleep(AC5 / AC6) + +DENY_SLEEP = [ + "sleep 30 && tail -5 x.log", + "while ! test -s x; do sleep 5; done", + "until [ -s f ]; do sleep 1; done", + "bash -c 'sleep 30'", + 'timeout 590 bash -c "until [ -s f ]; do sleep 5; done"', + "sh -c 'until test -s x; do sleep 1; done'", + "for i in 1 2; do sleep 10; done", + "while a; do while b; do sleep 1; done; done", + "while a; do\n for i in 1 2; do sleep 1; done\ndone", + 'eval "sleep 30"', + "echo start\nsleep 60\necho end", + "sleep 1m", +] + +ALLOW_SLEEP = [ + "while read l; do echo \"$l\"; done < f; sleep 1", + "python3 -m http.server & sleep 2", + "for p in 1 2; do gh api x; sleep 1; done", + "for i in 1 2; do sleep 3; done", + "echo sleep 30", + 'git commit -m "sleep 60"', + "# sleep 30", + "ls # sleep 30", + 'echo "while x; do sleep 9; done"', + "cat <<'EOF'\nsleep 60\nwhile true; do sleep 1; done\nEOF", + "cat <<-EOF > f\n\tsleep 60\n\tEOF\necho ok", + "sleep 5", + "sleep $X", + "ls -la", + "while read l; do echo $l; done < f", +] + + +@pytest.mark.parametrize("cmd", DENY_SLEEP) +def test_sleep_denied(cmd, state): + reason = denied(run(bash(cmd), state)) + assert reason, cmd + assert "run_in_background" in reason + assert "waiting.md" in reason + assert "Monitor" in reason + + +@pytest.mark.parametrize("cmd", ALLOW_SLEEP) +def test_sleep_allowed(cmd, state): + assert denied(run(bash(cmd), state)) is None, cmd + + +def test_background_bash_is_allowed(state): + p = bash("sleep 30 && tail x", run_in_background=True) + assert denied(run(p, state)) is None + + +def test_monitor_is_not_judged(state): + p = {"tool_name": "Monitor", "tool_input": {"command": "while true; do sleep 1; done"}} + assert denied(run(p, state)) is None + + +def test_sleep_guard_env(state): + assert denied(run(bash("sleep 30"), state, {"NDF_SLEEP_GUARD": "0"})) is None + assert denied(run(bash("sleep 10"), state, {"NDF_SLEEP_MAX_SEC": "30"})) is None + assert denied(run(bash("sleep 40"), state, {"NDF_SLEEP_MAX_SEC": "30"})) + + +# ---------------------------------------------------------------- 連続 Read(AC7) + +def read(path, session="s1", **extra): + ti = {"file_path": str(path)} + ti.update(extra) + return {"tool_name": "Read", "tool_input": ti, "session_id": session} + + +def test_repeat_read_denied_on_third(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("") + assert denied(run(read(f), state)) is None + assert denied(run(read(f), state)) is None + reason = denied(run(read(f), state)) + assert reason and "run_in_background" in reason and "waiting.md" in reason + # 拒否の後も同じなら拒否が続く + assert denied(run(read(f), state)) + + +def test_repeat_read_resets_when_file_changes(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("a") + run(read(f), state) + run(read(f), state) + f.write_text("ab") + assert denied(run(read(f), state)) is None + assert denied(run(read(f), state)) is None + assert denied(run(read(f), state)) + + +def test_repeat_read_resets_on_other_range(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("a\nb\n") + run(read(f), state) + run(read(f), state) + assert denied(run(read(f, offset=2), state)) is None + assert denied(run(read(f), state)) is None + + +def test_repeat_read_resets_on_replaced_file(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("aaaa") + run(read(f), state) + run(read(f), state) + g = tmp_path / "new.txt" + g.write_text("bbbb") + os.utime(g, ns=(f.stat().st_atime_ns, f.stat().st_mtime_ns)) + os.replace(g, f) + assert denied(run(read(f), state)) is None + + +def test_repeat_read_is_per_session(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("") + run(read(f), state) + run(read(f), state) + assert denied(run(read(f, session="s2"), state)) is None + + +def test_repeat_read_env(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("") + env = {"NDF_READ_REPEAT_LIMIT": "2"} + assert denied(run(read(f), state, env)) is None + assert denied(run(read(f), state, env)) + g = tmp_path / "g.txt" + g.write_text("") + off = {"NDF_READ_REPEAT_GUARD": "0"} + for _ in range(4): + assert denied(run(read(g), state, off)) is None + + +def test_parallel_reads_do_not_lose_updates(tmp_path, state): + f = tmp_path / "out.txt" + f.write_text("") + env = {"NDF_READ_REPEAT_LIMIT": "10"} + run(read(f), state, env) + threads = [threading.Thread(target=run, args=(read(f), state, env)) for _ in range(2)] + for t in threads: + t.start() + for t in threads: + t.join() + saved = json.loads((state / "guards" / "read-s1.json").read_text()) + assert saved["count"] == 3 + + +# ---------------------------------------------------------------- 可用性(AC9) + +def test_broken_json_passes(state): + p = run(None, state, raw="{not json") + assert p.returncode == 0 and p.stdout == "" + + +def test_without_jq_passes(tmp_path, state): + bindir = tmp_path / "bin" + bindir.mkdir() + for tool in ("bash", "cat", "tail", "stat", "mkdir", "date", "mv", "rm", "find", "python3"): + src = shutil.which(tool) + if src: + (bindir / tool).symlink_to(src) + p = run(bash("sleep 30"), state, {"PATH": str(bindir)}) + assert p.returncode == 0 and p.stdout == "" + + +def test_unwritable_state_skips_read_but_keeps_sleep(tmp_path): + ro = tmp_path / "ro" + ro.mkdir() + ro.chmod(0o500) + try: + env = {"CLAUDE_PLUGIN_DATA": "", "XDG_STATE_HOME": str(ro), "HOME": str(ro), + "TMPDIR": str(ro)} + f = tmp_path / "x.txt" + f.write_text("") + for _ in range(4): + assert denied(run(read(f), ro / "none", env)) is None + assert denied(run(bash("sleep 30"), ro / "none", env)) + finally: + ro.chmod(0o700) + + +def test_lock_held_passes(tmp_path, state): + f = tmp_path / "x.txt" + f.write_text("") + guards = state / "guards" + guards.mkdir(parents=True) + lock = guards / "s1.lock" + lock.mkdir() + (lock / "held").write_text("") + (lock / "pid").write_text(str(os.getpid())) + (lock / "token").write_text("t") + for _ in range(4): + assert denied(run(read(f), state)) is None + + +@pytest.mark.parametrize("env,expect", [ + ({"CLAUDE_PLUGIN_DATA": "{d}/pd"}, "{d}/pd/guards"), + ({"CLAUDE_PLUGIN_DATA": "", "XDG_STATE_HOME": "{d}/xdg"}, "{d}/xdg/ndf/guards"), + ({"CLAUDE_PLUGIN_DATA": "", "XDG_STATE_HOME": "", "HOME": "{d}/home"}, + "{d}/home/.local/state/ndf/guards"), + ({"CLAUDE_PLUGIN_DATA": "", "XDG_STATE_HOME": "", "HOME": "", "TMPDIR": "{d}/tmp"}, + "{d}/tmp/ndf-guards"), +]) +def test_guards_dir_follows_wf_state_dir(tmp_path, env, expect): + env = {k: v.format(d=tmp_path) for k, v in env.items()} + (tmp_path / "tmp").mkdir() + f = tmp_path / "x.txt" + f.write_text("") + e = {k: v for k, v in os.environ.items() if not k.startswith("NDF_")} + e.update(env) + subprocess.run(["bash", str(SCRIPT)], input=json.dumps(read(f)), text=True, + capture_output=True, env=e, check=True) + assert (pathlib.Path(expect.format(d=tmp_path)) / "read-s1.json").is_file() + wf = subprocess.run( + ["bash", "-c", f". '{WF_DOCS}/scripts/lib/workflow-common.sh'; wf_state_dir"], + text=True, capture_output=True, env=e).stdout.strip() + assert pathlib.Path(wf).parent == pathlib.Path(expect.format(d=tmp_path)).parent + + +# ---------------------------------------------------------------- 文脈量(AC12〜AC16) + +def transcript(tmp_path, total, name="t.jsonl", usage=True): + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + lines = [{"type": "user", "message": {"content": "x"}}] + msg = {"role": "assistant", "content": []} + if usage: + msg["usage"] = {"input_tokens": 10, "cache_read_input_tokens": total - 110, + "cache_creation_input_tokens": 100, "output_tokens": 5} + lines.append({"type": "assistant", "message": msg}) + lines.append({"type": "attachment"}) + path.write_text("\n".join(json.dumps(x) for x in lines) + "\n") + return path + + +def skill(tp, name="ndf:implementation-plan", args="#829", session="s1", **extra): + p = {"tool_name": "Skill", "tool_input": {"skill": name, "args": args}, + "session_id": session, "transcript_path": str(tp)} + p.update(extra) + return p + + +def agent(tp, desc="設計: #829 #830", session="s1", tool="Agent", **extra): + p = {"tool_name": tool, "tool_input": {"description": desc, "prompt": "x"}, + "session_id": session, "transcript_path": str(tp)} + p.update(extra) + return p + + +def test_context_over_limit_denies_stage_skill(tmp_path, state): + tp = transcript(tmp_path, 250_000) + reason = denied(run(skill(tp), state)) + assert reason and "/ndf:development-workflow #829" in reason + assert "context-window.md" in reason + assert "250000" in reason or "250,000" in reason + + +def test_context_over_limit_denies_supervisor(tmp_path, state): + tp = transcript(tmp_path, 250_000) + reason = denied(run(agent(tp), state)) + assert reason and "/ndf:development-workflow #829 #830" in reason + assert "/goal" in reason + assert denied(run(agent(tp, desc="実装: #1", session="s9", tool="Task"), state)) + + +def test_context_issue_placeholder(tmp_path, state): + tp = transcript(tmp_path, 250_000) + reason = denied(run(skill(tp, args=""), state)) + assert "/ndf:development-workflow <課題番号>" in reason + + +def test_context_within_limit_passes(tmp_path, state): + tp = transcript(tmp_path, 150_000) + assert denied(run(skill(tp), state)) is None + + +def test_context_subagent_passes(tmp_path, state): + tp = transcript(tmp_path, 250_000) + assert denied(run(skill(tp, agent_id="a1"), state)) is None + assert denied(run(agent(tp, agent_id="a1"), state)) is None + sub = transcript(tmp_path, 250_000, name="sess/subagents/agent-1.jsonl") + assert denied(run(skill(sub), state)) is None + assert denied(run(agent(sub), state)) is None + + +def test_context_non_stage_passes(tmp_path, state): + tp = transcript(tmp_path, 250_000) + for name in ("ndf:markdown-writing", "ndf:worktree", "ndf:progress-tracking", + "ndf:out-of-scope"): + assert denied(run(skill(tp, name=name), state)) is None, name + assert denied(run(agent(tp, desc="調査: 既存の規約"), state)) is None + assert denied(run(agent(tp, desc="何かの説明"), state)) is None + + +def test_context_once_then_pass_same_key(tmp_path, state): + tp = transcript(tmp_path, 250_000) + f = tmp_path / "x.txt" + f.write_text("") + assert denied(run(skill(tp), state)) + run(bash("ls"), state) + run(read(f), state) + run(skill(tp, name="ndf:markdown-writing"), state) + assert denied(run(skill(tp), state)) is None + assert denied(run(skill(tp, name="ndf:merged"), state)) + + +def test_context_other_key_replaces_mark(tmp_path, state): + tp = transcript(tmp_path, 250_000) + assert denied(run(skill(tp), state)) + assert denied(run(skill(tp, args="#830"), state)) + assert denied(run(skill(tp, args="#830"), state)) is None + + +def test_context_agent_once_then_pass(tmp_path, state): + tp = transcript(tmp_path, 250_000) + assert denied(run(agent(tp, desc="設計: #829"), state)) + assert denied(run(agent(tp, desc="設計: #829"), state)) is None + assert denied(run(agent(tp, desc="実装: #829"), state)) + + +def test_context_guard_env(tmp_path, state): + tp = transcript(tmp_path, 250_000) + assert denied(run(skill(tp), state, {"NDF_CONTEXT_GUARD": "0"})) is None + assert denied(run(skill(tp), state, {"NDF_CONTEXT_LIMIT": "300000"})) is None + + +def test_context_unreadable_passes(tmp_path, state): + assert denied(run(skill(tmp_path / "missing.jsonl"), state)) is None + tp = transcript(tmp_path, 250_000, usage=False) + assert denied(run(skill(tp), state)) is None + p = skill(tp) + del p["transcript_path"] + assert denied(run(p, state)) is None + + +def test_context_parallel_second_call_passes_once(tmp_path, state): + tp = transcript(tmp_path, 250_000) + assert denied(run(skill(tp), state)) + results = [] + + def go(): + results.append(denied(run(skill(tp), state))) + + threads = [threading.Thread(target=go) for _ in range(2)] + for t in threads: + t.start() + for t in threads: + t.join() + assert sum(1 for r in results if r is None) == 1 + + +def test_context_large_transcript_is_fast(tmp_path, state): + import time + tp = tmp_path / "big.jsonl" + line = json.dumps({"type": "user", "message": {"content": "y" * 1000}}) + "\n" + with tp.open("w") as fh: + for _ in range(50_000): + fh.write(line) + transcript_tail = transcript(tmp_path, 250_000, name="tail.jsonl").read_text() + with tp.open("a") as fh: + fh.write(transcript_tail) + start = time.monotonic() + assert denied(run(skill(tp), state)) + assert time.monotonic() - start < 1.5 + + +# ---------------------------------------------------------------- 一覧と登録(AC14 / AC24) + +def test_stage_list_matches_design(): + names = {l.strip() for l in STAGES.read_text().splitlines() + if l.strip() and not l.startswith("#")} + assert names == STAGE_SKILLS + listed = set() + for m in MANIFESTS.glob("*-skills.txt"): + listed |= {l.strip() for l in m.read_text().splitlines() if l.strip()} + assert names <= listed + + +def test_hook_registered_for_claude(): + hooks = json.loads(HOOKS.read_text())["hooks"]["PreToolUse"] + ours = [h for h in hooks if any("token-guard.sh" in x["command"] for x in h["hooks"])] + assert len(ours) == 1 + assert set(ours[0]["matcher"].split("|")) == {"Bash", "Read", "Skill", "Agent", "Task"} + entry = ours[0]["hooks"][0] + assert entry["continueOnError"] is True and entry["timeout"] == 5 + assert "worktree-guard.sh" in hooks[0]["hooks"][0]["command"] + + +def test_hook_not_registered_for_other_runtimes(): + for f in (ROOT / "hooks").glob("*.json"): + if f.name != "claude.json": + assert "token-guard" not in f.read_text(), f + + +# ---------------------------------------------------------------- 文書(AC1〜AC4 / AC18〜AC23) + +def test_waiting_doc_lists_methods(): + text = WAITING.read_text() + for word in ("run_in_background", "Monitor", "token-guard.sh", "NDF_SLEEP_GUARD", + "NDF_READ_REPEAT_GUARD", "tasks/*.output"): + assert word in text, word + + +def _code_blocks(text): + import re + return re.findall(r"```[a-z]*\n(.*?)```", text, re.S) + + +def test_no_foreground_sleep_loop_examples(): + import re + for doc in (WAITING, LAYERS): + for block in _code_blocks(doc.read_text()): + if "run_in_background" in block: + continue + assert not re.search(r"\b(while|until)\b.*\bdo\b[^`]*\bsleep\b", block, re.S), doc + + +def test_agent_layers_refers_waiting(): + text = LAYERS.read_text() + sup = text.split("supervisor が守る規則:")[1].split("\n\n")[1] + wrk = text.split("worker が守る規則:")[1].split("\n\n")[1] + assert "waiting.md" in sup + assert "waiting.md" in wrk + + +def test_foreground_loop_docs_point_to_waiting(): + skills = ROOT / "skills" + for rel in ("external-ai/references/cli-codex.md", "external-ai/references/cli-agy.md", + "qa-security-scan/03-report-template.md", + "release/references/completion-check.md"): + assert "development-workflow/references/waiting.md" in (skills / rel).read_text(), rel + + +def test_context_window_doc(): + text = CONTEXT.read_text() + assert "実測ではない" not in text + assert "#827" in text + assert "NDF_CONTEXT_LIMIT" in text + assert "200,000" in text or "200000" in text + assert "/ndf:development-workflow #" in text + assert "stage-check.sh report" in text + assert "closedByPullRequestsReferences" in text + + +def test_context_limit_default_matches_doc(tmp_path, state): + tp = transcript(tmp_path, 200_001) + assert denied(run(skill(tp), state)) + tp2 = transcript(tmp_path, 200_000, name="t2.jsonl") + assert denied(run(skill(tp2, session="s2"), state)) is None + + +def test_workflow_skill_handover_rule(): + text = WF_SKILL.read_text() + assert "引き継ぎの 1 行" in text + assert "## 持ち場の報告" in text + assert "結果: 関門" in text + assert "context-window.md" in text + + +def test_readme_runtime_table(): + text = README.read_text() + assert "token-guard.sh" in text + for rt in ("Claude Code", "Codex", "Kiro", "agy"): + assert rt in text diff --git a/plugins/ndf/scripts/token-guard.sh b/plugins/ndf/scripts/token-guard.sh new file mode 100755 index 000000000..48a4eaf7e --- /dev/null +++ b/plugins/ndf/scripts/token-guard.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# NDF plugin: 待ちの呼び出しと、文脈が上限を超えた conductor の工程の起動を止める +# PreToolUse hook(#829 / #830)。Claude Code にだけ登録する。 +# +# | tool_name | 判定 | +# | ------------------ | ---------------------------------------------------------- | +# | Bash | 前景の `sleep` で待つ(ループの本体にあるか、上限を超える) | +# | Read | 変わらないファイルの同じ範囲を続けて読み直す | +# | Skill / Agent・Task | 文脈が上限を超えた conductor が工程へ入る | +# +# **拒否は `permissionDecision: deny` で返し、終了コードは常に 0 にする。** 通すときは何も +# 出さない。判定が失敗したとき(入力が読めない・jq が無い・控えを書けない・記録を読めない・ +# ロックを 1 秒で取れない)は通す。hook の失敗でツールの実行を止めないためである。 +# +# 規約は skills/development-workflow/references/waiting.md(待ち方)と +# context-window.md(会話を切る)にある。 + +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd) || exit 0 +WAITING_DOC="development-workflow/references/waiting.md" +CONTEXT_DOC="development-workflow/references/context-window.md" + +command -v jq >/dev/null 2>&1 || exit 0 +INPUT=$(cat) || exit 0 +TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0 +[ -n "$TOOL" ] || exit 0 + +field() { + printf '%s' "$INPUT" | jq -r "$1 // empty | tostring" 2>/dev/null +} + +deny() { + jq -cn --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse", + permissionDecision:"deny", permissionDecisionReason:$r}}' + exit 0 +} + +# 控えの置き場所。順は workflow-common.sh の wf_state_dir と同じ(あちらは stages/、 +# こちらは guards/ を置く)。workflow-common.sh は通信の層まで読み込むため、毎回の hook +# では読み込まない。 +guards_dir() { + local base fallback="${TMPDIR:-/tmp}/ndf-guards" + if [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then + base="$CLAUDE_PLUGIN_DATA/guards" + elif [ -n "${XDG_STATE_HOME:-}" ]; then + base="$XDG_STATE_HOME/ndf/guards" + elif [ -n "${HOME:-}" ]; then + base="$HOME/.local/state/ndf/guards" + else + base="$fallback" + fi + if mkdir -p "$base" 2>/dev/null && [ -w "$base" ]; then + printf '%s\n' "$base" + return 0 + fi + mkdir -p "$fallback" 2>/dev/null && [ -w "$fallback" ] || return 1 + printf '%s\n' "$fallback" +} + +# session ごとのロックを取る。取れなければ 1(呼び出し側は判定せずに通す)。 +LOCK= +take_lock() { + local dir="$1" sid="$2" + # shellcheck source=lib/lock-common.sh + . "$HERE/lib/lock-common.sh" 2>/dev/null || return 1 + LOCK="$dir/$sid.lock" + ndf_lock_acquire "$LOCK" 1 || { LOCK=; return 1; } + trap 'ndf_lock_release "$LOCK"' EXIT +} + +# 置き換えで書く。途中で落ちても壊れた JSON を残さない。 +write_json() { + local path="$1" body="$2" tmp + tmp="$path.$$.tmp" + printf '%s\n' "$body" >"$tmp" 2>/dev/null && mv -f "$tmp" "$path" 2>/dev/null + find "$(dirname "$path")" -maxdepth 1 -type f -name '*.json' -mtime +7 -delete 2>/dev/null + return 0 +} + +guard_sleep() { + [ "${NDF_SLEEP_GUARD:-1}" = 0 ] && exit 0 + [ "$(field '.tool_input.run_in_background')" = true ] && exit 0 + local cmd max + cmd=$(field '.tool_input.command') + [ -n "$cmd" ] || exit 0 + case "$cmd" in *sleep*) ;; *) exit 0 ;; esac + command -v python3 >/dev/null 2>&1 || exit 0 + max=${NDF_SLEEP_MAX_SEC:-5} + printf '%s' "$cmd" | python3 "$HERE/lib/token_guard_sleep.py" "$max" >/dev/null 2>&1 && exit 0 + deny "前景で sleep を使って待つと、待つ呼び出しのたびに会話の文脈の全体を読み直す(ループの本体の sleep と、${max} 秒を超える sleep を止めている)。同じ条件の until ループ(例: until [ -s <ファイル> ]; do sleep 5; done)を Bash の run_in_background: true で起動し、完了通知を待つ(通知は 1 回で、待つ間は呼び出しが増えない)。出来事を 1 つずつ受けるなら Monitor を使う。規約: ${WAITING_DOC}(止めるなら NDF_SLEEP_GUARD=0)" +} + +file_stat() { + # 大きさ・更新時刻(ナノ秒)・inode。無いファイルは -1。 + stat -c '%s %.9Y %i' "$1" 2>/dev/null || stat -f '%z %Fm %i' "$1" 2>/dev/null || echo "-1 -1 -1" +} + +guard_read() { + [ "${NDF_READ_REPEAT_GUARD:-1}" = 0 ] && exit 0 + local sid path key limit dir st size mtime inode prev count state + sid=$(field '.session_id') + path=$(field '.tool_input.file_path') + [ -n "$sid" ] && [ -n "$path" ] || exit 0 + key="$path"$'\t'"$(field '.tool_input.offset')"$'\t'"$(field '.tool_input.limit')" + limit=${NDF_READ_REPEAT_LIMIT:-3} + dir=$(guards_dir) || exit 0 + take_lock "$dir" "$sid" || exit 0 + read -r size mtime inode <<<"$(file_stat "$path")" + state="$dir/read-$sid.json" + prev=$(jq -r --arg k "$key" --arg s "$size" --arg m "$mtime" --arg i "$inode" \ + 'if .key == $k and (.size|tostring) == $s and .mtime == $m and (.inode|tostring) == $i + then .count else 0 end' "$state" 2>/dev/null) || prev=0 + count=$(( ${prev:-0} + 1 )) + write_json "$state" "$(jq -cn --arg k "$key" --argjson s "$size" --arg m "$mtime" \ + --argjson i "$inode" --argjson c "$count" \ + '{key:$k, size:$s, mtime:$m, inode:$i, count:$c}')" + [ "$count" -ge "$limit" ] || exit 0 + deny "同じファイルの同じ範囲を、変わらないまま ${count} 回続けて読もうとした(${path})。書き終わりを待つなら until [ -s <ファイル> ]; do sleep 1; done を Bash の run_in_background: true で起動して完了通知を待つか、背景の処理そのものの完了通知を待つ。サブエージェントの tasks/*.output は読まずに完了通知を待つ。規約: ${WAITING_DOC}(止めるなら NDF_READ_REPEAT_GUARD=0)" +} + +# 最後の assistant 行の usage から文脈量を読む。末尾だけを読むのは大きな記録でも速く終えるため。 +context_tokens() { + tail -n 200 "$1" 2>/dev/null | jq -rs ' + [ .[] | select(.type == "assistant" and (.message.usage | type) == "object") + | .message.usage + | (.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0) + ] | last // empty' 2>/dev/null +} + +guard_context() { + [ "${NDF_CONTEXT_GUARD:-1}" = 0 ] && exit 0 + # サブエージェントの中の起動は見ない。agent_id はサブエージェントの中でだけ付く + # (Claude Code 2.1.280 で実測。サブエージェントの transcript_path は親の記録を指す) + [ -n "$(field '.agent_id')" ] && exit 0 + local tp sid key words total limit dir mark issues skill + tp=$(field '.transcript_path') + case "$tp" in */subagents/*) exit 0 ;; esac + sid=$(field '.session_id') + [ -n "$tp" ] && [ -n "$sid" ] || exit 0 + if [ "$TOOL" = Skill ]; then + skill=$(field '.tool_input.skill') + skill=${skill#ndf:} + grep -qxF "$skill" "$HERE/lib/token-guard-stages.txt" 2>/dev/null || exit 0 + words=$(field '.tool_input.args') + key="skill"$'\t'"$skill"$'\t'"$words" + else + words=$(field '.tool_input.description') + case "${words%%:*}" in 設計|実装|検査|取り込み|仕上げ) ;; *) exit 0 ;; esac + case "$words" in *:*) ;; *) exit 0 ;; esac + key="agent"$'\t'"$words" + fi + total=$(context_tokens "$tp") + case "$total" in ''|*[!0-9]*) exit 0 ;; esac + limit=${NDF_CONTEXT_LIMIT:-200000} + dir=$(guards_dir) || exit 0 + take_lock "$dir" "$sid" || exit 0 + mark="$dir/context-$sid.json" + if [ "$(jq -r '.key // empty' "$mark" 2>/dev/null)" = "$key" ]; then + rm -f "$mark" 2>/dev/null + exit 0 + fi + [ "$total" -gt "$limit" ] || exit 0 + write_json "$mark" "$(jq -cn --arg k "$key" '{key:$k}')" + issues=$(printf '%s\n' "$words" | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ + | grep -oE '[0-9]+' | sed 's/^/#/' | tr '\n' ' ') + issues=${issues% } + deny "会話の文脈が ${total} トークンで、上限 ${limit} を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: /ndf:development-workflow ${issues:-<課題番号>}(3 層で進めているなら、新しい会話で /goal に同じ 1 行を渡す)。<課題番号> のままなら、進めている課題の番号を補って示す。このまま続けると利用者が決めたら、同じ起動をもう一度行うと 1 度だけ通る。規約: ${CONTEXT_DOC}(止めるなら NDF_CONTEXT_GUARD=0、上限は NDF_CONTEXT_LIMIT)" +} + +case "$TOOL" in + Bash) guard_sleep ;; + Read) guard_read ;; + Skill|Agent|Task) guard_context ;; +esac +exit 0 diff --git a/plugins/ndf/skills/development-workflow/SKILL.md b/plugins/ndf/skills/development-workflow/SKILL.md index d46118d3e..cef475b06 100644 --- a/plugins/ndf/skills/development-workflow/SKILL.md +++ b/plugins/ndf/skills/development-workflow/SKILL.md @@ -202,6 +202,14 @@ mode: standard 残っているのに後の工程の判断だけが悪くなる。** 切れ目・委譲してよい対象・残量の見方は [references/context-window.md](references/context-window.md) にある。 +**conductor は、`context-window.md` の 4 つの切れ目で、次の工程を始める引き継ぎの 1 行 +(`/ndf:development-workflow #<課題>`)を出す。** 3 層では conductor が `## 持ち場の報告` を +受け取った時点で出し、supervisor は出さない。持ち場の境がこの切れ目に当たるためである。 +文脈量の hook(`token-guard.sh`)が起動を止めたときも出す。ただし報告が `結果: 関門` なら +受け取った時点では出さず、関門の承認と取り込み(設計 Pull Request のマージなど)の後に出す。 +関門の前に会話を切らないためである。**その 1 行で始めた新しい会話が状態を戻す手順は、 +`context-window.md` の「新しい会話で戻す」にある。** + ## 範囲外の課題を見つけたとき この変更の受け入れ条件にも、直す対象にも含まれない課題は、**見つけたその場で `out-of-scope` が diff --git a/plugins/ndf/skills/development-workflow/references/agent-layers.md b/plugins/ndf/skills/development-workflow/references/agent-layers.md index 7f563e985..e9ef94573 100644 --- a/plugins/ndf/skills/development-workflow/references/agent-layers.md +++ b/plugins/ndf/skills/development-workflow/references/agent-layers.md @@ -122,6 +122,7 @@ supervisor が守る規則: `merged` は削除の一覧を示す。supervisor は人間へ問えないため、**確認待ちで止まらない** 3. 工程に入った時点で `progress-tracking` を呼ぶ。記録のコマンドは 1 回の Bash 実行に 1 件 4. 待ちで応答を終えない。待ちの道具から戻った後、同じ応答の中で次の段へ進む + 待ち方は [waiting.md](waiting.md) に従う(`sleep` を挟んだ問い合わせの繰り返しと、出力ファイルの読み直しをしない) 5. 外部へ書く前に、既に書いたものがあるかを確かめる(Pull Request・コメント・進行の記録) 6. 範囲外の課題は `out-of-scope` で起票する 7. 委譲してよい作業は worker へ出す。委譲しない 5 つは自分で行う(「委譲の線」) @@ -152,6 +153,7 @@ worker が守る規則: 3. 収束の判定・設計の決定・受け入れ条件の書き換えを行わない。判断が要るときは `結果: 判断が要る` で返す 4. 最後の応答の末尾に `## 作業の報告` を置く +5. 待ち方は [waiting.md](waiting.md) に従う。背景の処理を残したまま応答を終えない ## 報告の形 diff --git a/plugins/ndf/skills/development-workflow/references/context-window.md b/plugins/ndf/skills/development-workflow/references/context-window.md index e97422481..97849e2c6 100644 --- a/plugins/ndf/skills/development-workflow/references/context-window.md +++ b/plugins/ndf/skills/development-workflow/references/context-window.md @@ -38,13 +38,20 @@ worker の 3 層)は [agent-layers.md](agent-layers.md) が持つ。 **目安は 1 工程あたり 10 万トークンで、遅くとも 20 万で切る。** モデルが持つ上限まで 詰めない。 -**前提: この数値は公開された評価の設定と第三者のベンチマークからの推定であり、この -リポジトリでの実測ではない。** 実測が出た時点で書き換える。根拠は「出典」にある。 +**この上限は実測で守られていなかった**(#827。会話の記録から conductor の文脈量を数えた)。 + +| 範囲 | conductor の最大文脈 | 20 万を超えた会話 | 工程の開始ごとに切ったときの再読込の削減見込み | +| --- | --- | --- | --- | +| 2026-09-20 以降の全プロジェクト | 68 万 | 7 件中 4 件 | 58% | +| ai-plugins の 30 日間 | 平均 41 万 | 45 件中 37 件 | 62% | + +目安の値そのものは、公開された評価の設定と第三者のベンチマークからの推定である(「出典」)。 +**書いた規定だけでは守られないため、上限を超えたら hook が止める**(次の節)。 **要約して詰め直すより、切って入り直すほうを既定にする。** 要約は落ちた情報を残さない ため、**落ちたことに気づけない**。工程の状態は会話の外にあり(課題の本文・盤面・通過工程の -控え・Pull Request の差分)、**捨てても復元できる**。復元の手順を持つのは各工程の -Skill であって、この文書ではない。 +控え・Pull Request の差分)、**捨てても復元できる**。新しい会話で戻す手順は、この文書の +「新しい会話で戻す」節が持つ。 ### 切ってよい点は 4 つある @@ -70,6 +77,58 @@ Skill であって、この文書ではない。 **切れ目でないところで尽きたときは、尽きた側に合わせる。** 残量が足りないまま次の 工程へ入るより、進行を記録して切るほうが安い。 +## 上限を超えたら hook が止める + +**Claude Code では、文脈が上限を超えた conductor が工程へ入る起動を hook が 1 度止める** +(`scripts/token-guard.sh`)。上限の既定は **200,000** トークンで、上の「遅くとも 20 万」と +同じ値である。環境変数 `NDF_CONTEXT_LIMIT` で変えられ、`NDF_CONTEXT_GUARD=0` で止められる。 + +| 経路 | 止める起動 | +| --- | --- | +| 対話 | 工程 Skill(上の 4 つの切れ目の直後に始まる工程と、入口の `development-workflow` / `issue-plan-strategy`。一覧は `scripts/lib/token-guard-stages.txt`) | +| 3 層 | `description` の先頭語が持ち場(`設計` / `実装` / `検査` / `取り込み` / `仕上げ`)の Agent | + +- **文脈量は、会話の記録の最後の assistant 呼び出しの `usage` から読む** + (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`)。読めなければ止めない +- **サブエージェントの中の起動は止めない。** 見るのは conductor だけである +- **止めるのは工程の切れ目ごとに 1 度である。** 止めた直後に、工程へ入る次の起動が同じもの + (Skill なら名前と引数、Agent なら `description`)であれば 1 度だけ通す。**続けると決めたら、 + 同じ起動をもう一度行う。** 別の起動なら、上限を超えている限り再び止める +- 止めたときの理由の欄が、利用者へ示す 1 行(次の節)を持つ。conductor はその 1 行を示して + 応答を終える + +Codex / Kiro / agy には hook を置かない。4 つの切れ目で 1 行を出す規約だけで守る +([waiting.md](waiting.md) の「hook」節の表)。 + +## 新しい会話で戻す + +**conductor は、4 つの切れ目と hook に止められたときに、次の工程を始める 1 行を出す。** + +```text +/ndf:development-workflow #829 #830 +``` + +- **形は `development-workflow` を起動する 1 行である。** 工程 Skill はモードと作業ツリーを戻す + 手順を持たないため、入口から入り直す。Codex と Kiro では、それぞれの README が示す Skill の + 起動の書き方に読み替える +- **3 層では、conductor が `## 持ち場の報告` を受け取った時点で出す**(supervisor は出さない)。 + 持ち場の境が切れ目に当たるためである。ただし報告が `結果: 関門` なら、関門の承認と取り込み + (設計 Pull Request のマージなど)が済んだ後に出す。関門の前に会話を切らないためで、 + 切れ目 1 はこの形で満たす。新しい会話では `/goal` に同じ 1 行を渡す + +**新しい会話の `development-workflow` は、次の順で状態を戻す。** + +| # | 読むもの | 戻すもの | +| --- | --- | --- | +| 1 | 課題の本文の `## 進行`(`gh issue view <番号> --json body`) | モード・作業ツリー・計画ファイル・通った工程 | +| 2 | `stage-check.sh report <番号>` | 通過工程の控え。本文と食い違えば控えを正とする | +| 3 | 1 の作業ツリー(`.worktrees/<ブランチ名>`)のブランチ名で `gh pr list --head <ブランチ名> --state all`。実装の Pull Request は `gh issue view <番号> --json closedByPullRequestsReferences` でも引く | 設計・実装の Pull Request と状態 | +| 4 | 1〜3 から、チェックの付いていない最初の必須の工程 | 次に起動する工程 Skill | + +**Pull Request は番号の全文検索で引かない。** 同じ番号に触れただけの別の Pull Request も返す +ためである。設計の Pull Request は閉じる語を持たないため、課題との結び付きでは引けず、 +ブランチ名で引く。 + ## 粒度は比で決める **基準は値ではなく比である。** 固定費はリポジトリとモデルで変わるため、閾値を書くと @@ -148,6 +207,7 @@ supervisor と配下の worker の固定費の合計が、その supervisor の | 要約より新しい文脈を選ぶ助言 | モデル提供者の公式ドキュメントの「プロンプトの実践」の節 | | 残量の自己認識を持つモデルが限られること | 同上の「コンテキストの自己認識」の節 | | 入力長に対する劣化が全モデルで単調であること | 第三者が 18 モデルを比べた公開の研究 | +| conductor の最大文脈・20 万を超えた会話の数・再読込の削減見込み | #827(会話の記録からの実測) | **値そのものを写さない。** 版が変わると数字が変わり、写した側だけが古くなる。この文書が 持つのは**運用の目安**であって、モデルの性能値ではない。 diff --git a/plugins/ndf/skills/development-workflow/references/waiting.md b/plugins/ndf/skills/development-workflow/references/waiting.md new file mode 100644 index 000000000..bd8311598 --- /dev/null +++ b/plugins/ndf/skills/development-workflow/references/waiting.md @@ -0,0 +1,83 @@ +# 待ち方 + +**待つ間に状態を問い合わせる呼び出しを繰り返さない。** 待ち方の規約はこの文書だけが持ち、 +他の文書は写さずにここを指す。 + +## 待ちの費用 + +**呼び出しは 1 回ごとに、その時点の会話の文脈の全体を読み直す。** `sleep 60 && tail -5 x.log` +を 30 回繰り返すと、30 回とも文脈の全体を読む。背景で待って通知を 1 回受けるなら、待つ時間が +長くても呼び出しは増えない。 + +#827 の実測では、待つ間の繰り返しの問い合わせ(ポーリング)が全体の費用の 16%(2026-09-20 +以降)、ai-plugins の 30 日間では 19% を占めた。 + +| 待ち方 | 待つ間の呼び出し | 費用 | +| --- | --- | --- | +| 前景の `sleep` を挟んで状態を問い合わせ直す | 待つ時間 ÷ 間隔 | 回数 × その時点の文脈 | +| 出力ファイルを読み直す | 読み直した回数 | 同上 | +| `run_in_background` で起動し、完了通知を待つ | 0(通知が 1 回) | 待つ時間に依らない | +| `Monitor` で出来事を 1 つずつ受ける | 出来事の数 | 出来事の数 × 文脈 | + +## 許す待ち方 + +| ランタイム | 待ち方 | +| --- | --- | +| Claude Code | **条件の until ループを Bash の `run_in_background: true` で起動し、完了通知を 1 回受ける。** 出来事を 1 つずつ受けるなら `Monitor`。サブエージェントは完了通知を待つ | +| Codex / Kiro / agy | 1 回の前景の until ループ。600 秒を超えるなら `bg-wait.sh` | + +**1 回で足りる待ちは `Monitor` ではなく `run_in_background` にする。** `Monitor` は出来事の +たびに通知が届き、その都度文脈を読む。終わりだけを知りたい待ちでは通知が 1 回で済む +`run_in_background` のほうが安い。 + +## 禁じる待ち方 + +- **`sleep` を挟んだ呼び出しの繰り返し。** `sleep 30 && tail x.log` を何度も打つ形と、前景の + `while` / `until` のループの本体で `sleep` する形 +- **出力ファイルの繰り返しの読み直し。** 変わっていないファイルの同じ範囲を続けて読む形 +- **サブエージェントの `tasks/*.output` を読むこと。** 会話の記録の全体で、読むと文脈を埋める。 + 完了通知を待つ + +## 待つ相手ごとの手 + +| 待つ相手 | 手(Claude Code) | +| --- | --- | +| サブエージェント | 完了通知を待つ。途中の出力を読まない | +| 背景で動かす CLI(`codex exec` など) | CLI そのものを `run_in_background: true` で起動し、完了通知を待つ | +| 既に起動したプロセス・書き終わりを待つファイル | 終わりを待つ until ループ(例: `until [ -s out.md ]; do sleep 5; done`)を `run_in_background: true` で起動する | +| Pull Request の検査 | `gh pr checks <番号> --watch` を `run_in_background: true` で起動する | +| 新しいコメントを 1 件ずつ | `Monitor` | + +**サブエージェントは、背景の処理を残したまま応答を終えない。** 完了通知で再開はされるが、 +**親には応答を終えた時点で 1 度「終わった」と通知が届き、途中の文面が結果として渡る** +(Claude Code 2.1.280 で実測。`codex exec` を背景で起動して応答を終えたサブエージェントは、 +約 2 秒後の完了通知で再開して報告を出し直し、親には通知が 2 回届いた)。親が 1 回目を +結果と読むと、報告の無い持ち場を受け取る。supervisor と worker は待ちで応答を終えない +([agent-layers.md](agent-layers.md) の規則)。背景の処理を起動した後は、同じ応答の中で +他の作業を進め、通知を受けてから次の段へ進む。他の作業が無いまま待つときの手は #656 が扱う。 +**親の側は、この 2 回目の通知を待ってから報告を読む**(1 回目の通知の注記に「再開しうる」と出る)。 + +## hook + +**Claude Code では、禁じる待ち方を hook が止める**(`scripts/token-guard.sh`。PreToolUse の +`Bash` と `Read` で動く)。止めたときは理由の欄に代わりの待ち方が出る。 + +| 判定 | 止める条件 | 止め方 | 上限を変える | +| --- | --- | --- | --- | +| sleep | 前景の Bash で、コマンドの位置の `sleep <数>` が `while` / `until` のループの本体にあるか、秒数が上限を超える。コメント・引用・ヒアドキュメントの本文は見ず、`bash -c` / `sh -c` / `eval` の中身は見る | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | +| 連続 Read | 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらないまま上限の回数に達する | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | + +- **止めないもの:** `run_in_background: true` の Bash、`Monitor` の中の `sleep`、ループの本体の + 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep` +- **判定が失敗したときは止めない**(入力が読めない・`jq` や `python3` が無い・控えを書けない) +- 同じ hook が、文脈が上限を超えた conductor の工程の起動も止める([context-window.md](context-window.md) + の「上限を超えたら hook が止める」) + +**hook を置くのは Claude Code だけである。** + +| ランタイム | 待ち方(#829) | 会話を切る(#830) | 理由 | +| --- | --- | --- | --- | +| Claude Code | hook + この規約 | hook + 引き継ぎの 1 行 | 代わりの待ち方(`Monitor` / `run_in_background` の通知)と会話の記録の場所を持つ | +| Codex | この規約だけ | 引き継ぎの 1 行だけ | 背景の起動と完了通知が無く、1 回の前景のループが待ち方になる | +| Kiro | この規約だけ | 引き継ぎの 1 行だけ | 実行前の hook は拒否しか返せず、既存の設計も実行前の hook を置いていない | +| agy | この規約だけ | 引き継ぎの 1 行だけ | 実行前の hook は案内を控えへ積む形で、拒否の口を使っていない | diff --git a/plugins/ndf/skills/external-ai/references/cli-agy.md b/plugins/ndf/skills/external-ai/references/cli-agy.md index 290d03974..a527a38dd 100644 --- a/plugins/ndf/skills/external-ai/references/cli-agy.md +++ b/plugins/ndf/skills/external-ai/references/cli-agy.md @@ -120,6 +120,8 @@ $ agy --output-format json -p="1+1は?数字だけ答えて" sentinel を出さないため、**プロセスの終了**を見る。 +**Claude Code では、このループを Bash の `run_in_background: true` で実行して完了通知を待つ**(前景で回すと hook が止める。規約は `development-workflow/references/waiting.md`)。 + ```bash until ! kill -0 $PID 2>/dev/null; do sleep 30 diff --git a/plugins/ndf/skills/external-ai/references/cli-codex.md b/plugins/ndf/skills/external-ai/references/cli-codex.md index 295edf0cd..13c785949 100644 --- a/plugins/ndf/skills/external-ai/references/cli-codex.md +++ b/plugins/ndf/skills/external-ai/references/cli-codex.md @@ -106,6 +106,8 @@ Codex は最終 message を返さなくても `apply_patch` でファイルを `ps -p $PID` は zombie (defunct) にも 0 を返すため、**PID watch は永久ループになりうる**。 stderr 末尾の sentinel を脱出条件にする。 +**Claude Code では、このループを Bash の `run_in_background: true` で実行して完了通知を待つ**(前景で回すと hook が止める。規約は `development-workflow/references/waiting.md`)。 + ```bash # ❌ 永久ループ化しうる until ! ps -p $PID; do sleep 30; done diff --git a/plugins/ndf/skills/qa-security-scan/03-report-template.md b/plugins/ndf/skills/qa-security-scan/03-report-template.md index 6ad9f7ec3..2ab5ab0b9 100644 --- a/plugins/ndf/skills/qa-security-scan/03-report-template.md +++ b/plugins/ndf/skills/qa-security-scan/03-report-template.md @@ -125,6 +125,8 @@ codex exec --dangerously-bypass-approvals-and-sandbox \ 2> /tmp/sec-scan-err.log & # === 3. 完了確認(^tokens used$ sentinel を待つ。`ps -p` は zombie を生存と誤判定する) === +# Claude Code では、このループを Bash の run_in_background: true で実行して完了通知を待つ +# (前景で回すと hook が止める。規約は development-workflow/references/waiting.md) until grep -q '^tokens used$' /tmp/sec-scan-err.log 2>/dev/null; do sleep 30 done diff --git a/plugins/ndf/skills/release/references/completion-check.md b/plugins/ndf/skills/release/references/completion-check.md index ccca865ff..1dabeb7bc 100644 --- a/plugins/ndf/skills/release/references/completion-check.md +++ b/plugins/ndf/skills/release/references/completion-check.md @@ -35,6 +35,8 @@ そのまま貼って使う。**再起動・pidfile・結果ファイルの規約は持たず、ログの読み取りだけを行う。** +**Claude Code では、このループを Bash の `run_in_background: true` で実行して完了通知を待つ**(前景で回すと hook が止める。規約は `development-workflow/references/waiting.md`)。 + ```bash # $LOG は追記されるログ、$DONE と $FAIL は先に流して確かめた語、$IDLE と $LIMIT は秒 # 未設定の変数と作れないログはここで止める。空の値のまま進むと比較が構文誤りを出し続ける From b9c0e05d54ee394a7dfd9888b06e02c7c205c347 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:09:42 +0000 Subject: [PATCH 12/30] =?UTF-8?q?Docs:=20=E6=96=B0=E3=81=97=E3=81=84?= =?UTF-8?q?=E4=BC=9A=E8=A9=B1=E3=81=A7=E6=88=BB=E3=81=99=E6=89=8B=E9=A0=86?= =?UTF-8?q?=E3=81=AE=20stage-check.sh=20=E3=81=AE=E5=A0=B4=E6=89=80?= =?UTF-8?q?=E3=82=92=E6=AD=A3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AC20 の実機の確認で、プラグインの scripts/ にあると読めて探す手間が出た。 実体は development-workflow の scripts/ にある。 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- .../skills/development-workflow/references/context-window.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ndf/skills/development-workflow/references/context-window.md b/plugins/ndf/skills/development-workflow/references/context-window.md index 97849e2c6..bf9665abe 100644 --- a/plugins/ndf/skills/development-workflow/references/context-window.md +++ b/plugins/ndf/skills/development-workflow/references/context-window.md @@ -121,7 +121,7 @@ Codex / Kiro / agy には hook を置かない。4 つの切れ目で 1 行を | # | 読むもの | 戻すもの | | --- | --- | --- | | 1 | 課題の本文の `## 進行`(`gh issue view <番号> --json body`) | モード・作業ツリー・計画ファイル・通った工程 | -| 2 | `stage-check.sh report <番号>` | 通過工程の控え。本文と食い違えば控えを正とする | +| 2 | `bash <この Skill のディレクトリ>/scripts/stage-check.sh report <番号>`(プラグインの `scripts/` ではなく、`development-workflow` の `scripts/` にある) | 通過工程の控え。本文と食い違えば控えを正とする | | 3 | 1 の作業ツリー(`.worktrees/<ブランチ名>`)のブランチ名で `gh pr list --head <ブランチ名> --state all`。実装の Pull Request は `gh issue view <番号> --json closedByPullRequestsReferences` でも引く | 設計・実装の Pull Request と状態 | | 4 | 1〜3 から、チェックの付いていない最初の必須の工程 | 次に起動する工程 Skill | From c0bbfb8192b42c6a0e1cdea1eb5650af3aa8b1b2 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:10:15 +0000 Subject: [PATCH 13/30] =?UTF-8?q?Docs:=20=E5=AE=9F=E6=B8=AC=E3=81=AE?= =?UTF-8?q?=E8=A1=A8=E3=81=AE=E5=88=97=E3=82=92=E6=9C=80=E5=A4=A7=E3=81=A8?= =?UTF-8?q?=E5=B9=B3=E5=9D=87=E3=81=8C=E4=B8=A6=E3=81=B6=E5=BD=A2=E3=81=AB?= =?UTF-8?q?=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- .../skills/development-workflow/references/context-window.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/ndf/skills/development-workflow/references/context-window.md b/plugins/ndf/skills/development-workflow/references/context-window.md index bf9665abe..a4aea6482 100644 --- a/plugins/ndf/skills/development-workflow/references/context-window.md +++ b/plugins/ndf/skills/development-workflow/references/context-window.md @@ -40,9 +40,9 @@ worker の 3 層)は [agent-layers.md](agent-layers.md) が持つ。 **この上限は実測で守られていなかった**(#827。会話の記録から conductor の文脈量を数えた)。 -| 範囲 | conductor の最大文脈 | 20 万を超えた会話 | 工程の開始ごとに切ったときの再読込の削減見込み | +| 範囲 | conductor の文脈 | 20 万を超えた会話 | 工程の開始ごとに切ったときの再読込の削減見込み | | --- | --- | --- | --- | -| 2026-09-20 以降の全プロジェクト | 68 万 | 7 件中 4 件 | 58% | +| 2026-09-20 以降の全プロジェクト | 最大 68 万 | 7 件中 4 件 | 58% | | ai-plugins の 30 日間 | 平均 41 万 | 45 件中 37 件 | 62% | 目安の値そのものは、公開された評価の設定と第三者のベンチマークからの推定である(「出典」)。 From 9f12b34166e9a687b7c8db6007de9eb660aae59f Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:29:43 +0000 Subject: [PATCH 14/30] =?UTF-8?q?Test:=20token=5Fguard=5Fsleep.py=20?= =?UTF-8?q?=E3=81=AE=20main=20=E3=81=AB=20zsh=20-c=20/=20dash=20-c=20?= =?UTF-8?q?=E3=81=AE=E7=8F=BE=E7=8A=B6=E5=9B=BA=E5=AE=9A=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 再帰的に実行文字列を検査する対象として明示された shell のうち、bash と sh は DENY_SLEEP で固定済みだが zsh と dash の -c 経路は未固定だった。上限超過の sleep と ループ本体の sleep を含む zsh -c / dash -c 入力を DENY_SLEEP に足し、いずれも拒否 (deny)になる現状を固定する。対象コードは変更していない。 Item-Id: R1-001 Round: 1 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 3443b0ba4..a542c48b8 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -85,6 +85,10 @@ def bash(cmd, **extra): "bash -c 'sleep 30'", 'timeout 590 bash -c "until [ -s f ]; do sleep 5; done"', "sh -c 'until test -s x; do sleep 1; done'", + "zsh -c 'sleep 30'", + "zsh -c 'until [ -s f ]; do sleep 5; done'", + "dash -c 'sleep 30'", + "dash -c 'until [ -s f ]; do sleep 5; done'", "for i in 1 2; do sleep 10; done", "while a; do while b; do sleep 1; done; done", "while a; do\n for i in 1 2; do sleep 1; done\ndone", From edfde0109e1da5e7852fba3b265a1f7d1a7d6841 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:38:10 +0000 Subject: [PATCH 15/30] =?UTF-8?q?Test:=20select=20=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97=E5=86=85=20sleep=20=E3=81=AE=E7=8F=BE=E7=8A=B6?= =?UTF-8?q?=E3=82=92=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit select ループ本体の上限超過と上限以下の sleep 判定を既存のパラメータ化テストへ追加する。 Item-Id: R1-002 Round: 1 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index a542c48b8..f811bc590 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -90,6 +90,7 @@ def bash(cmd, **extra): "dash -c 'sleep 30'", "dash -c 'until [ -s f ]; do sleep 5; done'", "for i in 1 2; do sleep 10; done", + "select x in a b; do sleep 10; done", "while a; do while b; do sleep 1; done; done", "while a; do\n for i in 1 2; do sleep 1; done\ndone", 'eval "sleep 30"', @@ -102,6 +103,7 @@ def bash(cmd, **extra): "python3 -m http.server & sleep 2", "for p in 1 2; do gh api x; sleep 1; done", "for i in 1 2; do sleep 3; done", + "select x in a b; do sleep 3; done", "echo sleep 30", 'git commit -m "sleep 60"', "# sleep 30", From 547e8de53248c6c61d3c34a5672b2ef50ee0b78a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:46:36 +0000 Subject: [PATCH 16/30] =?UTF-8?q?Test:=20characterization=20=E2=80=94=20pl?= =?UTF-8?q?ugins/ndf/scripts/token-guard.sh#guard=5Fcontext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assistant の usage が複数あるとき、最後の値で上限判定する経路を現状固定する。 上限超過の後に上限以内の usage が来ると通り、順序を逆にすると拒否されることを 比較する現状固定テストを追加。対象コードは変更しない。 Item-Id: R1-003 Round: 1 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index f811bc590..6acd645c7 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -322,6 +322,21 @@ def transcript(tmp_path, total, name="t.jsonl", usage=True): return path +def transcript_multi(tmp_path, totals, name="t.jsonl"): + """複数の assistant usage を順に持つ transcript。最後の usage が判定に使われる。""" + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + lines = [{"type": "user", "message": {"content": "x"}}] + for total in totals: + lines.append({"type": "assistant", "message": { + "role": "assistant", "content": [], + "usage": {"input_tokens": 10, "cache_read_input_tokens": total - 110, + "cache_creation_input_tokens": 100, "output_tokens": 5}}}) + lines.append({"type": "attachment"}) + path.write_text("\n".join(json.dumps(x) for x in lines) + "\n") + return path + + def skill(tp, name="ndf:implementation-plan", args="#829", session="s1", **extra): p = {"tool_name": "Skill", "tool_input": {"skill": name, "args": args}, "session_id": session, "transcript_path": str(tp)} @@ -363,6 +378,15 @@ def test_context_within_limit_passes(tmp_path, state): assert denied(run(skill(tp), state)) is None +def test_context_uses_last_assistant_usage(tmp_path, state): + # 現状固定: assistant の usage が複数あるとき、最後の値で上限判定する。 + # 上限超過の後に上限以内が来れば通り、順序を逆にすると拒否される。 + over_then_under = transcript_multi(tmp_path, [250_000, 150_000], name="ou.jsonl") + assert denied(run(skill(over_then_under, session="sou"), state)) is None + under_then_over = transcript_multi(tmp_path, [150_000, 250_000], name="uo.jsonl") + assert denied(run(skill(under_then_over, session="suo"), state)) + + def test_context_subagent_passes(tmp_path, state): tp = transcript(tmp_path, 250_000) assert denied(run(skill(tp, agent_id="a1"), state)) is None From 222bd9a9d9f8be8bb88404dbcf9d9ba1b402c1fc Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 02:55:02 +0000 Subject: [PATCH 17/30] =?UTF-8?q?Test:=20=E5=A3=8A=E3=82=8C=E3=81=9Ftransc?= =?UTF-8?q?ript=E3=81=AEfail-open=E3=82=92=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上限超過のusageに壊れたJSON行が続く場合、公開hookが拒否せず終了する現状を結合テストで固定する。 Item-Id: R1-004 Round: 1 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 6acd645c7..01e0a30b4 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -387,6 +387,17 @@ def test_context_uses_last_assistant_usage(tmp_path, state): assert denied(run(skill(under_then_over, session="suo"), state)) +def test_context_malformed_transcript_passes(tmp_path, state): + # 現状固定: usage が上限超過でも、壊れた JSON 行がある記録は読めず fail-open する。 + tp = transcript(tmp_path, 250_000) + with tp.open("a") as fh: + fh.write("{not json\n") + + proc = run(skill(tp), state) + assert proc.returncode == 0 + assert proc.stdout == "" + + def test_context_subagent_passes(tmp_path, state): tp = transcript(tmp_path, 250_000) assert denied(run(skill(tp, agent_id="a1"), state)) is None From 12ef269116c74f67a2d3bef0cc377dcd74b75cc2 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:03:30 +0000 Subject: [PATCH 18/30] =?UTF-8?q?Test:=20characterization=20=E2=80=94=20pl?= =?UTF-8?q?ugins/ndf/scripts/token-guard.sh#guard=5Fread?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 既存の session 読み直し状態 JSON が壊れている場合の、guard_read の 読み直し判定と状態の回復を現状固定する。壊れた状態では 0 から数え直し、 最初の Read が通って状態が有効な JSON(count=1)へ置き換わり、以後は 現在の上限回で拒否されることを固定する。 Item-Id: R1-005 Round: 1 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 01e0a30b4..19e47e2d6 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -199,6 +199,24 @@ def test_repeat_read_resets_on_replaced_file(tmp_path, state): assert denied(run(read(f), state)) is None +def test_repeat_read_recovers_from_broken_state(tmp_path, state): + # 現状固定: 既存の read 状態 JSON が壊れているとき、読み直し判定は 0 から数え直す。 + # 最初の Read は通り、状態は有効な JSON(count=1)へ置き換わる。以後は同じ範囲を + # 続けて読むと現在の上限(既定 3)回で拒否される。 + f = tmp_path / "out.txt" + f.write_text("") + guards = state / "guards" + guards.mkdir(parents=True) + broken = guards / "read-s1.json" + broken.write_text("{not json") + assert denied(run(read(f), state)) is None + saved = json.loads(broken.read_text()) + assert saved["count"] == 1 + assert denied(run(read(f), state)) is None + reason = denied(run(read(f), state)) + assert reason and "3 回" in reason + + def test_repeat_read_is_per_session(tmp_path, state): f = tmp_path / "out.txt" f.write_text("") From bb24659b36054d5b6902d86615b34203d0db83c4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:18:02 +0000 Subject: [PATCH 19/30] =?UTF-8?q?Test:=20context=20guard=20=E3=81=AE?= =?UTF-8?q?=E6=9C=AB=E5=B0=BE200=E8=A1=8C=E5=A2=83=E7=95=8C=E3=82=92?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上限超過 usage が末尾200行の外側と内側にある場合の現状挙動を比較する。 Item-Id: R2-001 Round: 2 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 19e47e2d6..d026b7355 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -405,6 +405,21 @@ def test_context_uses_last_assistant_usage(tmp_path, state): assert denied(run(skill(under_then_over, session="suo"), state)) +def test_context_only_reads_last_200_lines(tmp_path, state): + # 現状固定: 上限超過の usage が末尾 200 行から外れると通り、範囲内なら拒否される。 + outside_tail = transcript(tmp_path, 250_000, name="outside.jsonl") + with outside_tail.open("a") as fh: + for _ in range(201): + fh.write(json.dumps({"type": "attachment"}) + "\n") + assert denied(run(skill(outside_tail, session="sout"), state)) is None + + inside_tail = transcript(tmp_path, 250_000, name="inside.jsonl") + with inside_tail.open("a") as fh: + for _ in range(198): + fh.write(json.dumps({"type": "attachment"}) + "\n") + assert denied(run(skill(inside_tail, session="sin"), state)) + + def test_context_malformed_transcript_passes(tmp_path, state): # 現状固定: usage が上限超過でも、壊れた JSON 行がある記録は読めず fail-open する。 tp = transcript(tmp_path, 250_000) From 8959ebee0f1109f70afe5ba47d048d0c4c8b12c4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:26:32 +0000 Subject: [PATCH 20/30] =?UTF-8?q?Test:=20characterization=20=E2=80=94=20pl?= =?UTF-8?q?ugins/ndf/scripts/token-guard.sh#guard=5Fcontext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guard_context の agent 分岐で、description の課題番号が # を付けない裸の番号でも 案内が sed 's/^/#/' で # 付きへ整えられる振る舞いを現状固定する。検査/取り込み/仕上げ の 3 入力が終了コード 0 の deny になり、/ndf:development-workflow #829 を示すことを固定。 Item-Id: R2-002 Round: 2 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index d026b7355..ee8872d47 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -475,6 +475,17 @@ def test_context_agent_once_then_pass(tmp_path, state): assert denied(run(agent(tp, desc="実装: #829"), state)) +@pytest.mark.parametrize("desc", ["検査: 829", "取り込み: 829", "仕上げ: 829"]) +def test_context_agent_bare_issue_number_is_normalized(tmp_path, state, desc): + # 現状固定: description の課題番号が # を付けない裸の番号でも、案内は + # sed 's/^/#/' で # 付きへ整えられる。各入力は終了コード 0 の deny になり、 + # 案内は /ndf:development-workflow #829 を示す(session を分けて 1 回目で拒否)。 + tp = transcript(tmp_path, 250_000) + session = "sbare" + desc[:1] + reason = denied(run(agent(tp, desc=desc, session=session), state)) + assert reason and "/ndf:development-workflow #829" in reason + + def test_context_guard_env(tmp_path, state): tp = transcript(tmp_path, 250_000) assert denied(run(skill(tp), state, {"NDF_CONTEXT_GUARD": "0"})) is None From b97b7dacef82ed83e2f3699119cb2e4e29e5a680 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:35:06 +0000 Subject: [PATCH 21/30] =?UTF-8?q?Test:=20=E6=AC=A0=E8=90=BD=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE=E9=80=A3=E7=B6=9ARead?= =?UTF-8?q?=E3=82=92=E7=8F=BE=E7=8A=B6=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 存在しない同一ファイルへのReadが3回目に拒否される境界経路を固定する。 Item-Id: R2-003 Round: 2 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index ee8872d47..2ab995dd1 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -167,6 +167,15 @@ def test_repeat_read_denied_on_third(tmp_path, state): assert denied(run(read(f), state)) +def test_repeat_read_denied_on_third_when_file_is_missing(tmp_path, state): + # 現状固定: 存在しないファイルも file_stat の sentinel 値で同じ状態として数える。 + missing = tmp_path / "missing.txt" + assert denied(run(read(missing), state)) is None + assert denied(run(read(missing), state)) is None + reason = denied(run(read(missing), state)) + assert reason and "3 回" in reason + + def test_repeat_read_resets_when_file_changes(tmp_path, state): f = tmp_path / "out.txt" f.write_text("a") From 5236de77bccffeb9040687d8422bd9334a4452f8 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:43:35 +0000 Subject: [PATCH 22/30] =?UTF-8?q?Test:=20guard=5Fsleep=20=E3=81=AE?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=83=BB=E6=97=A5=E5=8D=98=E4=BD=8D=E3=81=A8?= =?UTF-8?q?=E5=B0=8F=E6=95=B0=E7=A7=92=E6=8F=9B=E7=AE=97=E3=81=AE=E7=8F=BE?= =?UTF-8?q?=E7=8A=B6=E5=9B=BA=E5=AE=9A=20=E2=80=94=20plugins/ndf/scripts/t?= =?UTF-8?q?oken-guard.sh#guard=5Fsleep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DENY_SLEEP は 'sleep 1m'(分)だけを固定し、時間・日の単位(h / d)と小数の 秒換算の経路が固定されていなかった。既定の上限 5 秒に対し 'sleep 0.1h'(360 秒)と 'sleep 1d'(86400 秒)が拒否されることを、waiting.md への部分一致だけで固定する 現状固定テストを追加した。対象のコードは変更していない。 Item-Id: R2-004 Round: 2 Impl-Runtime: kiro Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 2ab995dd1..475df33e7 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -132,6 +132,17 @@ def test_sleep_allowed(cmd, state): assert denied(run(bash(cmd), state)) is None, cmd +@pytest.mark.parametrize("cmd", ["sleep 0.1h", "sleep 1d"]) +def test_sleep_denied_on_hour_and_day_units(cmd, state): + # 現状固定: DENY_SLEEP は 'sleep 1m'(分)だけを固定していたが、時間・日の単位と + # 小数の秒換算の経路は固定されていなかった。既定の上限 5 秒に対し 'sleep 0.1h' は + # 0.1*3600=360 秒、'sleep 1d' は 86400 秒へ換算され、いずれも拒否される(deny)。 + # 拒否理由は waiting.md への部分一致だけで確かめ、文言全体には結合しない。 + reason = denied(run(bash(cmd), state)) + assert reason, cmd + assert "waiting.md" in reason + + def test_background_bash_is_allowed(state): p = bash("sleep 30 && tail x", run_in_background=True) assert denied(run(p, state)) is None From 15385c2772b953a3cf73e56e013f834a8c03b5a2 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 03:52:06 +0000 Subject: [PATCH 23/30] =?UTF-8?q?Test:=20here-string=20=E3=81=AE=20sleep?= =?UTF-8?q?=20=E5=88=A4=E5=AE=9A=E3=82=92=E7=8F=BE=E7=8A=B6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit here-string の内容をヒアドキュメント本文と誤認せず、拒否しない既存経路を固定する。 Item-Id: R2-005 Round: 2 Impl-Runtime: codex Impl-Model: default --- plugins/ndf/scripts/tests/test_token_guard.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 475df33e7..420e33f45 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -132,6 +132,11 @@ def test_sleep_allowed(cmd, state): assert denied(run(bash(cmd), state)) is None, cmd +def test_sleep_in_here_string_is_allowed(state): + # 現状固定: here-string の内容はヒアドキュメント本文ではなく、sleep 判定の対象外になる。 + assert denied(run(bash('grep x <<< "sleep 60"'), state)) is None + + @pytest.mark.parametrize("cmd", ["sleep 0.1h", "sleep 1d"]) def test_sleep_denied_on_hour_and_day_units(cmd, state): # 現状固定: DENY_SLEEP は 'sleep 1m'(分)だけを固定していたが、時間・日の単位と From 2dcef96a78f8c3b62d64650154fedee832786987 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:04:13 +0000 Subject: [PATCH 24/30] =?UTF-8?q?Fix:=20#844=20=E3=83=A9=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=89=201=20=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=EF=BC=88sleep=20=E5=88=A4=E5=AE=9A=E3=81=AE=E4=BB=A3?= =?UTF-8?q?=E5=85=A5=E8=AA=9E=E3=83=BB=E8=83=8C=E6=99=AF=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=83=BB=E5=BC=95=E6=95=B0=E4=BB=98=E3=81=8D=E3=82=AA=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=BB=E3=83=AB=E3=83=BC=E3=83=97?= =?UTF-8?q?=E5=86=85=E3=81=AE=E5=8B=95=E7=9A=84=E7=A7=92=E6=95=B0=E3=80=81?= =?UTF-8?q?=E7=89=88=E6=95=B0=E3=82=92=E8=AA=B2=E9=A1=8C=E7=95=AA=E5=8F=B7?= =?UTF-8?q?=E3=81=A8=E8=AA=AD=E3=81=BE=E3=81=AA=E3=81=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- plugins/ndf/scripts/lib/token_guard_sleep.py | 25 +++++++++++++------ plugins/ndf/scripts/tests/test_token_guard.py | 14 +++++++++++ plugins/ndf/scripts/token-guard.sh | 3 ++- .../references/waiting.md | 4 +-- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/plugins/ndf/scripts/lib/token_guard_sleep.py b/plugins/ndf/scripts/lib/token_guard_sleep.py index 23bb07e39..ee715f351 100644 --- a/plugins/ndf/scripts/lib/token_guard_sleep.py +++ b/plugins/ndf/scripts/lib/token_guard_sleep.py @@ -3,10 +3,11 @@ 標準入力にコマンドの文字列を受け、第 1 引数に秒数の上限を受ける。拒否するなら終了コード 1、 通すなら 0 で終わる。**読めないコマンドは通す**(hook の失敗でツールを止めない)。 -拒否するのは、コマンドの位置にある `sleep <数>` が次のどちらかに当たるときだけである。 +拒否するのは、コマンドの位置にある `sleep <引数>` が次のどちらかに当たるときだけである。 +`&` で終わる(バックグラウンドで動く)`sleep` は前景を待たせないため見ない。 -- `while` / `until` のループの本体(`do` と対応する `done` の間)にある -- 秒数が上限を超える +- `while` / `until` のループの本体(`do` と対応する `done` の間)にある(秒数が変数でも止める) +- 秒数が数で、上限を超える コメント・引用の中・ヒアドキュメントの本文は見ない。`bash -c` / `sh -c` / `zsh -c` / `eval` の 実行される引数は、取り出して同じ規則で見る。 @@ -21,6 +22,9 @@ # コマンドの位置を作る語。この後ろの語はコマンドとして読む OPENERS = {"do", "then", "else", "elif", "if", "while", "until", "{", "!", "time"} UNIT = {"": 1, "s": 1, "m": 60, "h": 3600, "d": 86400} +# 引数を取る shell のオプション。引数を読み飛ばして `-c` を探す +SHELL_OPTS_WITH_ARG = {"-o", "+o", "-O", "+O"} +ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=") HEREDOC = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2") @@ -80,16 +84,21 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) stack[-1] = "body" if stack[-1] == "cond" else "forbody" elif tok == "done" and stack: stack.pop() - elif tok == "sleep" and i + 1 < len(toks): + elif tok == "sleep" and i + 1 < len(toks) and not is_separator(toks[i + 1]): + background = i + 2 < len(toks) and toks[i + 2] == "&" sec = seconds(toks[i + 1]) - if sec is not None and (looping or sec > limit): + if not background and (looping or (sec is not None and sec > limit)): return True - cmd_pos = tok in OPENERS + # 先頭の代入語(`X=1 sleep 30`)の後ろもコマンドの位置のまま + cmd_pos = tok in OPENERS or bool(ASSIGN.match(tok)) # 実行される引数を取り出して同じ規則で見る if tok in SHELLS: j = i + 1 - while j < len(toks) and toks[j].startswith("-") and not is_separator(toks[j]): - if "c" in toks[j].lstrip("-") and not toks[j].startswith("--"): + while j < len(toks) and toks[j][:1] in "-+" and not is_separator(toks[j]): + if toks[j] in SHELL_OPTS_WITH_ARG: + j += 2 + continue + if toks[j].startswith("-") and "c" in toks[j].lstrip("-") and not toks[j].startswith("--"): if j + 1 < len(toks) and should_deny(toks[j + 1], limit, looping, depth + 1): return True break diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 420e33f45..138b95fa5 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -96,6 +96,10 @@ def bash(cmd, **extra): 'eval "sleep 30"', "echo start\nsleep 60\necho end", "sleep 1m", + "X=1 sleep 30", + "bash -O extglob -c 'sleep 30'", + "until [ -s f ]; do sleep $X; done", + "until [ -s f ]; do sleep $(cat n); done", ] ALLOW_SLEEP = [ @@ -115,6 +119,8 @@ def bash(cmd, **extra): "sleep $X", "ls -la", "while read l; do echo $l; done < f", + "sleep 30 & echo done", + "echo X=1 sleep 30", ] @@ -500,6 +506,14 @@ def test_context_agent_once_then_pass(tmp_path, state): assert denied(run(agent(tp, desc="実装: #829"), state)) +@pytest.mark.parametrize("desc", ["設計: v10.16.1 のリリース作業", "実装: リリース 2.0.3"]) +def test_context_version_is_not_issue_number(tmp_path, state, desc): + # 版数・小数を課題番号と読まない。番号が無ければ <課題番号> へ落ちる + tp = transcript(tmp_path, 250_000) + reason = denied(run(agent(tp, desc=desc, session="sver" + desc[:1]), state)) + assert reason and "/ndf:development-workflow <課題番号>" in reason + + @pytest.mark.parametrize("desc", ["検査: 829", "取り込み: 829", "仕上げ: 829"]) def test_context_agent_bare_issue_number_is_normalized(tmp_path, state, desc): # 現状固定: description の課題番号が # を付けない裸の番号でも、案内は diff --git a/plugins/ndf/scripts/token-guard.sh b/plugins/ndf/scripts/token-guard.sh index 48a4eaf7e..3420eb8a2 100755 --- a/plugins/ndf/scripts/token-guard.sh +++ b/plugins/ndf/scripts/token-guard.sh @@ -160,7 +160,8 @@ guard_context() { fi [ "$total" -gt "$limit" ] || exit 0 write_json "$mark" "$(jq -cn --arg k "$key" '{key:$k}')" - issues=$(printf '%s\n' "$words" | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ + # 版数・小数(v10.16.1 / 2.0.3)は課題番号ではないので先に取り除く + issues=$(printf '%s\n' "$words" | sed -E 's/[0-9]+(\.[0-9]+)+//g' | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ | grep -oE '[0-9]+' | sed 's/^/#/' | tr '\n' ' ') issues=${issues% } deny "会話の文脈が ${total} トークンで、上限 ${limit} を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: /ndf:development-workflow ${issues:-<課題番号>}(3 層で進めているなら、新しい会話で /goal に同じ 1 行を渡す)。<課題番号> のままなら、進めている課題の番号を補って示す。このまま続けると利用者が決めたら、同じ起動をもう一度行うと 1 度だけ通る。規約: ${CONTEXT_DOC}(止めるなら NDF_CONTEXT_GUARD=0、上限は NDF_CONTEXT_LIMIT)" diff --git a/plugins/ndf/skills/development-workflow/references/waiting.md b/plugins/ndf/skills/development-workflow/references/waiting.md index bd8311598..6432f446d 100644 --- a/plugins/ndf/skills/development-workflow/references/waiting.md +++ b/plugins/ndf/skills/development-workflow/references/waiting.md @@ -64,11 +64,11 @@ | 判定 | 止める条件 | 止め方 | 上限を変える | | --- | --- | --- | --- | -| sleep | 前景の Bash で、コマンドの位置の `sleep <数>` が `while` / `until` のループの本体にあるか、秒数が上限を超える。コメント・引用・ヒアドキュメントの本文は見ず、`bash -c` / `sh -c` / `eval` の中身は見る | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | +| sleep | 前景の Bash で、コマンドの位置(先頭の代入語 `X=1` の後ろを含む)の `sleep` が `while` / `until` のループの本体にある(秒数が変数でも止める)か、秒数が上限を超える。コメント・引用・ヒアドキュメントの本文は見ず、`bash -c` / `sh -c` / `eval` の中身は見る | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | | 連続 Read | 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらないまま上限の回数に達する | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | - **止めないもの:** `run_in_background: true` の Bash、`Monitor` の中の `sleep`、ループの本体の - 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep` + 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、`&` で終わるバックグラウンドの `sleep` - **判定が失敗したときは止めない**(入力が読めない・`jq` や `python3` が無い・控えを書けない) - 同じ hook が、文脈が上限を超えた conductor の工程の起動も止める([context-window.md](context-window.md) の「上限を超えたら hook が止める」) From fba6e05e0a1d51b71e344ab4da3b142b20869c21 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:11:26 +0000 Subject: [PATCH 25/30] =?UTF-8?q?Fix:=20#844=20=E6=9C=80=E7=B5=82=E3=82=B9?= =?UTF-8?q?=E3=82=A4=E3=83=BC=E3=83=97=E3=81=AE=E6=8C=87=E6=91=98=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=EF=BC=88sleep=20=E5=88=A4=E5=AE=9A=E3=81=AE?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E5=AE=9F=E8=A1=8C=E3=83=BB=E5=86=8D=E5=B8=B0?= =?UTF-8?q?=E3=81=AE=E4=BD=8D=E7=BD=AE=E3=83=BB=E7=B5=90=E5=90=88=E5=BD=A2?= =?UTF-8?q?=E3=81=AE=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 背景実行は sleep の引数の後ろから次のコマンド境界までの `&` で判定する(`sleep 30 >/tmp/x &` を通す)。`2>&1` / `>&` / `&>` の `&` は背景と読まない - bash / sh / zsh / dash -c と eval の中身を見るのはコマンドの位置の語だけにする(`echo bash -c ...` を止めない)。`timeout 590` / `nohup` / `env` などの前置きの後ろもコマンドの位置とする - 末尾が o / O の結合形のオプション(`-euo pipefail`)の引数を読み飛ばして `-c` を探す - waiting.md の説明を判定に合わせる Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- plugins/ndf/scripts/lib/token_guard_sleep.py | 43 ++++++++++++++----- plugins/ndf/scripts/tests/test_token_guard.py | 12 ++++++ .../references/waiting.md | 4 +- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/plugins/ndf/scripts/lib/token_guard_sleep.py b/plugins/ndf/scripts/lib/token_guard_sleep.py index ee715f351..b97653de6 100644 --- a/plugins/ndf/scripts/lib/token_guard_sleep.py +++ b/plugins/ndf/scripts/lib/token_guard_sleep.py @@ -4,13 +4,16 @@ 通すなら 0 で終わる。**読めないコマンドは通す**(hook の失敗でツールを止めない)。 拒否するのは、コマンドの位置にある `sleep <引数>` が次のどちらかに当たるときだけである。 -`&` で終わる(バックグラウンドで動く)`sleep` は前景を待たせないため見ない。 +`&` で終わる(バックグラウンドで動く)`sleep` は前景を待たせないため見ない。`&` の判定は +sleep の引数の後ろから次のコマンド境界までを見る(`sleep 30 >/tmp/x &` も背景)。リダイレクトの +`>&` / `2>&1` / `&>` の `&` は背景と読まない。 - `while` / `until` のループの本体(`do` と対応する `done` の間)にある(秒数が変数でも止める) - 秒数が数で、上限を超える -コメント・引用の中・ヒアドキュメントの本文は見ない。`bash -c` / `sh -c` / `zsh -c` / `eval` の -実行される引数は、取り出して同じ規則で見る。 +コメント・引用の中・ヒアドキュメントの本文は見ない。コマンドの位置にある `bash -c` / `sh -c` / +`zsh -c` / `dash -c` / `eval` の実行される引数は、取り出して同じ規則で見る(`echo bash -c ...` の +ような引数の中の語は見ない)。`timeout 590` / `nohup` / `env` などの前置きの後ろもコマンドの位置とする。 """ from __future__ import annotations @@ -22,8 +25,10 @@ # コマンドの位置を作る語。この後ろの語はコマンドとして読む OPENERS = {"do", "then", "else", "elif", "if", "while", "until", "{", "!", "time"} UNIT = {"": 1, "s": 1, "m": 60, "h": 3600, "d": 86400} -# 引数を取る shell のオプション。引数を読み飛ばして `-c` を探す -SHELL_OPTS_WITH_ARG = {"-o", "+o", "-O", "+O"} +# 後ろの語をコマンドとして実行する前置き。オプションと数の引数(`timeout 590` / `nice -n 10`)を読み飛ばす +WRAPPERS = {"exec", "command", "nohup", "env", "nice", "timeout"} +# 引数を取る shell のオプション(`-o` / `+O` と、末尾が o / O の結合形 `-euo`)。引数を読み飛ばして `-c` を探す +SHELL_OPT_WITH_ARG = re.compile(r"[-+][A-Za-bd-z]*[oO]") ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=") HEREDOC = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2") @@ -60,6 +65,19 @@ def is_separator(tok: str) -> bool: return bool(tok) and all(c in ";&|()\n" for c in tok) +def is_background(toks: list[str], j: int) -> bool: + """toks[j] から次のコマンド境界までに、背景実行の `&` があるか。""" + while j < len(toks): + if is_separator(toks[j]): + if toks[j] != "&": + return False + redirect = toks[j - 1] in (">", "<") or (j + 1 < len(toks) and toks[j + 1] == ">") + if not redirect: + return True + j += 1 + return False + + def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) -> bool: if depth > 5: return False @@ -70,6 +88,7 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) i = 0 while i < len(toks): tok = toks[i] + at_cmd = cmd_pos looping = in_loop or "body" in stack if is_separator(tok): cmd_pos = True @@ -85,17 +104,21 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) elif tok == "done" and stack: stack.pop() elif tok == "sleep" and i + 1 < len(toks) and not is_separator(toks[i + 1]): - background = i + 2 < len(toks) and toks[i + 2] == "&" sec = seconds(toks[i + 1]) - if not background and (looping or (sec is not None and sec > limit)): + if (looping or (sec is not None and sec > limit)) and not is_background(toks, i + 2): return True + elif tok in WRAPPERS: + while i + 1 < len(toks) and (toks[i + 1][:1] == "-" or seconds(toks[i + 1]) is not None): + i += 1 + i += 1 + continue # 先頭の代入語(`X=1 sleep 30`)の後ろもコマンドの位置のまま cmd_pos = tok in OPENERS or bool(ASSIGN.match(tok)) # 実行される引数を取り出して同じ規則で見る - if tok in SHELLS: + if at_cmd and tok in SHELLS: j = i + 1 while j < len(toks) and toks[j][:1] in "-+" and not is_separator(toks[j]): - if toks[j] in SHELL_OPTS_WITH_ARG: + if SHELL_OPT_WITH_ARG.fullmatch(toks[j]): j += 2 continue if toks[j].startswith("-") and "c" in toks[j].lstrip("-") and not toks[j].startswith("--"): @@ -103,7 +126,7 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) return True break j += 1 - elif tok == "eval": + elif at_cmd and tok == "eval": j, words = i + 1, [] while j < len(toks) and not is_separator(toks[j]): words.append(toks[j]) diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index 138b95fa5..b50760b3f 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -100,6 +100,13 @@ def bash(cmd, **extra): "bash -O extglob -c 'sleep 30'", "until [ -s f ]; do sleep $X; done", "until [ -s f ]; do sleep $(cat n); done", + "bash -euo pipefail -c 'sleep 100'", + "bash -eo pipefail -c 'sleep 100'", + "sleep 30 2>&1 | tee x", + "sleep 30 &>/dev/null", + "sleep 30 >&2", + "nohup sleep 30", + "echo a; bash -c 'sleep 30'", ] ALLOW_SLEEP = [ @@ -121,6 +128,11 @@ def bash(cmd, **extra): "while read l; do echo $l; done < f", "sleep 30 & echo done", "echo X=1 sleep 30", + "sleep 30 >/tmp/x &", + "sleep 100 >/dev/null &", + "sleep 30 >>x 2>&1 & echo started", + "echo bash -c 'sleep 30'", + "printf '%s' eval sleep 30", ] diff --git a/plugins/ndf/skills/development-workflow/references/waiting.md b/plugins/ndf/skills/development-workflow/references/waiting.md index 6432f446d..d1fcbde71 100644 --- a/plugins/ndf/skills/development-workflow/references/waiting.md +++ b/plugins/ndf/skills/development-workflow/references/waiting.md @@ -64,11 +64,11 @@ | 判定 | 止める条件 | 止め方 | 上限を変える | | --- | --- | --- | --- | -| sleep | 前景の Bash で、コマンドの位置(先頭の代入語 `X=1` の後ろを含む)の `sleep` が `while` / `until` のループの本体にある(秒数が変数でも止める)か、秒数が上限を超える。コメント・引用・ヒアドキュメントの本文は見ず、`bash -c` / `sh -c` / `eval` の中身は見る | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | +| sleep | 前景の Bash で、コマンドの位置(先頭の代入語 `X=1` と、`timeout 590` / `nohup` / `env` などの前置きの後ろを含む)の `sleep` が `while` / `until` のループの本体にある(秒数が変数でも止める)か、秒数が上限を超える。コメント・引用・ヒアドキュメントの本文は見ず、コマンドの位置にある `bash -c` / `sh -c` / `zsh -c` / `dash -c` / `eval` の中身は見る(`echo bash -c ...` のような引数の中の語は見ない) | `NDF_SLEEP_GUARD=0` | `NDF_SLEEP_MAX_SEC`(既定 5) | | 連続 Read | 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらないまま上限の回数に達する | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | - **止めないもの:** `run_in_background: true` の Bash、`Monitor` の中の `sleep`、ループの本体の - 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、`&` で終わるバックグラウンドの `sleep` + 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、同じコマンドの末尾が `&` のバックグラウンドの `sleep`(`sleep 30 >/tmp/x &` のようにリダイレクトを挟んでもよい。`2>&1` / `&>` の `&` は背景と読まない) - **判定が失敗したときは止めない**(入力が読めない・`jq` や `python3` が無い・控えを書けない) - 同じ hook が、文脈が上限を超えた conductor の工程の起動も止める([context-window.md](context-window.md) の「上限を超えたら hook が止める」) From 9ec21b3bf1941fef79314be6b947470a6c0149b8 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:20:22 +0000 Subject: [PATCH 26/30] =?UTF-8?q?Fix:=20#844=20sleep=20=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E3=81=A7=E5=9B=B2=E3=81=BF=E3=81=AE=E8=A4=87=E5=90=88=E3=82=B3?= =?UTF-8?q?=E3=83=9E=E3=83=B3=E3=83=89=E3=81=AE=E8=83=8C=E6=99=AF=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=82=92=E9=80=9A=E3=81=97=E3=80=81=E8=AA=B2=E9=A1=8C?= =?UTF-8?q?=E7=95=AA=E5=8F=B7=E6=8A=BD=E5=87=BA=E3=81=A7=E6=97=A5=E4=BB=98?= =?UTF-8?q?=E3=82=92=E9=99=A4=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (sleep 30) & / { sleep 30; } & / while ...; do sleep 1; done & / sleep 30 && echo x & を 背景として通す。前景のまま待つ形は引き続き止める - 課題番号の抽出で、ハイフン区切りの日付(2026-09-23)も版数と同じく取り除く - waiting.md の止めないものの説明を判定に合わせる Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- plugins/ndf/scripts/lib/token_guard_sleep.py | 69 ++++++++++++++++--- plugins/ndf/scripts/tests/test_token_guard.py | 24 ++++++- plugins/ndf/scripts/token-guard.sh | 4 +- .../references/waiting.md | 2 +- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/plugins/ndf/scripts/lib/token_guard_sleep.py b/plugins/ndf/scripts/lib/token_guard_sleep.py index b97653de6..8acef2ed4 100644 --- a/plugins/ndf/scripts/lib/token_guard_sleep.py +++ b/plugins/ndf/scripts/lib/token_guard_sleep.py @@ -5,8 +5,10 @@ 拒否するのは、コマンドの位置にある `sleep <引数>` が次のどちらかに当たるときだけである。 `&` で終わる(バックグラウンドで動く)`sleep` は前景を待たせないため見ない。`&` の判定は -sleep の引数の後ろから次のコマンド境界までを見る(`sleep 30 >/tmp/x &` も背景)。リダイレクトの -`>&` / `2>&1` / `&>` の `&` は背景と読まない。 +sleep の引数の後ろから、sleep を含むリスト(`&&` / `||` / `|` でつながる範囲)の終わりまでを見る +(`sleep 30 >/tmp/x &` も背景)。sleep を囲む `( )` / `{ }` / ループ / `if` があれば、その閉じの +後ろの `&` まで見る(`(sleep 30) &`・`while ...; do sleep 1; done &` も背景)。リダイレクトの +`>&` / `2>&1` / `&>` の `&` は背景と読まない。字句による近似で、`case` の囲みは数えない。 - `while` / `until` のループの本体(`do` と対応する `done` の間)にある(秒数が変数でも止める) - 秒数が数で、上限を超える @@ -30,6 +32,9 @@ # 引数を取る shell のオプション(`-o` / `+O` と、末尾が o / O の結合形 `-euo`)。引数を読み飛ばして `-c` を探す SHELL_OPT_WITH_ARG = re.compile(r"[-+][A-Za-bd-z]*[oO]") ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=") +# 複合コマンドを開く語・閉じる語(コマンドの位置にあるときだけ)。`( )` は区切りの文字で数える +GROUP_OPEN = {"{", "while", "until", "for", "select", "if"} +GROUP_CLOSE = {"}", "done", "fi"} HEREDOC = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2") @@ -65,15 +70,51 @@ def is_separator(tok: str) -> bool: return bool(tok) and all(c in ";&|()\n" for c in tok) -def is_background(toks: list[str], j: int) -> bool: - """toks[j] から次のコマンド境界までに、背景実行の `&` があるか。""" +def is_background(toks: list[str], j: int, enclosing: int) -> bool: + """toks[j] から sleep を含むリストの終わりまでに、背景実行の `&` があるか。 + + `enclosing` は sleep を囲む複合コマンド(`( )` / `{ }` / ループ / `if`)の数。囲みの中では + `;` で終わっても囲みの閉じまで進み、閉じの後ろの `&` を見る(`(sleep 30) &` も背景)。 + """ + depth, ended, cmd_pos = 0, False, False while j < len(toks): - if is_separator(toks[j]): - if toks[j] != "&": - return False - redirect = toks[j - 1] in (">", "<") or (j + 1 < len(toks) and toks[j + 1] == ">") - if not redirect: - return True + tok = toks[j] + if tok in ("&&", "||", "|", "|&"): + cmd_pos = True + elif is_separator(tok): + cmd_pos = True + redirect = tok == "&" and (toks[j - 1] in (">", "<") or (j + 1 < len(toks) and toks[j + 1] == ">")) + for c in tok: + if c == "(": + depth += 1 + elif c == ")": + if depth: + depth -= 1 + elif enclosing: + enclosing, ended = enclosing - 1, False + else: + return False + elif depth: + continue + elif c in ";\n": + if not enclosing: + return False + ended = True + elif c == "&" and not redirect: + if not ended: + return True + ended = True + else: + if cmd_pos and tok in GROUP_OPEN: + depth += 1 + elif cmd_pos and tok in GROUP_CLOSE: + if depth: + depth -= 1 + elif enclosing: + enclosing, ended = enclosing - 1, False + else: + return False + cmd_pos = tok in OPENERS j += 1 return False @@ -84,6 +125,7 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) toks = tokens(strip_heredocs(text)) # 各要素は "cond"(while/until の条件)/ "body"(while/until の本体)/ "for" / "forbody" stack: list[str] = [] + groups = 0 # いまの位置を囲む複合コマンドの数 cmd_pos = True i = 0 while i < len(toks): @@ -91,10 +133,15 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) at_cmd = cmd_pos looping = in_loop or "body" in stack if is_separator(tok): + groups = max(0, groups + tok.count("(") - tok.count(")")) cmd_pos = True i += 1 continue if cmd_pos: + if tok in GROUP_OPEN: + groups += 1 + elif tok in GROUP_CLOSE: + groups = max(0, groups - 1) if tok in ("while", "until"): stack.append("cond") elif tok in ("for", "select"): @@ -105,7 +152,7 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) stack.pop() elif tok == "sleep" and i + 1 < len(toks) and not is_separator(toks[i + 1]): sec = seconds(toks[i + 1]) - if (looping or (sec is not None and sec > limit)) and not is_background(toks, i + 2): + if (looping or (sec is not None and sec > limit)) and not is_background(toks, i + 2, groups): return True elif tok in WRAPPERS: while i + 1 < len(toks) and (toks[i + 1][:1] == "-" or seconds(toks[i + 1]) is not None): diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index b50760b3f..ddcf595dd 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -107,6 +107,13 @@ def bash(cmd, **extra): "sleep 30 >&2", "nohup sleep 30", "echo a; bash -c 'sleep 30'", + # 囲みの複合コマンドが前景のまま待つ形 + "(sleep 30)", + "{ sleep 30; }", + "(sleep 30) && echo", + "while test ! -s f; do sleep 1; done; echo x", + "{ sleep 30; (x) & }", + "(sleep 30);(x) &", ] ALLOW_SLEEP = [ @@ -133,6 +140,14 @@ def bash(cmd, **extra): "sleep 30 >>x 2>&1 & echo started", "echo bash -c 'sleep 30'", "printf '%s' eval sleep 30", + # sleep を囲む複合コマンドや and-or リスト全体が末尾の & で背景になる形 + "(sleep 30) &", + "(sleep 30)&", + "{ sleep 30; } &", + "while test ! -s f; do sleep 1; done &", + "if true; then sleep 30; fi &", + "( (sleep 30) ) & echo started", + "sleep 30 && echo x &", ] @@ -518,7 +533,7 @@ def test_context_agent_once_then_pass(tmp_path, state): assert denied(run(agent(tp, desc="実装: #829"), state)) -@pytest.mark.parametrize("desc", ["設計: v10.16.1 のリリース作業", "実装: リリース 2.0.3"]) +@pytest.mark.parametrize("desc", ["設計: v10.16.1 のリリース作業", "実装: リリース 2.0.3", "設計: 2026-09-23 の作業"]) def test_context_version_is_not_issue_number(tmp_path, state, desc): # 版数・小数を課題番号と読まない。番号が無ければ <課題番号> へ落ちる tp = transcript(tmp_path, 250_000) @@ -537,6 +552,13 @@ def test_context_agent_bare_issue_number_is_normalized(tmp_path, state, desc): assert reason and "/ndf:development-workflow #829" in reason +def test_context_date_is_not_issue_number(tmp_path, state): + # ハイフン区切りの日付を #2026 #09 #23 と読まず、# 付きの番号だけを示す + tp = transcript(tmp_path, 250_000) + reason = denied(run(agent(tp, desc="設計: 2026-09-23 の作業 #844", session="sdate"), state)) + assert reason and "/ndf:development-workflow #844(" in reason + + def test_context_guard_env(tmp_path, state): tp = transcript(tmp_path, 250_000) assert denied(run(skill(tp), state, {"NDF_CONTEXT_GUARD": "0"})) is None diff --git a/plugins/ndf/scripts/token-guard.sh b/plugins/ndf/scripts/token-guard.sh index 3420eb8a2..b6507b929 100755 --- a/plugins/ndf/scripts/token-guard.sh +++ b/plugins/ndf/scripts/token-guard.sh @@ -160,8 +160,8 @@ guard_context() { fi [ "$total" -gt "$limit" ] || exit 0 write_json "$mark" "$(jq -cn --arg k "$key" '{key:$k}')" - # 版数・小数(v10.16.1 / 2.0.3)は課題番号ではないので先に取り除く - issues=$(printf '%s\n' "$words" | sed -E 's/[0-9]+(\.[0-9]+)+//g' | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ + # 版数・小数・日付(v10.16.1 / 2.0.3 / 2026-09-23)は課題番号ではないので先に取り除く + issues=$(printf '%s\n' "$words" | sed -E 's/[0-9]+([.-][0-9]+)+//g' | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ | grep -oE '[0-9]+' | sed 's/^/#/' | tr '\n' ' ') issues=${issues% } deny "会話の文脈が ${total} トークンで、上限 ${limit} を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: /ndf:development-workflow ${issues:-<課題番号>}(3 層で進めているなら、新しい会話で /goal に同じ 1 行を渡す)。<課題番号> のままなら、進めている課題の番号を補って示す。このまま続けると利用者が決めたら、同じ起動をもう一度行うと 1 度だけ通る。規約: ${CONTEXT_DOC}(止めるなら NDF_CONTEXT_GUARD=0、上限は NDF_CONTEXT_LIMIT)" diff --git a/plugins/ndf/skills/development-workflow/references/waiting.md b/plugins/ndf/skills/development-workflow/references/waiting.md index d1fcbde71..cda4e277b 100644 --- a/plugins/ndf/skills/development-workflow/references/waiting.md +++ b/plugins/ndf/skills/development-workflow/references/waiting.md @@ -68,7 +68,7 @@ | 連続 Read | 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらないまま上限の回数に達する | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | - **止めないもの:** `run_in_background: true` の Bash、`Monitor` の中の `sleep`、ループの本体の - 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、同じコマンドの末尾が `&` のバックグラウンドの `sleep`(`sleep 30 >/tmp/x &` のようにリダイレクトを挟んでもよい。`2>&1` / `&>` の `&` は背景と読まない) + 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、末尾の `&` でバックグラウンドになる `sleep`(`sleep 30 >/tmp/x &` のようにリダイレクトを挟んでもよい。`sleep 30 && echo x &` のようなリストや、`(sleep 30) &`・`{ sleep 30; } &`・`while ...; do sleep 1; done &` のように sleep を囲む複合コマンドの全体が背景になる形も含む。`2>&1` / `&>` の `&` は背景と読まない) - **判定が失敗したときは止めない**(入力が読めない・`jq` や `python3` が無い・控えを書けない) - 同じ hook が、文脈が上限を超えた conductor の工程の起動も止める([context-window.md](context-window.md) の「上限を超えたら hook が止める」) From 01bb5d5cbc657d9ac3597ca5c871586c7421ad02 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:28:54 +0000 Subject: [PATCH 27/30] =?UTF-8?q?Fix:=20#844=20bash=20-c=20/=20eval=20?= =?UTF-8?q?=E3=81=AE=E5=A4=96=E5=81=B4=E3=81=8C=E8=83=8C=E6=99=AF=E3=81=AA?= =?UTF-8?q?=E3=82=89=E4=B8=AD=E8=BA=AB=E3=82=92=E8=A6=8B=E3=81=9A=E3=80=81?= =?UTF-8?q?=E8=AA=B2=E9=A1=8C=E7=95=AA=E5=8F=B7=E6=8A=BD=E5=87=BA=E3=81=A7?= =?UTF-8?q?=E7=AF=84=E5=9B=B2=E3=82=92=E6=AE=8B=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - token_guard_sleep.py: bash -c / eval の中身を再帰検査する前に、既存の is_background で外側の & を判定する - token-guard.sh: 日付(YYYY-MM-DD)だけを先に除き、版数は [0-9]+(\.[0-9]+)+ に戻す。#829-830 / 829-830 は #829 #830 として案内する - test_token_guard.py: 背景の bash -c / eval と、版数・日付・範囲を含む description の抽出結果を固定する - waiting.md: 止めないものに bash -c / eval の外側が背景になる形を加える Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- plugins/ndf/scripts/lib/token_guard_sleep.py | 8 ++++--- plugins/ndf/scripts/tests/test_token_guard.py | 22 +++++++++++++++++++ plugins/ndf/scripts/token-guard.sh | 5 +++-- .../references/waiting.md | 2 +- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/plugins/ndf/scripts/lib/token_guard_sleep.py b/plugins/ndf/scripts/lib/token_guard_sleep.py index 8acef2ed4..cd4536b52 100644 --- a/plugins/ndf/scripts/lib/token_guard_sleep.py +++ b/plugins/ndf/scripts/lib/token_guard_sleep.py @@ -15,7 +15,7 @@ コメント・引用の中・ヒアドキュメントの本文は見ない。コマンドの位置にある `bash -c` / `sh -c` / `zsh -c` / `dash -c` / `eval` の実行される引数は、取り出して同じ規則で見る(`echo bash -c ...` の -ような引数の中の語は見ない)。`timeout 590` / `nohup` / `env` などの前置きの後ろもコマンドの位置とする。 +ような引数の中の語は見ない)。外側が `&` で背景になる形(`bash -c 'sleep 30' &`)は中身を見ない。`timeout 590` / `nohup` / `env` などの前置きの後ろもコマンドの位置とする。 """ from __future__ import annotations @@ -169,7 +169,9 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) j += 2 continue if toks[j].startswith("-") and "c" in toks[j].lstrip("-") and not toks[j].startswith("--"): - if j + 1 < len(toks) and should_deny(toks[j + 1], limit, looping, depth + 1): + # `bash -c 'sleep 30' &` は外側ごと背景で動くので中身を見ない + if (j + 1 < len(toks) and not is_background(toks, j + 2, groups) + and should_deny(toks[j + 1], limit, looping, depth + 1)): return True break j += 1 @@ -178,7 +180,7 @@ def should_deny(text: str, limit: float, in_loop: bool = False, depth: int = 0) while j < len(toks) and not is_separator(toks[j]): words.append(toks[j]) j += 1 - if should_deny(" ".join(words), limit, looping, depth + 1): + if not is_background(toks, j, groups) and should_deny(" ".join(words), limit, looping, depth + 1): return True i += 1 return False diff --git a/plugins/ndf/scripts/tests/test_token_guard.py b/plugins/ndf/scripts/tests/test_token_guard.py index ddcf595dd..57f1a9073 100644 --- a/plugins/ndf/scripts/tests/test_token_guard.py +++ b/plugins/ndf/scripts/tests/test_token_guard.py @@ -114,6 +114,9 @@ def bash(cmd, **extra): "while test ! -s f; do sleep 1; done; echo x", "{ sleep 30; (x) & }", "(sleep 30);(x) &", + # 外側の & が別のコマンドのもの + "bash -c 'sleep 30'; x &", + "eval 'sleep 30'; x &", ] ALLOW_SLEEP = [ @@ -148,6 +151,11 @@ def bash(cmd, **extra): "if true; then sleep 30; fi &", "( (sleep 30) ) & echo started", "sleep 30 && echo x &", + # bash -c / eval の外側が背景になる形 + "bash -c 'sleep 30' &", + "bash -c 'sleep 30' >/tmp/x 2>&1 & echo started", + "eval 'sleep 30' &", + "(bash -c 'sleep 30') &", ] @@ -559,6 +567,20 @@ def test_context_date_is_not_issue_number(tmp_path, state): assert reason and "/ndf:development-workflow #844(" in reason +@pytest.mark.parametrize("desc, expect", [ + ("設計: v10.16.1 のリリース #844", "#844("), + ("設計: 2026-09-23 の作業 v10.16.1", "<課題番号>("), + ("検査: #829-830", "#829 #830("), + ("設計: 829-830 の作業", "#829 #830("), + ("実装: 2026-09-23 に #829-830 を v10.16.1 へ", "#829 #830("), +]) +def test_context_issue_extraction(tmp_path, state, desc, expect): + # 版数・日付を課題番号と読まず、範囲は両端の番号として案内する + tp = transcript(tmp_path, 250_000) + reason = denied(run(agent(tp, desc=desc, session="sx" + str(abs(hash(desc)))), state)) + assert reason and "/ndf:development-workflow " + expect in reason, reason + + def test_context_guard_env(tmp_path, state): tp = transcript(tmp_path, 250_000) assert denied(run(skill(tp), state, {"NDF_CONTEXT_GUARD": "0"})) is None diff --git a/plugins/ndf/scripts/token-guard.sh b/plugins/ndf/scripts/token-guard.sh index b6507b929..f9e597d51 100755 --- a/plugins/ndf/scripts/token-guard.sh +++ b/plugins/ndf/scripts/token-guard.sh @@ -160,8 +160,9 @@ guard_context() { fi [ "$total" -gt "$limit" ] || exit 0 write_json "$mark" "$(jq -cn --arg k "$key" '{key:$k}')" - # 版数・小数・日付(v10.16.1 / 2.0.3 / 2026-09-23)は課題番号ではないので先に取り除く - issues=$(printf '%s\n' "$words" | sed -E 's/[0-9]+([.-][0-9]+)+//g' | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ + # 日付・版数・小数(2026-09-23 / v10.16.1 / 2.0.3)は課題番号ではないので先に取り除く。 + # 範囲(#829-830 / 829-830)は残し、#829 #830 として案内する + issues=$(printf '%s\n' "$words" | sed -E 's/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}//g; s/[0-9]+(\.[0-9]+)+//g' | grep -oE '(^|[^0-9A-Za-z_/])#?[0-9]+\b' \ | grep -oE '[0-9]+' | sed 's/^/#/' | tr '\n' ' ') issues=${issues% } deny "会話の文脈が ${total} トークンで、上限 ${limit} を超えた。この工程は新しい会話で始める。利用者へ次の 1 行を示して応答を終える: /ndf:development-workflow ${issues:-<課題番号>}(3 層で進めているなら、新しい会話で /goal に同じ 1 行を渡す)。<課題番号> のままなら、進めている課題の番号を補って示す。このまま続けると利用者が決めたら、同じ起動をもう一度行うと 1 度だけ通る。規約: ${CONTEXT_DOC}(止めるなら NDF_CONTEXT_GUARD=0、上限は NDF_CONTEXT_LIMIT)" diff --git a/plugins/ndf/skills/development-workflow/references/waiting.md b/plugins/ndf/skills/development-workflow/references/waiting.md index cda4e277b..a42c5922d 100644 --- a/plugins/ndf/skills/development-workflow/references/waiting.md +++ b/plugins/ndf/skills/development-workflow/references/waiting.md @@ -68,7 +68,7 @@ | 連続 Read | 同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらないまま上限の回数に達する | `NDF_READ_REPEAT_GUARD=0` | `NDF_READ_REPEAT_LIMIT`(既定 3) | - **止めないもの:** `run_in_background: true` の Bash、`Monitor` の中の `sleep`、ループの本体の - 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、末尾の `&` でバックグラウンドになる `sleep`(`sleep 30 >/tmp/x &` のようにリダイレクトを挟んでもよい。`sleep 30 && echo x &` のようなリストや、`(sleep 30) &`・`{ sleep 30; } &`・`while ...; do sleep 1; done &` のように sleep を囲む複合コマンドの全体が背景になる形も含む。`2>&1` / `&>` の `&` は背景と読まない) + 外の上限以下の `sleep`、`for` のループの中の上限以下の `sleep`、末尾の `&` でバックグラウンドになる `sleep`(`sleep 30 >/tmp/x &` のようにリダイレクトを挟んでもよい。`sleep 30 && echo x &` のようなリストや、`(sleep 30) &`・`{ sleep 30; } &`・`while ...; do sleep 1; done &` のように sleep を囲む複合コマンドの全体が背景になる形も含む。`bash -c 'sleep 30' &`・`eval 'sleep 30' &` のように `bash -c` / `eval` の外側が背景になる形も含む。`2>&1` / `&>` の `&` は背景と読まない) - **判定が失敗したときは止めない**(入力が読めない・`jq` や `python3` が無い・控えを書けない) - 同じ hook が、文脈が上限を超えた conductor の工程の起動も止める([context-window.md](context-window.md) の「上限を超えたら hook が止める」) From 65f2a54da4a2beb97453b48ce51df296dc848b4b Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:45:23 +0000 Subject: [PATCH 28/30] =?UTF-8?q?Docs:=20#829=20#830=20=E3=81=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E3=83=BB=E8=A8=AD=E8=A8=88=E3=83=BB=E6=B1=BA=E5=AE=9A?= =?UTF-8?q?=E3=83=BB=E8=A8=88=E7=94=BB=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=B8=E3=81=BE=E3=81=A8=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 要求・設計・決定の記録・実装計画の 4 本を、現行の実装と一致する確定仕様 docs/specifications/ndf-token-waits-and-context-cut.md へ書き直した。元の 4 本は issues/old/milestone-26-token-waits/ へ退避した。 - 設計の「未確認」の決着(サブエージェントは agent_id で見分ける・PreToolUse の時点で その呼び出しの assistant 行は未記録・通知が 2 回届くこと)を実装のとおりに書いた - sleep の判定の背景の扱い(& で背景になる形・dash -c)など、実装レビューで決まった規則を反映した - token-guard-stages.txt の「正」の参照先を確定仕様へ向け直した Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- docs/specifications/README.md | 1 + .../ndf-token-waits-and-context-cut.md | 373 ++++++++++++++++++ issues/old/README.md | 1 + .../issue-829-830-design-decisions.md | 0 .../issue-829-830-design.md | 0 .../issue-829-830-implementation-plan.md | 0 .../issue-829-830-requirements.md | 0 .../ndf/scripts/lib/token-guard-stages.txt | 4 +- 8 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 docs/specifications/ndf-token-waits-and-context-cut.md rename issues/{ => old/milestone-26-token-waits}/issue-829-830-design-decisions.md (100%) rename issues/{ => old/milestone-26-token-waits}/issue-829-830-design.md (100%) rename issues/{ => old/milestone-26-token-waits}/issue-829-830-implementation-plan.md (100%) rename issues/{ => old/milestone-26-token-waits}/issue-829-830-requirements.md (100%) diff --git a/docs/specifications/README.md b/docs/specifications/README.md index 6844855a0..eb5ace2e2 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -27,5 +27,6 @@ | [ndf-execution-plan-and-parallel-capacity.md](ndf-execution-plan-and-parallel-capacity.md) | 並列の実行計画(依存を工程の対で書く・重なりの 3 区分・開いている間はコミットしない)、マイルストーンの組、メモリで見る本数(`parallel-measure.py`)。手順は `issue-plan-strategy` と `development-workflow` の `references/` が正 | | [ndf-instruction-files-check.md](ndf-instruction-files-check.md) | エージェント向け指示書の検査(`instructions-check.py`)。宣言 `.ndf/instructions.json` で決まる判定の強さ、即時読み込みと出た版の段落の判定、扱いの印、観点の調べ直し。呼び方と宣言の書き方は `release` の `references/instruction-files.md` が正 | | [test-monitor-env-isolation.md](test-monitor-env-isolation.md) | テストの実行中だけ監視の上限を指す環境変数(接頭辞 `MONITOR_`)をリポジトリの根の共通の前提で外すこと、外す時点と戻す時点、根の設定ファイルで基準のディレクトリを固定すること | +| [ndf-token-waits-and-context-cut.md](ndf-token-waits-and-context-cut.md) | 待つ間の問い合わせ(前景の `sleep` の待ち・変わらないファイルの読み直し)と、文脈が上限を超えた conductor の工程の起動を止める hook(`token-guard.sh`)の判定・記録の形・入出力の契約、引き継ぎの 1 行、4 ランタイムの扱い。規約は `development-workflow` の `references/waiting.md` と `context-window.md` が正 | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/ndf-token-waits-and-context-cut.md b/docs/specifications/ndf-token-waits-and-context-cut.md new file mode 100644 index 000000000..3fabf6622 --- /dev/null +++ b/docs/specifications/ndf-token-waits-and-context-cut.md @@ -0,0 +1,373 @@ +# 待つ間の問い合わせを止め、conductor の会話を工程の切れ目で切る + +Claude Code の PreToolUse hook(`plugins/ndf/scripts/token-guard.sh`)が、待つ間に文脈を +読み直す呼び出し(前景の `sleep` の待ちと、変わらないファイルの読み直し)と、文脈が上限を +超えた conductor が工程へ入る起動を止める。止めたときは理由の欄に代わりの手段を示す。 +この文書は、判定の条件・記録の形・入出力の契約と、それぞれをそう決めた理由を残す。 + +**待ち方と会話の切り方の規約は Skill の文書が正である。** 許す待ち方・待つ相手ごとの手・ +新しい会話で状態を戻す手順をここへ書き写さない。 + +| 何を読むか | 正本 | +| --- | --- | +| 待ちの費用、許す待ち方と禁じる待ち方、待つ相手ごとの手、hook の止め方、4 ランタイムの扱い | `plugins/ndf/skills/development-workflow/references/waiting.md` | +| 会話を切る 4 つの切れ目、上限を超えたら hook が止めること、新しい会話で戻す手順 | `plugins/ndf/skills/development-workflow/references/context-window.md` の「context window は工程の切れ目で切る」「上限を超えたら hook が止める」「新しい会話で戻す」 | +| conductor が引き継ぎの 1 行を出す時点 | `plugins/ndf/skills/development-workflow/SKILL.md`(「工程は 1 つの context window で通し切らなくてよい」の段落) | +| supervisor と worker が待ち方に従う規則 | `plugins/ndf/skills/development-workflow/references/agent-layers.md` | +| `sleep` の判定の字句の規則 | `plugins/ndf/scripts/lib/token_guard_sleep.py` の docstring | + +## 概要 + +**例(#829)。** サブエージェントが `codex exec` を背景で起動し、`sleep 60 && tail -5 /tmp/x.log` +を 30 回繰り返すと、30 回とも文脈の全体を読み直す。hook は 1 回目の `sleep 60 && tail` を止め、 +「待ちの条件を until ループにして `run_in_background: true` で起動し、完了通知を待つ」よう示す。 + +**例(#830)。** conductor の文脈が 41 万のまま `/ndf:implementation-plan #829` を起動する +(3 層では `実装: #829` の supervisor を起動する)と、hook が起動を 1 度止め、「新しい会話で +`/ndf:development-workflow #829` を打つ」よう示す。conductor はその 1 行を利用者へ示して止まる。 + +**待ちの費用は「呼び出しの回数 × その時点の文脈」で決まる。** 背景で待って通知を 1 回受ける +なら、待つ時間の長さは費用を増やさない。#827 の実測では、待つ間の繰り返しの問い合わせ +(ポーリング)が全体の費用の 16%(2026-09-20 以降)、ai-plugins の 30 日間では 19%(258M)を +占めた。conductor の会話を工程の開始ごとに切っていれば、conductor の再読込量は 58%(30 日間 +では 62%)減る見込みだった。 + +**規定を書くだけでは守られなかったため、hook で止める。** `context-window.md` は以前から +「遅くとも 20 万で切る」と定めていたが、#827 の実測で守られていなかった。Claude Code 本体も +前景の `sleep` を本体の会話でしか止めず、サブエージェントの中の `sleep 12 && echo` は 12 秒 +待って成功した(Claude Code 2.1.280、2026-09-23)。本体の仕組みには頼れない。 + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| ポーリング | 待つ間に、状態を確かめるための呼び出しを繰り返すこと | +| 前景の Bash | `run_in_background` を付けずに実行する Bash。終わるまで呼び出しが返らない | +| 文脈量 | 1 回の API 呼び出しで読んだトークン数。`input_tokens + cache_read_input_tokens + cache_creation_input_tokens` | +| 工程 Skill | `context-window.md` の 4 つの切れ目の直後に始まる工程の Skill と、入口の `development-workflow` / `issue-plan-strategy`(下の「工程 Skill の一覧」) | +| 引き継ぎの 1 行 | 新しい会話の最初に打てば、その工程から再開できるコマンド 1 行。`/ndf:development-workflow #<課題> [#<課題> ...]` | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| `plugins/ndf/scripts/token-guard.sh` | PreToolUse の入口。`tool_name` で 3 つの判定(`Bash` → sleep / `Read` → 連続 Read / `Skill`・`Agent`・`Task` → 文脈量)へ振り分け、拒否か通過を返す。排他は `scripts/lib/lock-common.sh` を読み込んで使う | +| `plugins/ndf/scripts/lib/token_guard_sleep.py` | sleep の判定。標準入力にコマンド、第 1 引数に秒数の上限を受け、拒否なら 1、通すなら 0 で終わる | +| `plugins/ndf/scripts/lib/token-guard-stages.txt` | 工程 Skill の名前の一覧(1 行 1 名、13 個) | +| `plugins/ndf/hooks/claude.json` | PreToolUse に matcher `Bash\|Read\|Skill\|Agent\|Task` で `token-guard.sh` を登録する(既存の `worktree-guard.sh` の登録と順序は変えない) | +| `development-workflow/references/waiting.md` | 待ち方の規約の唯一の置き場所 | +| `external-ai/references/cli-codex.md`・`cli-agy.md`・`qa-security-scan/03-report-template.md`・`release/references/completion-check.md` | 前景の待ちのループの直前に「Claude Code では、このループを `run_in_background: true` で実行して完了通知を待つ」の 1 行を置き、`waiting.md` を指す | + +**sleep の判定は `python3` で書く。** 入れ子の `do` / `done` の対応と引用の除去を bash の +正規表現では読める形で書けないためである。`python3` が無いときは判定を通す。 + +```mermaid +graph TB + AG["エージェント
conductor / supervisor / worker"] + subgraph HK["hooks/claude.json の PreToolUse"] + WG["worktree-guard.sh"] + TG["token-guard.sh"] + end + subgraph ST["状態"] + RS["連続 Read の控えと案内の印
guards/"] + TR["会話の記録
transcript_path"] + SL["token-guard-stages.txt"] + end + subgraph DOC["development-workflow の文書"] + WT["references/waiting.md"] + CW["references/context-window.md"] + end + AG -->|"Bash / Read / Skill / Agent・Task"| TG + AG -->|"編集系 / Bash"| WG + TG -->|"Read の判定"| RS + TG -->|"Skill・Agent の判定"| TR + TG -->|"Skill の判定"| SL + TG -. "拒否の理由が指す" .-> WT + TG -. "拒否の理由が指す" .-> CW +``` + +## 仕様 + +### 常に成り立つ条件 + +- **hook の終了コードは常に 0 である。** 拒否は `permissionDecision: deny` で返し、通すときは + 何も出さない +- **判定が失敗したときは通す。** 入力が読めない・`jq` や `python3` が無い・`guards/` を作れない・ + 控えを書けない・記録を読めない・ロックを 1 秒で取れない、のいずれでもツールの実行を止めない。 + sleep の判定は状態を持たないため、`guards/` が使えなくても続ける +- **拒否の理由の欄は、代わりの手段と規約の場所と止める環境変数を必ず含む。** エージェントが + 理由の欄だけで次の手を決められるようにするためである + +### sleep の判定 + +**前景の Bash で、コマンドの位置の `sleep` が次のどちらかに当たると拒否する。** + +- `while` / `until` のループの本体(`do` と対応する `done` の間)にある。秒数が変数でも止める +- 秒数が数で、上限(既定 5 秒)を超える。ループの本体の外の `sleep` はこれだけで見る + +| 見方 | 規則 | +| --- | --- | +| 見ない部分 | コメント(引用の外の `#` 以降)・引用の中・ヒアドキュメントの本文 | +| 中身を取り出して同じ規則で見る | コマンドの位置にある `bash -c` / `sh -c` / `zsh -c` / `dash -c` / `eval` の実行される引数。入れ子も 1 段ずつ見る | +| コマンドの位置 | 行頭・`;` `&&` `\|\|` `\|` `&` `(` `do` `then` `else` の直後。先頭の代入語(`X=1`)と前置き(`timeout 590` / `nohup` / `env` など)の後ろも含む | +| 背景とみなして見ない | `run_in_background: true`、`&` で終わる `sleep`(リダイレクトを挟む形、sleep を含むリスト、`( )` / `{ }` / ループで囲んだ全体が背景になる形、外側が背景の `bash -c` / `eval` を含む)。`2>&1` / `&>` の `&` は背景と読まない | + +**ループの本体の `sleep` を止めるのは、そこに費用の大半があるためである。** 2026-08-23 以降の +全プロジェクトの記録(6,713 本、Bash 74,223 件)で、`sleep <数>` を含む前景の Bash は次のとおり +だった。 + +| 形 | 件数 | 費用(input 換算) | +| --- | ---: | ---: | +| 前景・ループの中 | 1,543 | 67.3M | +| 前景・ループなし | 743 | 9.9M | +| 背景(`run_in_background`) | 441 | 5.3M | + +ループの本体の外の短い `sleep`(サーバの起動を待つ間など)と、`for` のループで 5 秒以下の +`sleep` を挟む形(API の照会の間隔)は通す。代わりの手段が無いためである。`while` と `sleep` +が同じコマンドにあるだけでは止めない。ループの後の短い間まで止めることになる。 + +**文字列の中の `sleep` は止めない。** `echo sleep 30` や `git commit -m "sleep 60"` を止めない +ため、引用を除いた後の語の位置で判定する。ただし引用の中でも `bash -c 'sleep 30'` は実行される +ため、除く前に中身を取り出す。判定は字句による近似で、`case` の囲みは数えない。 + +### 連続 Read の判定 + +**同じ `file_path`・`offset`・`limit` の Read が、ファイルの大きさ・更新時刻・inode が変わらない +まま、その会話で続けて上限の回数(既定 3)に達すると拒否する。** 別の引数の Read が挟まるか、 +ファイルが変われば数え直す。拒否した Read も回数を進める。 + +- **「空ファイル」ではなく「変わっていない」で見る。** hook は実行の前に呼ばれ、読んだ中身を + 知らない。空ファイルの読み直しはこれに含まれ、書き込みが進むログの読み直しは含まれない +- **更新時刻はナノ秒の精度で持ち、inode も比べる。** 同じ秒に同じ大きさの内容で置き換えた + (`mv`)ファイルを、変わっていないと取り違えないためである +- **`offset` と `limit` を鍵に入れる。** 大きなファイルを範囲を変えて読み進める使い方を止めない +- **3 回目にしたのは実測による。** 2026-08-23 以降の記録で、同じ引数の Read が 3 回以上続いた + のは 6 本で、うち 5 本が `tasks/*.output` の読み直し(最長 1,168 回)、残る 1 本は画像を見直す + 3 回だった +- 間に他のツールが挟まったら数え直す形は採らない。hook は Read の呼び出しにしか登録されず、 + 間のツールを見られない + +### 文脈量の判定 + +**conductor が工程へ入る起動で、会話の文脈量が上限(既定 200,000)を超えていると、1 度拒否 +して引き継ぎの 1 行を示す。** 工程へ入る起動は経路によって違うツールに現れるため、両方を見る。 + +| 経路 | 見る入力 | 印の鍵 | +| --- | --- | --- | +| 対話 | `Skill`。名前(`ndf:` を外したもの)が `token-guard-stages.txt` にある | `skill`・`args` | +| 3 層 | `Agent`(旧名 `Task`)。`description` の `:` の前が持ち場の語彙(`設計` / `実装` / `検査` / `取り込み` / `仕上げ`) | `description` | + +- **サブエージェントの中の起動は見ない。** 入力に `agent_id` が付くか、`transcript_path` が + `/subagents/` を含めば見ない。Claude Code 2.1.280 の実測では、`agent_id` はサブエージェントの + 中でだけ付き、サブエージェントの `transcript_path` は親の記録を指すため、区別は `agent_id` で + つく。supervisor は 1 つの持ち場の中で複数の工程を通すため、工程の起動で止めると持ち場が + 途中で途切れる +- **工程でない Skill と、先頭語が作業の種類(`調査` など)の Agent は見ない。** 工程の途中で + 起動されるため切れ目にならない +- **拒否の後、次に工程へ入る起動が同じ鍵なら 1 度だけ通し、印を消す。** 間に他のツールや + 工程でない Skill・Agent が挟まっても印は残る。次の起動が別の鍵なら、上限を超えていれば印を + 置き換えて再び拒否する。これで工程の切れ目ごとに 1 度ずつ止まり、「このまま続ける」と決めた + 利用者は同じ起動をもう一度行えば続けられる。毎回拒否すると同じ工程をやり直せず、会話ごとに + 1 度にすると以後の切れ目で止まらない。案内だけを足す形(`additionalContext`)は、規定が読み + 流された実測があるため採らない +- **文脈量を読めない(記録が無い・`usage` が無い)ときは通す** + +**上限の既定は 200,000 で、`context-window.md` の「遅くとも 20 万」と `skill-stats.py` の +`DEFAULT_WINDOW_LIMIT` と同じ値にする。** 測る側と止める側の上限を食い違わせないためである。 +10 万(目安の側)にしないのは、#827 で固定費だけで約 4 万あり、1 工程の途中で止まる回数が +増えるためである。 + +**文脈量は `transcript_path` の末尾 200 行の、最後の assistant 行の `message.usage` から読む。** +`transcript_agents.py` の `_input_total` と `statusline.sh` と同じ足し方である。PreToolUse の時点で +その呼び出しを出した assistant 行はまだ記録に書かれていないため、1 つ以上前の呼び出しの値に +なる(差は 1 回分の出力と結果)。末尾だけを読むのは、大きな記録でも速く終えるためである。 + +**拒否の理由の欄の `<課題>` は、Skill の `args`(Agent なら `description`)から取り出す。** +`#<数>` と数だけの語を課題番号とし、日付・版数・小数(`2026-09-23` / `v10.16.1` / `2.0.3`)は +番号と読まない。範囲(`#829-830`)は `#829 #830` として示す。番号が無ければ `<課題番号>` の +文字のまま示し、通過工程の控えから推測しない。並行して別の課題を進めていると、最新の控えは +別の課題を指すためである。 + +### 工程 Skill の一覧 + +`token-guard-stages.txt` は工程表から機械的に抽出しない。次の 13 個を正とする。 + +| 切れ目 | 直後に始まる工程の Skill | +| --- | --- | +| 1 ドキュメントレビューのマージの後 | `implementation-plan` / `document-drafting` | +| 2 構造改善と実装レビューの前後 | `cross-refactoring` / `cross-review` / `pr-review` / `quality-gates` | +| 3 Pull Request を出した後 | `plan-to-spec` / `merged` | +| 4 配布の後 | `layout-review` / `release-verification` / `retrospective` | +| 入口 | `development-workflow` / `issue-plan-strategy` | + +`worktree` など切れ目の内側の工程は含めない。`cross-review` は切れ目 1 の前(ドキュメント +レビュー)でも起動されるが、その時点で上限を超えていれば止めてよいので含める。 + +### 引き継ぎの 1 行 + +**形は `/ndf:development-workflow #<課題> [#<課題> ...]` とする。** 工程 Skill を直接起動する形 +(`/ndf:implementation-plan #829`)は採らない。工程 Skill はモード・作業ツリー・承認の状態を戻す +手順を持たず、戻す手順を持つのは `development-workflow` の側だからである。経由すると固定費に +約 1 万トークンが足されるが、切る前の会話の文脈(#827 で平均 41 万)に比べて小さい。Codex と +Kiro では、それぞれの README が示す Skill の起動の書き方に読み替える。 + +**conductor は `context-window.md` の 4 つの切れ目と、文脈量の hook が拒否したときにこの 1 行を +出す。** 3 層では supervisor の持ち場の境がこの切れ目に当たるため、conductor が `## 持ち場の報告` +を受け取った時点で出し、supervisor は出さない。報告が `結果: 関門` のときは、関門の承認と +取り込み(設計 Pull Request のマージなど)が済んだ後に出す。関門の前に会話を切らないためである。 + +**新しい会話の `development-workflow` は、課題の本文の `## 進行`・通過工程の控え・Pull Request +から、モード・作業ツリー・現在の工程を戻す。** Pull Request は番号の全文検索で引かず、作業 +ツリーのブランチ名と課題の `closedByPullRequestsReferences` で引く。設計の Pull Request は閉じる +語を持たないため、ブランチ名でしか引けない。手順は `context-window.md` の「新しい会話で戻す」が +持つ。 + +### 待ち方 + +**1 回で足りる待ちは `Monitor` ではなく、`run_in_background` で起動した until ループにする。** +`Monitor` は出来事のたびに通知が届き、既定 5 分・最長 30 分で打ち切られて張り直しが要る。 +終わりだけを知りたい待ちでは、通知が 1 回で済む `run_in_background` のほうが呼び出しが少ない。 + +**サブエージェントは背景の処理を残したまま応答を終えない。** Claude Code 2.1.280 の実測では、 +`codex exec` を `run_in_background` で起動して応答を終えたサブエージェントは、完了通知(約 2 秒後) +で再開して報告を出し直した。ただし親には応答を終えた時点で 1 度「終わった」と通知が届き、途中の +文面が結果として渡った。親が 1 回目を結果と読むと、報告の無い持ち場を受け取る。 + +**待ち方の規約は `waiting.md` の新しいファイルに置く。** `agent-layers.md` の節にすると、 +`external-ai` などの文書から参照するたびに 3 層の規約の全体を読ませる。 + +**配布物の文書にある前景の待ちのループは、ループを書き換えずに案内の 1 行を足す。** 拒否の +理由が「同じループを `run_in_background: true` で」と案内するため、Claude Code では 1 回の +回り道で済む。hook と案内の行を同じ版で配布するため、拒否と文書の順序が食い違わない。ループの +書き換え(道具の共通化)は #731 が扱う。 + +## データ・設定 + +### 控えの置き場所 + +**`guards/` の場所は `token-guard.sh` が自前で解決する。** 次の順で先に使えたものの下に置く。 +順は `workflow-common.sh` の `wf_state_dir` と同じである。`workflow-common.sh` は読み込まない。 +末尾で通信の層まで読み込むため毎回の hook には重く、その層の変更が hook へ波及する。 + +1. `$CLAUDE_PLUGIN_DATA/guards` +2. `$XDG_STATE_HOME/ndf/guards` +3. `$HOME/.local/state/ndf/guards` +4. `${TMPDIR:-/tmp}/ndf-guards`(上が作れない・書けないときも使う) + +| ファイル | 中身 | +| --- | --- | +| `read-.json` | 連続 Read の控え(下の表) | +| `context-.json` | 文脈量の案内の印。`{"key": "<鍵>"}`。鍵は Skill なら `skill\t<名前>\t`、Agent なら `agent\t` | +| `.lock` | session ごとのロック | + +連続 Read の控え: + +| キー | 型 | 意味 | +| --- | --- | --- | +| `key` | 文字列 | 直前の Read の `file_path`・`offset`・`limit` を `\t` でつないだもの | +| `size` | 整数 | 直前の Read の時点のファイルの大きさ(バイト)。無いファイルは `-1` | +| `mtime` | 文字列 | 同じく更新時刻(ナノ秒の精度。GNU の `stat -c %.9Y`、BSD の `stat -f %Fm`) | +| `inode` | 整数 | 同じく inode 番号。無いファイルは `-1` | +| `count` | 整数 | `key`・`size`・`mtime`・`inode` が変わらないまま続いた Read の回数 | + +- **書き込みは置き換えで行う**(一時ファイルへ書いて `mv`)。途中で落ちても壊れた JSON を残さない +- **控えと印の読み・判定・書き込みは session ごとのロックの中で行う。** 同じ session の hook が + 並列に走ると、置き換えだけでは `count` の更新や印が失われる。ロックは `lock-common.sh` の + `ndf_lock_acquire 1` / `ndf_lock_release` で取り、1 秒で取れなければ判定せず通す。 + sleep の判定はロックを取らない +- **7 日より古い控えは、書き込みのついでに消す。** 会話が終わった合図を hook は受け取らない + +### 環境変数 + +| 変数 | 既定 | 意味 | +| --- | --- | --- | +| `NDF_SLEEP_GUARD` | `1` | `0` で sleep の判定を止める | +| `NDF_SLEEP_MAX_SEC` | `5` | `sleep` の秒数の上限(ループの本体の外ではこれだけで見る) | +| `NDF_READ_REPEAT_GUARD` | `1` | `0` で連続 Read の判定を止める | +| `NDF_READ_REPEAT_LIMIT` | `3` | 連続 Read を拒否する回数 | +| `NDF_CONTEXT_GUARD` | `1` | `0` で文脈量の判定を止める | +| `NDF_CONTEXT_LIMIT` | `200000` | 文脈量の上限(トークン) | + +## 外部連携 + +### hook の入力(Claude Code の PreToolUse) + +| キー | 使う判定 | 無いとき | +| --- | --- | --- | +| `tool_name` | 振り分け | 通す | +| `tool_input.command` / `tool_input.run_in_background` | sleep | 通す | +| `tool_input.file_path` / `offset` / `limit` | 連続 Read | 通す | +| `tool_input.skill` / `tool_input.args` | 文脈量(対話の経路) | 通す | +| `tool_input.description` | 文脈量(3 層の経路) | 通す | +| `session_id` | 連続 Read の控え・案内の印 | 通す | +| `transcript_path` | 文脈量 | 通す | +| `agent_id` | 文脈量(付いていれば見ない) | conductor とみなす | + +### hook の出力 + +```json +{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", + "permissionDecisionReason":"<理由>"}} +``` + +| 判定 | 理由の欄が示すこと | +| --- | --- | +| sleep | 同じ条件の until ループを `run_in_background: true` で起動して完了通知を待つこと。出来事を 1 つずつ受けるなら `Monitor`。規約 `waiting.md`。`NDF_SLEEP_GUARD=0` | +| 連続 Read | 書き終わりを待つなら until ループを `run_in_background: true` で起動するか、背景の処理の完了通知を待つこと。`tasks/*.output` は読まない。規約 `waiting.md`。`NDF_READ_REPEAT_GUARD=0` | +| 文脈量 | 文脈量と上限、利用者へ示す引き継ぎの 1 行(3 層なら新しい会話の `/goal` に渡す)、続けるなら同じ起動をもう一度行うこと。規約 `context-window.md`。`NDF_CONTEXT_GUARD=0` と `NDF_CONTEXT_LIMIT` | + +### 4 ランタイム + +**hook は Claude Code にだけ登録し、他の 3 ランタイムは規約で守る。** 拒否の理由が案内する +代わりの手段(`Monitor` / `run_in_background` の通知)は Claude Code にしか無く、文脈量も +Claude Code の `transcript_path` からしか読めない。#827 の実測も Claude Code の記録だけである。 +ランタイムごとの扱いの表は `waiting.md` の「hook」と `plugins/ndf/README.md` にある。Codex / Kiro / +agy の CLI 側の消費を測った後に、登録するかを改めて決める。 + +## 運用 + +- **止める:** 環境変数で判定ごとに止める。hook そのものを外すなら `hooks/claude.json` の登録を + 1 つ外す。データの移行は無い +- **続ける:** 文脈量の拒否の後、利用者がこのまま続けると決めたら、同じ起動をもう一度行う +- **性能:** 1 回の実行は、50 MB の記録でも競合しないとき 1 秒以内、ロックを待つときは 2 秒以内に + 終わる。記録は末尾 200 行だけを読み、Bash と Read の判定は記録を読まない。登録の `timeout` は + 5 秒で、`continueOnError: true` を付ける + +## テスト観点 + +テストは `plugins/ndf/scripts/tests/test_token_guard.py` にあり、入力 JSON と記録の見本を与えて +終了コードと出力を見る。 + +- 前景の `sleep` の待ち(`sleep 30 && tail`・`while` / `until` の本体の `sleep`・`bash -c` / `sh -c` / + `timeout ... bash -c` の中身・`for` の本体の上限超え・入れ子の `while`)を拒否し、理由の欄に + `run_in_background` と `waiting.md` を含むこと +- 背景の `sleep`・`Monitor`・`sleep` を含まない Bash・本体の外の上限以下の `sleep`・`for` の本体の + 上限以下の `sleep`・文字列やコメントやヒアドキュメントの中の `sleep` を通すこと +- 同じ範囲の変わらない Read の 3 回目を拒否し、追記・`offset` の変更・同じ大きさの `mv` の置き換えで + 数え直すこと。同じ session の並列の hook で更新が失われないこと +- 文脈量が上限を超えた conductor の工程 Skill と持ち場の Agent を拒否し、引き継ぎの 1 行に課題番号を + 示すこと。番号が無ければ `<課題番号>` のまま示すこと +- サブエージェントの中の起動・工程でない Skill・作業の種類の Agent を通すこと +- 拒否の後の同じ起動を 1 度だけ通し(間に Bash と Read が挟まっても)、別の起動を再び拒否すること。 + 同じ起動の 2 回目を並列に起動しても通るのは 1 本だけであること +- 壊れた入力・`jq` の無い `PATH`・書けない控えの場所・取れないロック・読めない記録で、出力なし・ + 終了コード 0 で通すこと。`guards/` の親が `wf_state_dir` の親と一致すること +- 環境変数で判定ごとに止まり、上限が変わること +- `token-guard-stages.txt` の名前が上の 13 個と一致し、どれも `plugins/ndf/manifests/` の Skill 一覧に + あること +- `waiting.md` が 1 か所にあって `agent-layers.md` の supervisor と worker の規則から参照され、 + Claude Code 向けに前景の `sleep` のループを勧める例が無いこと。`context-window.md` に #827 の + 実測値・上限の値・戻す手順があり、`SKILL.md` に引き継ぎの 1 行の規約があること。README に + 4 ランタイムの表があること +- Codex / Kiro / agy の既存の hook の動作が変わらないこと(既存のテスト) + +効果の数値(ポーリングの費用の割合、conductor の最大文脈と再読込量)は、配布後に #827 の +`measure.py` / `poll.py` / `extra.py` を変更前と同じ条件で回して比べる。変更前の値は、ポーリング +が全体の 16%、conductor の最大文脈 683k、再読込の削減見込み 58% である。 + +## 関連リンク + +- [#829](https://github.com/devbasex/ai-plugins/issues/829) / [#830](https://github.com/devbasex/ai-plugins/issues/830)(親は [#827](https://github.com/devbasex/ai-plugins/issues/827)) +- [#731](https://github.com/devbasex/ai-plugins/issues/731) — 待ちの道具(`bg-wait.sh`)を共通層へ移す +- [ndf-context-window-metrics.md](ndf-context-window-metrics.md) — 会話の記録から文脈量を測る部品 +- [ndf-agent-layers-unattended-run.md](ndf-agent-layers-unattended-run.md) — 3 層の運転 diff --git a/issues/old/README.md b/issues/old/README.md index 4ea635195..7ebeca888 100644 --- a/issues/old/README.md +++ b/issues/old/README.md @@ -44,6 +44,7 @@ | [#312](https://github.com/devbasex/ai-plugins/issues/312) / [#315](https://github.com/devbasex/ai-plugins/issues/315) / [#313](https://github.com/devbasex/ai-plugins/issues/313) / [#573](https://github.com/devbasex/ai-plugins/issues/573) / [#610](https://github.com/devbasex/ai-plugins/issues/610) / [#495](https://github.com/devbasex/ai-plugins/issues/495) | 作業ツリー運用の残課題(まとまり「04 worktree 運用の残課題」)。確定仕様は [ndf-worktree-declaration-and-entry-points.md](../../docs/specifications/ndf-worktree-declaration-and-entry-points.md) と [ndf-testenv-lock-and-registry.md](../../docs/specifications/ndf-testenv-lock-and-registry.md) | [milestone-04-worktree/](milestone-04-worktree/issue-312-315-requirements.md) | | [#561](https://github.com/devbasex/ai-plugins/issues/561) / [#623](https://github.com/devbasex/ai-plugins/issues/623) / [#550](https://github.com/devbasex/ai-plugins/issues/550) / [#657](https://github.com/devbasex/ai-plugins/issues/657) / [#540](https://github.com/devbasex/ai-plugins/issues/540) / [#541](https://github.com/devbasex/ai-plugins/issues/541) / [#621](https://github.com/devbasex/ai-plugins/issues/621) / [#554](https://github.com/devbasex/ai-plugins/issues/554) | 無人運転と工程の測定(まとまり「10 無人運転と工程の測定」、マイルストーン 18)。確定仕様は [ndf-cleanup-and-bundle-closing.md](../../docs/specifications/ndf-cleanup-and-bundle-closing.md) / [ndf-agent-layers-unattended-run.md](../../docs/specifications/ndf-agent-layers-unattended-run.md) / [ndf-context-window-metrics.md](../../docs/specifications/ndf-context-window-metrics.md) / [ndf-execution-plan-and-parallel-capacity.md](../../docs/specifications/ndf-execution-plan-and-parallel-capacity.md) / [ndf-instruction-files-check.md](../../docs/specifications/ndf-instruction-files-check.md) | [milestone-18-unattended/](milestone-18-unattended/issue-561-623-requirements.md)(要求・設計・契約・決定・計画・調査の 24 本。#762 の要件は下の行) | | [#762](https://github.com/devbasex/ai-plugins/issues/762) | `agent-layers.md` の「並行の本数」の節で、実行計画の持ち主を `issue-plan-strategy` の `execution-plan.md` へ向ける(`light`、マイルストーン 18 の続き。#550 の AC60「`light` の課題 1 件を無人で通す」の確認に使った) | [milestone-18-unattended/issue-762-requirements.md](milestone-18-unattended/issue-762-requirements.md) | +| [#829](https://github.com/devbasex/ai-plugins/issues/829) / [#830](https://github.com/devbasex/ai-plugins/issues/830) | 待つ間の問い合わせを止め、conductor の会話を工程の切れ目で切る(マイルストーン 26「17 トークン消費の削減」のまとまり 1)。確定仕様は [ndf-token-waits-and-context-cut.md](../../docs/specifications/ndf-token-waits-and-context-cut.md) | [milestone-26-token-waits/](milestone-26-token-waits/issue-829-830-requirements.md)(要求・設計・決定・計画の 4 本) | ## 計画と調査資料 diff --git a/issues/issue-829-830-design-decisions.md b/issues/old/milestone-26-token-waits/issue-829-830-design-decisions.md similarity index 100% rename from issues/issue-829-830-design-decisions.md rename to issues/old/milestone-26-token-waits/issue-829-830-design-decisions.md diff --git a/issues/issue-829-830-design.md b/issues/old/milestone-26-token-waits/issue-829-830-design.md similarity index 100% rename from issues/issue-829-830-design.md rename to issues/old/milestone-26-token-waits/issue-829-830-design.md diff --git a/issues/issue-829-830-implementation-plan.md b/issues/old/milestone-26-token-waits/issue-829-830-implementation-plan.md similarity index 100% rename from issues/issue-829-830-implementation-plan.md rename to issues/old/milestone-26-token-waits/issue-829-830-implementation-plan.md diff --git a/issues/issue-829-830-requirements.md b/issues/old/milestone-26-token-waits/issue-829-830-requirements.md similarity index 100% rename from issues/issue-829-830-requirements.md rename to issues/old/milestone-26-token-waits/issue-829-830-requirements.md diff --git a/plugins/ndf/scripts/lib/token-guard-stages.txt b/plugins/ndf/scripts/lib/token-guard-stages.txt index 716a51231..7666d9fff 100644 --- a/plugins/ndf/scripts/lib/token-guard-stages.txt +++ b/plugins/ndf/scripts/lib/token-guard-stages.txt @@ -1,6 +1,6 @@ # 文脈量の hook(token-guard.sh)が見る工程 Skill の一覧(#830)。1 行 1 名。 -# 正は issues/issue-829-830-design.md の「工程 Skill の一覧」(context-window.md の 4 つの切れ目の直後の工程と入口)。 -# worktree など切れ目の内側の工程は載せない(決定 6)。 +# 正は docs/specifications/ndf-token-waits-and-context-cut.md の「工程 Skill の一覧」(context-window.md の 4 つの切れ目の直後の工程と入口)。 +# worktree など切れ目の内側の工程は載せない(理由は同じ仕様書の「文脈量の判定」)。 implementation-plan document-drafting cross-refactoring From 577e20111c8bb9cdc900d7213484ba166f7787be Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 04:56:13 +0000 Subject: [PATCH 29/30] Release: ndf v10.17.0-dev.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #829 #830(待つ間の問い合わせと長い conductor の工程の起動を hook で止める)を develop の チャネルへ載せる開発版。版数を持つ 15 箇所、更新案内の本文と手元で確かめるコマンド、 CHANGELOG.md の ndf 10.17.0 の節、docs/ndf-version-decisions.md の v10.17.0 の判断を更新する。 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdEztjNvLXF6EasAvTjAfk --- .claude-plugin/marketplace.json | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 20 +++++++++++++++++ README.md | 4 ++-- docs/ndf-version-decisions.md | 22 +++++++++++++++++- docs/versioning-and-distribution.md | 12 +++++----- plugins/ndf/.claude-plugin/plugin.json | 4 ++-- plugins/ndf/.codex-plugin/plugin.json | 4 ++-- plugins/ndf/README.md | 31 +++++++++++++------------- plugins/ndf/dev.agy/plugin.json | 4 ++-- 10 files changed, 72 insertions(+), 33 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 331f0fd6e..3de3637b9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.16.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.17.0-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/AGENTS.md b/AGENTS.md index 9532ad999..b3bdbc4a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.16.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.17.0-dev.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 - Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める - Claude Code版は 8個の専門サブエージェント、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/CHANGELOG.md b/CHANGELOG.md index 88becbe02..315a34726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.17.0] - 2026-09-23 + +### 追加 + +- **PreToolUse hook `scripts/token-guard.sh` を足した(Claude Code だけ)**(#829 #830)。前景の Bash で + `while` / `until` のループの本体にある `sleep` と 5 秒を超える `sleep`、変わらないファイルの同じ範囲を + 3 回続けて読む Read、文脈が 200,000 を超えた conductor が工程 Skill か持ち場の supervisor を起動することを、 + 理由の欄に代わりの手段を書いて止める。文脈量の拒否は起動ごとに 1 度で、同じ起動をもう一度行えば通る。 + 環境変数 `NDF_SLEEP_GUARD` / `NDF_SLEEP_MAX_SEC` / `NDF_READ_REPEAT_GUARD` / `NDF_READ_REPEAT_LIMIT` / + `NDF_CONTEXT_GUARD` / `NDF_CONTEXT_LIMIT` で止める・上限を変える +- **待ち方の規約 `development-workflow/references/waiting.md` を足した**(#829)。`agent-layers.md` の + supervisor / worker の規則と、前景の待ちのループを持つ 4 文書(`cli-codex.md` / `cli-agy.md` / + `qa-security-scan/03-report-template.md` / `release/references/completion-check.md`)から指す + +### 変更 + +- **`context-window.md` の「前提: 実測ではない」を #827 の実測値に置き換え、「上限を超えたら hook が止める」 + 「新しい会話で戻す」の節を足した**(#830)。`development-workflow/SKILL.md` に、4 つの切れ目で conductor が + 引き継ぎの 1 行(`/ndf:development-workflow #<課題>`)を出す規約を足した + ## [ndf 10.16.1] - 2026-09-22 ### 修正 diff --git a/README.md b/README.md index ac2b45696..f0a9e4718 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Claude Code / Codex / Kiro CLI / agy 向けのスキル・MCP設定を共有す このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI / agy)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v10.16.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 +**NDFプラグイン v10.17.0-dev.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 - **公開Skills**: Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個に分離。 - **元Skills(45個)**: @@ -110,7 +110,7 @@ hook を効かせる手順と、新しい版へ入れ替える手順は | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 10.16.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | +| **ndf** | 10.17.0-dev.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | | **playwright-kit** | 2.0.3 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [README](./plugins/playwright-kit/README.md) | ### 変更履歴 diff --git a/docs/ndf-version-decisions.md b/docs/ndf-version-decisions.md index 512711f55..cdb209e4a 100644 --- a/docs/ndf-version-decisions.md +++ b/docs/ndf-version-decisions.md @@ -1,4 +1,4 @@ -# NDF の版ごとの決定と理由(v10.12.0〜v10.16.1) +# NDF の版ごとの決定と理由(v10.12.0〜v10.17.0) `CLAUDE.md` から移した、出た版の記録である。**その版で何を決め、なぜそう決めたか**を残す。 変更点の列挙は `CHANGELOG.md` にあり、こちらは判断の理由を持つ。**`CLAUDE.md` へ書くのは @@ -257,3 +257,23 @@ claude の旧い形は記録にも実行ファイルにも 0 件のため足し ときも、理由は「実行できない」として出す** ── Python の起動は「見つからない」と「実行権が無い」を 同じ例外で返し、見分けるには検索のパスを自分で辿り直すことになって、確認を 1 つ走らせる関数の範囲を 超える。理由に権限の拒否が出れば、利用者は検索のパスとファイルの権限を見ればよい。 + +v10.17.0 で待つ間の問い合わせと、長い conductor の会話を止めた(マイルストーン 26「17 トークン消費の +削減」のまとまり 1、#829 #830)。確定仕様は +[ndf-token-waits-and-context-cut.md](specifications/ndf-token-waits-and-context-cut.md) にある。 + +**規定を書くだけでは守られなかったため、hook で止める**(#829 #830)。`context-window.md` は以前から +「遅くとも 20 万で切る」と定めていたが、#827 の実測で守られていなかった。Claude Code 本体の前景の +`sleep` の制限もサブエージェントには掛からない。**止めるのはループの本体の `sleep` と 5 秒を超える +`sleep` だけにする** ── 前景の `sleep` の費用の 87% がループの中にあり、ループの外の短い間と `for` の +照会の間隔には代わりの手段が無い。**連続 Read は「空」ではなく「変わっていない」で見る** ── hook は +実行の前に呼ばれ、読んだ中身を知らない。 + +**文脈量の拒否は起動ごとに 1 度にし、同じ起動の 2 回目を通す**(#830)。毎回拒否すると続けると +決めた利用者が同じ工程をやり直せず、会話ごとに 1 度にすると以後の切れ目で止まらない。案内だけを +足す形は、規定が読み流された実測があるため採らない。**引き継ぎの 1 行は `development-workflow` を +起動する形にする** ── 工程 Skill はモード・作業ツリー・承認の状態を戻す手順を持たない。 + +**hook は Claude Code にだけ置く**(#829 #830)。代わりの待ち方(`run_in_background` の通知と +`Monitor`)と文脈量を読む記録を持つのが Claude Code だけで、#827 の実測も Claude Code の記録だけで +ある。Codex / Kiro / agy は規約と引き継ぎの 1 行で守り、CLI 側の消費を測った後に改めて決める。 diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index e4432a166..68cfc4a70 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -57,15 +57,15 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` | 版 | 形 | 意味 | | --- | --- | --- | -| 正式版 | `10.16.1` | 利用者が常用してよい | -| 開発版 | `10.17.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | -| 公開前の確認版 | `10.17.0-rc.1` | 正式版の候補。残るのは確認だけ | +| 正式版 | `10.17.0` | 利用者が常用してよい | +| 開発版 | `10.18.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | +| 公開前の確認版 | `10.18.0-rc.1` | 正式版の候補。残るのは確認だけ | -- 接尾辞は**次に出す正式版の版数へ付ける**。`10.16.1` の次を開発するなら `10.17.0-dev.1` +- 接尾辞は**次に出す正式版の版数へ付ける**。`10.17.0` の次を開発するなら `10.18.0-dev.1` - 連番は開発版を出すたびに増やす。**同じ版数で中身を差し替えない**。差し替えると、利用者の 手元にある版と `main` の版が同じ番号で別物になり、何を確かめたのかが分からなくなる -- **正式版を出すときは接尾辞を外す。** `10.17.0-dev.3` の次は `10.17.0` -- 順序は semver に従い `10.17.0-dev.1` < `10.17.0-rc.1` < `10.17.0` になる +- **正式版を出すときは接尾辞を外す。** `10.18.0-dev.3` の次は `10.18.0` +- 順序は semver に従い `10.18.0-dev.1` < `10.18.0-rc.1` < `10.18.0` になる ## ランタイムごとの取得と導入 diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index ddf8c0dee..778719a1a 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.1", - "description": "Claude Code plugin (v10.16.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "version": "10.17.0-dev.1", + "description": "Claude Code plugin (v10.17.0-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 62ca0211b..35d4af527 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.16.1", - "description": "Codex plugin (v10.16.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "version": "10.17.0-dev.1", + "description": "Codex plugin (v10.17.0-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 689102404..459bd821f 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -89,7 +89,7 @@ bash plugins/ndf/dev.kiro/install.sh --dry-run ```bash python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.16.1) +# => NDF統合開発エージェント(Kiro CLI用 / v10.17.0-dev.1) ``` ### agy @@ -119,21 +119,20 @@ agy plugin list # => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} ``` -## v10.16.1 へ更新するとき +## v10.17.0-dev.1 へ更新するとき -**収束ループが、codex と claude の利用上限で止まった担当を起動し直さなくなり、読めない -ディレクトリを含む `PATH` でも始まるようにしました**(マイルストーン 17「10.16.0 のリリース後 -テストで不合格だった条件」、#811 #813)。Skill の数は変わりません。引数・Skill・スクリプトの -削除や改名は無く、記録の移行も要りません(前の版で始めた状態ファイルはそのまま読めます)。 -変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 +**Claude Code で、待つ間の繰り返しの問い合わせと、文脈が上限を超えた conductor の工程の起動を +hook が止めるようにしました**(マイルストーン 26「17 トークン消費の削減」、#829 #830)。Skill の数は +変わりません。引数・Skill・スクリプトの削除や改名は無く、記録の移行も要りません。Codex / Kiro / agy の +hook は変わりません。変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 -**正式版です。** `main` に載ります。中身は開発版 `10.16.1-dev.1` と同じで、版数の接尾辞だけを -外しました。 +**開発版です。** `develop` にだけ載ります。取得元へ `#develop` を足す手順は +[docs/versioning-and-distribution.md の「開発版を試す」](../../docs/versioning-and-distribution.md#開発版を試す)にあります。 | 変わったこと | 中身 | | --- | --- | -| **codex と claude の利用上限を理由として報告します**(#811) | 担当が利用上限で止まったときの実物の文言(codex の 2 形・claude の 5 形)を照合に足しました。理由が「結果ファイル無し」ではなく「利用上限」になり、同じラウンドで起動し直しません。行頭で始まる行だけを読むため、担当が差分や文書を読み上げた行では止まりません | -| **読めないディレクトリを含む `PATH` でも始まります**(#813) | 認証の確認が、確認コマンドを起動できない理由(権限の拒否・実行形式でないファイル)を「通らない」として返します。`PATH` に読めないディレクトリがあり、CLI が 1 者欠けている環境でも、2 つの開始の手順は終了コード 0 で終わり、使える者だけで始まります | +| **前景の `sleep` の待ちと、変わらないファイルの読み直しを止めます**(#829) | `while` / `until` のループの本体にある `sleep` と 5 秒を超える `sleep`、同じファイルの同じ範囲を変わらないまま 3 回続けて読む Read を、理由の欄に代わりの待ち方(`run_in_background: true` で起動して完了通知を待つ / `Monitor`)を書いて止めます。待ち方の規約は `development-workflow/references/waiting.md` にあります。`NDF_SLEEP_GUARD=0` / `NDF_READ_REPEAT_GUARD=0` で止められます | +| **文脈が 200,000 を超えた conductor の工程の起動を 1 度止めます**(#830) | 工程 Skill か持ち場の supervisor を起動すると、新しい会話で打つ 1 行(`/ndf:development-workflow #<課題>`)を示して止めます。このまま続けるなら同じ起動をもう一度行えば通ります。`NDF_CONTEXT_GUARD=0` で止め、`NDF_CONTEXT_LIMIT` で上限を変えられます | 正式版のチャネル(ref を指定せずに登録した取得元)なら、次で入れ替わります。**動いているセッションには 反映されない**ため、更新したあとは起動し直してください。開発版を試すために `develop` を登録した @@ -156,8 +155,8 @@ codex plugin add ndf@ai-plugins にあります。 ```bash -grep -q "You\['’\]ve hit your" "$SCRIPTS/lib/monitor.py"; echo "exit=$?" # 0 なら codex と claude の上限の文言を読む -grep -q 'except OSError' "$SCRIPTS/lib/auth.py"; echo "exit=$?" # 0 なら起動できない確認コマンドで落ちない +grep -q 'token-guard.sh' "$SCRIPTS/../hooks/claude.json"; echo "exit=$?" # 0 なら hook が登録されている +test -f "$SCRIPTS/../skills/development-workflow/references/waiting.md"; echo "exit=$?" # 0 なら待ち方の規約がある ``` ## Playwright テストについて @@ -322,7 +321,7 @@ agy models # 認証の確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.16.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.17.0-dev.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -344,14 +343,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.16.1/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.17.0-dev.1/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 10.16.1 +# => ndf@ai-plugins installed, enabled 10.17.0-dev.1 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 5a522db25..9ac27bd9f 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.16.1", - "description": "Antigravity CLI plugin (v10.16.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "version": "10.17.0-dev.1", + "description": "Antigravity CLI plugin (v10.17.0-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." } From ee6474bb267b455bd5f29933e51f50ba12b1a04e Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 23 Sep 2026 05:11:49 +0000 Subject: [PATCH 30/30] Release: ndf v10.17.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 開発版 10.17.0-dev.1 の接尾辞を外し、正式版 10.17.0 として `main` へ配る。 更新案内の本文を正式版の記述へ直した。 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016FbePpugNBKWFzJDmz4mgA --- .claude-plugin/marketplace.json | 2 +- AGENTS.md | 2 +- README.md | 4 ++-- plugins/ndf/.claude-plugin/plugin.json | 4 ++-- plugins/ndf/.codex-plugin/plugin.json | 4 ++-- plugins/ndf/README.md | 14 +++++++------- plugins/ndf/dev.agy/plugin.json | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3de3637b9..75437d250 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.17.0-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.17.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/AGENTS.md b/AGENTS.md index b3bdbc4a7..c937c0807 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.17.0-dev.1)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v10.17.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf/` の1ディレクトリにまとまっています。 - Skill の実体は `plugins/ndf/skills/` の1箇所。配布先は `plugins/ndf/manifests/*-skills.txt` が決める - Claude Code版は 8個の専門サブエージェント、公開Skills、PreToolUse/SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/README.md b/README.md index f0a9e4718..c338b4b8e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Claude Code / Codex / Kiro CLI / agy 向けのスキル・MCP設定を共有す このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI / agy)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v10.17.0-dev.1** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 +**NDFプラグイン v10.17.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI / agy へ配布されるプラグインです。配布物は `plugins/ndf/` の1ディレクトリにまとまっており、Skill の実体は `plugins/ndf/skills/` の1箇所だけです。どのランタイムへ配るかは `plugins/ndf/manifests/*-skills.txt` が決めます。 - **公開Skills**: Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個に分離。 - **元Skills(45個)**: @@ -110,7 +110,7 @@ hook を効かせる手順と、新しい版へ入れ替える手順は | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 10.17.0-dev.1 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | +| **ndf** | 10.17.0 | Claude Code / Codex / Kiro CLI / agy へ 1 ディレクトリから配布する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 45個、Kiro向け core 44個、Codex向け core 43個、agy向け core 43個)、4ランタイム共通の作業ツリー運用フック(PreToolUse / SessionStart / userPromptSubmit / agentSpawn / PreInvocation)、Claude Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [README](./plugins/ndf/README.md) | | **playwright-kit** | 2.0.3 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [README](./plugins/playwright-kit/README.md) | ### 変更履歴 diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 778719a1a..0ebbb32ea 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.17.0-dev.1", - "description": "Claude Code plugin (v10.17.0-dev.1): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "version": "10.17.0", + "description": "Claude Code plugin (v10.17.0): 8 specialized agents and 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 35d4af527..93da3ca55 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "10.17.0-dev.1", - "description": "Codex plugin (v10.17.0-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "version": "10.17.0", + "description": "Codex plugin (v10.17.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 459bd821f..4b38b8107 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -89,7 +89,7 @@ bash plugins/ndf/dev.kiro/install.sh --dry-run ```bash python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v10.17.0-dev.1) +# => NDF統合開発エージェント(Kiro CLI用 / v10.17.0) ``` ### agy @@ -119,15 +119,15 @@ agy plugin list # => {"imports":[{"name":"ndf","source":"antigravity","components":["skills","agents","hooks"]}]} ``` -## v10.17.0-dev.1 へ更新するとき +## v10.17.0 へ更新するとき **Claude Code で、待つ間の繰り返しの問い合わせと、文脈が上限を超えた conductor の工程の起動を hook が止めるようにしました**(マイルストーン 26「17 トークン消費の削減」、#829 #830)。Skill の数は 変わりません。引数・Skill・スクリプトの削除や改名は無く、記録の移行も要りません。Codex / Kiro / agy の hook は変わりません。変更点の一覧は [CHANGELOG.md](../../CHANGELOG.md) にあります。 -**開発版です。** `develop` にだけ載ります。取得元へ `#develop` を足す手順は -[docs/versioning-and-distribution.md の「開発版を試す」](../../docs/versioning-and-distribution.md#開発版を試す)にあります。 +**正式版です。** `main` に載ります。中身は開発版 `10.17.0-dev.1` と同じで、版数の接尾辞だけを +外しました。 | 変わったこと | 中身 | | --- | --- | @@ -321,7 +321,7 @@ agy models # 認証の確認 ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/10.17.0-dev.1/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/10.17.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -343,14 +343,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/10.17.0-dev.1/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/10.17.0/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 10.17.0-dev.1 +# => ndf@ai-plugins installed, enabled 10.17.0 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 9ac27bd9f..5fd2b7a8b 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", - "version": "10.17.0-dev.1", - "description": "Antigravity CLI plugin (v10.17.0-dev.1): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "version": "10.17.0", + "description": "Antigravity CLI plugin (v10.17.0): 43 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." }