diff --git a/docs/guide/README.md b/docs/guide/README.md index ab75b820..3107b3a5 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -29,8 +29,9 @@ Recommended route / 推奨順路: conflicts - [Plan YAML editing](en/plan-yaml.md) — commented templates, offline validation, next commands, and recovery diffs -- [Slash commands](en/slash-commands.md) — all 24 accepted command names, - inline flags, file expansion, and profile inference +- [Slash commands](en/slash-commands.md) — all 24 accepted command names in the + general registry, plus the Gate 3/4 dedicated `/directive` and + `/confirm-directive`, inline flags, file expansion, and profile inference - [Configuration](en/configuration.md) — precedence, presets, paths, legacy files, and environment variables - [Providers](en/providers.md) — Ollama, LM Studio, OpenAI, and Gemini setup @@ -55,8 +56,9 @@ walkthrough. Read the [security model](../../SECURITY.md) before enabling - [CLI リファレンス](ja/cli-reference.md) — 公開されている全 66 フラグ、既定値、排他関係 - [Plan YAML の編集](ja/plan-yaml.md) — コメント付き template、offline 検証、次コマンド、 recovery 差分 -- [スラッシュコマンド](ja/slash-commands.md) — 受け付ける全 24 コマンド名、インラインフラグ、 - ファイル展開、プロファイル推論 +- [スラッシュコマンド](ja/slash-commands.md) — 一般レジストリで受け付ける全 24 コマンド名と + Gate 3/4 専用の `/directive`・`/confirm-directive`、インラインフラグ、ファイル展開、 + プロファイル推論 - [設定](ja/configuration.md) — 優先順位、preset、探索パス、旧形式ファイル、環境変数 - [プロバイダ](ja/providers.md) — Ollama、LM Studio、OpenAI、Gemini のセットアップ - [トラブルシューティング](ja/troubleshooting.md) — 起動時、プロバイダ、TUI の一般的な問題 diff --git a/docs/guide/en/cli-reference.md b/docs/guide/en/cli-reference.md index 8ba21254..5563c910 100644 --- a/docs/guide/en/cli-reference.md +++ b/docs/guide/en/cli-reference.md @@ -10,15 +10,17 @@ authority: use `commandagent --help` when its version differs from this checkout ## Invocation Use one of the action-selector flags for a direct command, or omit all of them -for the TUI. The action selectors are `--prompt`, `--plan-steps`, `--plan-run`, -`--run-plan`, `--ultra-plan`, `--ultra-plan-run`, `--run-ultra-plan`, -`--validate-plan`, `--setup-interaction-probe`, `--runs`, `--ux-demo`, `--model-probe`, -`--doctor`, and `--extensions`. The offline pack actions `--packs`, `--pack-verify`, and -`--pack-pin`, generated-artifact actions `--completions` and `--generate-man`, -config action `--init-config`, and delegated manifest actions -`--validate-manifest` and `--init-profile` are displayed in the same -`Actions (use one)` help group. CommandAgent rejects combinations whose action -contracts are mutually exclusive. +for the TUI. The action selectors are `--workflow`, `--prompt`, `--plan-steps`, +`--plan-run`, `--run-plan`, `--ultra-plan`, `--ultra-plan-run`, +`--run-ultra-plan`, `--validate-plan`, `--setup-interaction-probe`, `--runs`, +`--ux-demo`, `--model-probe`, `--doctor`, and `--extensions`. The offline pack +actions `--packs`, `--pack-verify`, and `--pack-pin`, generated-artifact actions +`--completions` and `--generate-man`, config action `--init-config`, and +delegated manifest actions `--validate-manifest` and `--init-profile` are +displayed in the same `Actions (use one)` help group. CommandAgent rejects +combinations whose action contracts are mutually exclusive, either in Clap +before parsing finishes or in the resolved configuration after parsing; see +[Conflicts and combinations](#conflicts-and-combinations). Clap also generates `-h`/`--help` and `-V`/`--version`. They are not part of the 66 application flags below. The hidden `--completion-contract-json ` is an @@ -112,6 +114,14 @@ least eight hexadecimal digits, such as `/confirm sha256:77cd5e23`. Set full hash. Prefixes are expanded to the frozen full hash before confirmation is persisted. +`/provider` switches only among the four built-in providers `ollama`, +`lm-studio`, `openai`, and `gemini`. The generic `openai-compatible` provider, +including its base URL and key variable, is configured before startup with +`--provider`, `--base-url`, and `--api-key-env`, or a preset; change it there +and restart CommandAgent instead of using `/provider`. When the executor +provider is OpenAI, `/model ` and a `/provider openai` switch reject the +ambiguous `gpt-5.6` alias with the same check the CLI applies at startup. + REPL history is isolated beneath `--state-dir` at `workspace-history/.txt`. Only the active workspace file is loaded, and history hints require two entered characters and @@ -143,20 +153,32 @@ See [Configuration](configuration.md) for the exact per-field layers. - `--allow` accepts `read`, `write`, and `bash:verify` as repeated or comma-separated values. Once supplied, omitted tool classes are blocked; `--yes` is the backward-compatible all-tools alias. -- Only one action selector may be used. This is checked after parsing and fails - with `only one action selector can be used at a time`. +- Clap rejects some combinations while parsing: `--workflow` conflicts with + `--intent`; `--origin` requires `--workflow`; the `--doctor`/`--extensions`/ + `--runs` JSON group and the pack direct-action group accept only one member + each. For example, `--doctor --runs` is a Clap-level conflict. +- After parsing, the resolved configuration accepts exactly one action selector + and fails with `only one action selector can be used at a time`. For example, + `--prompt x --ux-demo` reaches that check because each flag is a valid + selector on its own. `--workflow` without `--origin` also fails here, with + `--workflow requires --origin`. - `--completions` and `--generate-man` are Clap-level conflicts with every other action selector and with a trailing goal, so a combined invocation is rejected before generation begins. - Pre-run argument and configuration rejections exit `2` before a run starts and produce no run summary; see [headless execution](../../user/headless.md). + `--doctor` is the exception: it follows its diagnostic path, records the + failed Configuration check in its report (including JSON), and exits `1` + instead of `2`. For example, `--ux-demo --doctor --json` exits `1` with the + JSON report rather than the usual exit `2`. - `--packs`, `--pack-verify`, and `--pack-pin` are Clap-level direct actions. They conflict with one another, run action selectors, `--pack`, and `--pack-hash`. Listing allows `--extension-root`, while verify and pin take their target directory directly. - `--extensions` is read-only and accepts `--extension-root`; when omitted, - it resolves the top-level `extension_root` setting. `--json` is accepted - with either `--extensions` or `--doctor`. + it resolves the top-level `extension_root` setting. `--json` applies to + `--doctor`, `--extensions`, and `--runs`; using `--json` without one of those + targets is rejected. - `--plan-steps`, `--plan-run`, `--ultra-plan`, and `--ultra-plan-run` require a trailing goal. - `--validate-plan` is an offline, read-only action and conflicts with every @@ -164,6 +186,12 @@ See [Configuration](configuration.md) for the exact per-field layers. recovery UltraPlan YAML; see [Plan YAML editing](plan-yaml.md). - A different `--planner-provider` requires an explicit or preset `planner_model`; otherwise startup fails. +- A role that resolves to the generic provider needs its base URL: pass + `--base-url` or set preset `base_url` when the executor, planner, or + classifier is `openai-compatible`, including a preset that sets only the + classifier to the generic provider. With no `openai-compatible` role, + `--base-url` and `--api-key-env` are rejected by the resolved configuration. + These are not Clap `requires` relationships. - `--think` requires at least one resolved provider role to use Ollama. When both roles use another provider, startup fails instead of ignoring the flag. - For direct minimal-loop prompts, `--fresh-session` takes precedence over diff --git a/docs/guide/en/configuration.md b/docs/guide/en/configuration.md index 933ceea7..c08c151c 100644 --- a/docs/guide/en/configuration.md +++ b/docs/guide/en/configuration.md @@ -259,12 +259,22 @@ variables also affect normal user-visible behavior: | `XDG_STATE_HOME` | Changes the base used by the default state directory. | | `HOME` | Supplies user config paths and the fallback state-directory home. | | `LC_ALL`, then `LANG` | Determines whether the spinner uses UTF-8 frames. | +| `COMMANDAGENT_STEP_WALL_CLOCK_CAP_MS` / `ANVIL_STEP_WALL_CLOCK_CAP_MS` | Sets the operational per-step wall-clock cap in milliseconds. A `RunSessionOptions` value takes precedence over the environment. The value is trimmed and parsed as a `u64`; an unset or unparsable value uses the 15-minute default, and `0` is accepted by the parser. | +| `COMMANDAGENT_PACK_DIRECTORY` | Internal handoff for the selected pack directory. Configure it with `--pack` or a preset instead of setting it by hand. | +| `COMMANDAGENT_PACK_ID` | Internal handoff for the selected pack ID. Configure it with `--pack` or a preset instead of setting it by hand. | +| `COMMANDAGENT_PACK_VERSION` | Internal handoff for the selected pack version. Configure it with `--pack` or a preset instead of setting it by hand. | +| `COMMANDAGENT_PACK_HASH` | Internal handoff for the selected pack exact-byte hash. Configure it with `--pack` or a preset instead of setting it by hand. | +| `COMMANDAGENT_UX_DEMO_FAST` / `ANVIL_UX_DEMO_FAST` | Speeds up the `--ux-demo` presentation waits for development and demos. The variable's presence, not its value, is tested. | For `COMMANDAGENT_*` entries, the current name wins. The matching `ANVIL_*` name is used only when the current name is absent, and emits a one-time deprecation warning. The legacy names in the table are nevertheless the exact currently supported spellings. +The four `COMMANDAGENT_PACK_*` names are the exception: they are read directly +and have no `ANVIL_*` legacy name. They are an internal handoff for the selected +pack, not a user-configured setting; use `--pack` or a preset to select a pack. + ## Inspect the effective configuration Start the TUI and run `/status`. The startup banner and status card show key diff --git a/docs/guide/en/providers.md b/docs/guide/en/providers.md index 2e251145..41557afa 100644 --- a/docs/guide/en/providers.md +++ b/docs/guide/en/providers.md @@ -108,6 +108,13 @@ snapshot-qualified ID when repeatable comparisons matter. CommandAgent records the requested and returned model IDs plus `system_fingerprint` in the provider turn event so endpoint drift can be audited without exposing credentials. +The `gpt-5.6` alias rejection applies to every role that uses the built-in +OpenAI provider: executor, planner, and classifier. It is a check on the +built-in OpenAI provider only and is not applied to the generic +`openai-compatible` transport, which forwards the model ID unchanged. In the +REPL, `/model ` and a `/provider openai` switch run the executor check again +before the new selection is applied. + OpenAI Chat Completions reasoning effort is opt-in. Set `COMMANDAGENT_OPENAI_REASONING_EFFORT` in the process environment only when an explicit effort value is required. If it is unset or empty, CommandAgent omits @@ -180,6 +187,12 @@ LM Studio uses this same transport with its existing `lm-studio`, `--lm-studio-host`, and `LM_STUDIO_API_TOKEN` names. Those settings remain backward compatible and are not automatically rewritten to the generic form. +The interactive `/provider` command accepts only `ollama`, `lm-studio`, +`openai`, and `gemini`, so it cannot select this generic provider. Configure it +for a role with `--provider`, `--base-url`, and `--api-key-env`, or a preset, +and restart CommandAgent to change it. Whenever the executor, planner, or +classifier resolves to `openai-compatible`, its base URL is required. + ## Ollama host and models Ollama requires a running HTTP server and a locally available model. Local API diff --git a/docs/guide/en/slash-commands.md b/docs/guide/en/slash-commands.md index adb8c4a8..14ab5d0b 100644 --- a/docs/guide/en/slash-commands.md +++ b/docs/guide/en/slash-commands.md @@ -8,16 +8,18 @@ source of truth for the installed binary. ## Command reference -The registry contains 23 primary entries. `/quit` is a separately accepted -command name and an alias of `/exit`, giving 24 accepted names in total. +The general command registry contains 23 primary entries. `/quit` is a separately +accepted command name and an alias of `/exit`, giving 24 accepted names in total. +Two more names, `/directive` and `/confirm-directive`, are handled outside this +registry; see the Gate 3/4 dedicated commands section below. | Command name | Usage shown by `/help` | Behavior | | --- | --- | --- | | `/help` | `/help [command]` | Show grouped commands, or detailed usage and an example for one command. | | `/confirm` | `/confirm ` | Persist the reviewed Gate 1 card and immediately execute its request. A matching `sha256:` prefix needs at least eight hexadecimal digits unless strict confirmation is enabled. | | `/status` | `/status` | Show current execution first, followed by effective session configuration and readiness. | -| `/model` | `/model ` | Set the executor model used by new Gate 1 cards in this REPL session. | -| `/provider` | `/provider ` | Set the executor provider used by new Gate 1 cards in this REPL session. | +| `/model` | `/model ` | Set the executor model used by new Gate 1 cards in this REPL session. An OpenAI model ID is validated with the same policy as startup. | +| `/provider` | `/provider ` | Set the executor provider used by new Gate 1 cards in this REPL session. Accepts only `ollama`, `lm-studio`, `openai`, and `gemini`; the generic `openai-compatible` provider is not selectable here. | | `/profile` | `/profile ` | Set the explicit profile used by new Gate 1 cards in this REPL session. | | `/clear` | `/clear` | Clear the terminal screen without discarding the most recent result. | | `/last` | `/last` | Render the most recent REPL result again. | @@ -51,6 +53,29 @@ The full hash is always accepted. By default, a canonical `sha256:` prefix of latest pending Gate 1 card. `COMMANDAGENT_STRICT_CONFIRM=1` restores full-hash matching. The full frozen hash, never the prefix, is persisted and emitted. +`/provider` switches only among the four built-in providers (`ollama`, +`lm-studio`, `openai`, and `gemini`). The generic `openai-compatible` provider +—including its base URL and key variable—is configured before startup with the +`--provider`, `--base-url`, and `--api-key-env` flags or a preset. To change it, +adjust the configuration and restart CommandAgent rather than using `/provider`. + +## Gate 3/4 dedicated commands + +`/directive` and `/confirm-directive` are handled outside the general command +registry. `/help`, `/help `, and the command table above therefore do +not list them. They are available only after a confirmed terminal run reaches +Gate 3 or Gate 4; at any other time the REPL refuses them. + +- `/directive ` proposes a bounded human directive for the current + Gate 3 or Gate 4 terminal run. The REPL prints the proposal with its directive + hash and instructs you to confirm it. +- `/confirm-directive ` confirms a pending directive by its exact, + complete hash and prepares the continuation. + +The exact displayed directive hash is required. The Gate 1 `sha256:` prefix +matching described above does not apply to directives, and a directive is not +dispatched before this confirmation. + ## Inline flags Slash input recognizes these overrides before it joins the remaining words into diff --git a/docs/guide/en/troubleshooting.md b/docs/guide/en/troubleshooting.md index 122e9a0f..4840b1a6 100644 --- a/docs/guide/en/troubleshooting.md +++ b/docs/guide/en/troubleshooting.md @@ -94,10 +94,15 @@ terminal can clear a scroll region left by the terminal emulator. ## Model ID does not exist CommandAgent normally passes model IDs to the selected provider without a -cross-provider catalog. The one guarded alias is OpenAI executor ID `gpt-5.6`, -which is rejected before dispatch; use exact `gpt-5.6-luna` or an available -snapshot-qualified Luna/Sol ID. Other nonexistent, unavailable, or unauthorized -models fail at provider-call time. +cross-provider catalog. The one guarded alias is the built-in OpenAI model ID +`gpt-5.6`, which is rejected before dispatch for every OpenAI role — executor, +planner, and classifier. Use an exact `gpt-5.6-luna`, `gpt-5.6-terra`, or +`gpt-5.6-sol` ID, or an available snapshot-qualified Luna/Sol ID. This check +applies only to the built-in OpenAI provider, not to the generic +`openai-compatible` transport, which forwards the model ID unchanged. In the +REPL, `/model ` and a `/provider openai` switch repeat the executor check. +Other nonexistent, unavailable, or unauthorized models fail at provider-call +time. ### What the failure looks like diff --git a/docs/guide/ja/cli-reference.md b/docs/guide/ja/cli-reference.md index 840839ea..3fdb67b3 100644 --- a/docs/guide/ja/cli-reference.md +++ b/docs/guide/ja/cli-reference.md @@ -10,14 +10,16 @@ ## 呼び出し方 直接コマンドを実行するにはアクション選択フラグを 1 つ使い、TUI を使う場合はすべて省略します。 -アクション選択フラグは `--prompt`、`--plan-steps`、`--plan-run`、`--run-plan`、 +アクション選択フラグは `--workflow`、`--prompt`、`--plan-steps`、`--plan-run`、`--run-plan`、 `--ultra-plan`、`--ultra-plan-run`、`--run-ultra-plan`、 `--validate-plan`、`--setup-interaction-probe`、`--runs`、`--ux-demo`、`--model-probe`、 `--doctor`、`--extensions` です。 オフライン pack アクションの `--packs`、`--pack-verify`、`--pack-pin`、生成アクションの `--completions` と `--generate-man`、設定アクションの `--init-config`、委譲された manifest アクションの `--validate-manifest` と `--init-profile` も、help の -`Actions (use one)` グループに表示します。相互排他の action contract を組み合わせると拒否されます。 +`Actions (use one)` グループに表示します。相互排他の action contract を組み合わせると、Clap が +構文解析中に拒否するものと、構文解析後の解決済み設定が拒否するものがあります。詳細は +[排他関係と組み合わせ](#排他関係と組み合わせ)を参照してください。 Clap が生成する `-h`/`--help` と `-V`/`--version` は、以下のアプリケーション固有の 66 フラグには含めません。非表示の `--completion-contract-json ` は内部連携用であり、 @@ -109,6 +111,13 @@ identity を維持します。実行時の使用法と例は、グループ化 `COMMANDAGENT_STRICT_CONFIRM=1` を設定すると完全 hash を必須にできます。前方一致は、 confirmation を保存する前に固定済みの完全 hash へ展開されます。 +`/provider` が切り替えられるのは組み込みの 4 名 `ollama`、`lm-studio`、`openai`、`gemini` +だけです。汎用 `openai-compatible` は base URL やキー変数を含めて起動前に +`--provider`、`--base-url`、`--api-key-env`、または preset で設定します。変更する場合は +`/provider` ではなく設定を見直して CommandAgent を再起動してください。executor プロバイダが +OpenAI のとき、`/model ` と `/provider openai` への切替は、起動時と同じ検査で曖昧な +alias `gpt-5.6` を拒否します。 + REPL 履歴は `--state-dir` 配下の `workspace-history/.txt` に分離されます。アクティブな workspace のファイルだけを読み込み、履歴ヒントは 2 文字以上の入力後に 1 行へ収まる長さで @@ -147,24 +156,38 @@ context budget、timeout、profile、footer、stream などは `Config::from_cli canonical target がワークスペース外になる path も Read/Write/Edit/Grep で拒否され、 広域列挙では秘密本文を出さずにポリシー除外を知らせます。詳細は [セキュリティモデル](../../../SECURITY.md) を参照してください。 -- アクション選択フラグは 1 つだけ使用できます。構文解析後に検査され、違反すると - `only one action selector can be used at a time` で失敗します。 +- Clap は構文解析中に一部の組み合わせを拒否します。`--workflow` は `--intent` と排他で、 + `--origin` には `--workflow` が必要です。`--doctor`/`--extensions`/`--runs` の JSON + グループと pack 直接アクショングループは、それぞれ 1 つだけ受け付けます。例えば + `--doctor --runs` は Clap レベルの排他です。 +- 構文解析後、解決済み設定はアクション選択フラグを 1 つだけ受け付け、違反すると + `only one action selector can be used at a time` で失敗します。例えば `--prompt x --ux-demo` + は、どちらも単独では有効な selector のため、この検査に到達します。`--origin` なしの + `--workflow` もここで `--workflow requires --origin` として失敗します。 - `--completions` と `--generate-man` は、他のすべてのアクション選択フラグおよび 末尾のゴールと Clap レベルで排他です。併用した呼び出しは生成を始める前に拒否されます。 - 実行前の引数・設定の拒否は、run の開始前に exit `2` で終了し、run summary を 生成しません。詳細は [headless 実行](../../user/headless.md)を参照してください。 + `--doctor` は例外で、診断経路を通り、失敗した Configuration 検査を report(JSON を含む)に + 記録して exit `1` になります。例えば `--ux-demo --doctor --json` は、通常の exit `2` ではなく + JSON report とともに exit `1` になります。 - `--packs`、`--pack-verify`、`--pack-pin` は Clap レベルの直接アクションです。 相互、run アクション、`--pack`、`--pack-hash` と排他です。一覧では `--extension-root` を使えますが、verify と pin は対象 directory を直接取ります。 - `--extensions` は read-only で `--extension-root` を受け付けます。省略時は - トップレベルの `extension_root` 設定を解決します。`--json` は - `--extensions` と `--doctor` のどちらでも利用できます。 + トップレベルの `extension_root` 設定を解決します。`--json` は `--doctor`、 + `--extensions`、`--runs` に適用され、これらの対象なしで使うと拒否されます。 - `--plan-steps`、`--plan-run`、`--ultra-plan`、`--ultra-plan-run` には末尾のゴールが必要です。 - `--validate-plan` は offline かつ read-only の action で、実行 action および生成物 action の すべてと排他です。step plan、UltraPlan、recovery UltraPlan YAML を受け付けます。詳細は [Plan YAML の編集](plan-yaml.md)を参照してください。 - `--planner-provider` が実行プロバイダと異なる場合、明示または preset の `planner_model` が必要です。なければ起動に失敗します。 +- 汎用プロバイダに解決される役割には base URL が必要です。executor、planner、classifier の + いずれかが `openai-compatible` の場合は `--base-url` または preset の `base_url` を指定します。 + preset で classifier だけを汎用プロバイダにする場合も含みます。汎用プロバイダの役割がない + 状態で `--base-url` や `--api-key-env` を指定すると、解決済み設定が拒否します。これらは + Clap の `requires` ではありません。 - `--think` には、解決後の provider 役割の少なくとも一方で Ollama が必要です。 両方が別プロバイダの場合、フラグを無視せず起動に失敗します。 - 直接 minimal-loop prompt では `--fresh-session` が `--resume` より優先されます。 diff --git a/docs/guide/ja/configuration.md b/docs/guide/ja/configuration.md index f95b0675..4be5d98f 100644 --- a/docs/guide/ja/configuration.md +++ b/docs/guide/ja/configuration.md @@ -274,10 +274,20 @@ OpenAI キーと任意の `LM_STUDIO_API_TOKEN` はプロセス環境だけか | `XDG_STATE_HOME` | 既定の state directory が使うベースを変更します。 | | `HOME` | ユーザー設定パスと state directory fallback のホームを指定します。 | | `LC_ALL`、次に `LANG` | spinner が UTF-8 frame を使うか決めます。 | +| `COMMANDAGENT_STEP_WALL_CLOCK_CAP_MS` / `ANVIL_STEP_WALL_CLOCK_CAP_MS` | step の時間上限をミリ秒で指定する運用向け設定です。`RunSessionOptions` の指定が環境値より優先されます。値は trim して `u64` として解析し、未設定または解析不能なら既定の 15 分を使います。`0` も解析上は有効です。 | +| `COMMANDAGENT_PACK_DIRECTORY` | 選択済み pack の directory を内部で受け渡します。手で設定せず、`--pack` または preset で指定してください。 | +| `COMMANDAGENT_PACK_ID` | 選択済み pack の ID を内部で受け渡します。手で設定せず、`--pack` または preset で指定してください。 | +| `COMMANDAGENT_PACK_VERSION` | 選択済み pack の version を内部で受け渡します。手で設定せず、`--pack` または preset で指定してください。 | +| `COMMANDAGENT_PACK_HASH` | 選択済み pack の exact-byte hash を内部で受け渡します。手で設定せず、`--pack` または preset で指定してください。 | +| `COMMANDAGENT_UX_DEMO_FAST` / `ANVIL_UX_DEMO_FAST` | 開発・デモ用に `--ux-demo` の待機を短くします。値の真偽ではなく変数の存在を判定します。 | `COMMANDAGENT_*` 項目では現在名が優先されます。現在名がない場合だけ対応する `ANVIL_*` 名を使い、 1 回だけ非推奨警告を出します。それでも表中の旧名は、現在サポートされている正確な綴りです。 +`COMMANDAGENT_PACK_*` の 4 変数は例外で、直接読み込まれ、`ANVIL_*` の旧名を持ちません。これらは +選択済み pack の内部受け渡しであり、利用者向け設定ではありません。pack の選択には `--pack` または +preset を使ってください。 + ## 実効設定の確認 TUI を起動して `/status` を実行してください。起動 banner と status card は、`flag`、 diff --git a/docs/guide/ja/providers.md b/docs/guide/ja/providers.md index 2e355ed2..63b5dd87 100644 --- a/docs/guide/ja/providers.md +++ b/docs/guide/ja/providers.md @@ -108,6 +108,11 @@ Responses の native-tool turnでは、providerが返したreasoning output item function outputとともに再送します。response ID、service tier、cached input token、reasoning token数は provider turn eventへ記録します。 +`gpt-5.6` alias の拒否は、組み込み OpenAI provider を使うすべての役割、すなわち executor、 +planner、classifier に適用されます。これは組み込み OpenAI provider に対する検査であり、model ID を +そのまま転送する汎用 `openai-compatible` transport には適用しません。REPL では `/model ` と +`/provider openai` への切替が、新しい選択を適用する前に executor の検査を再実行します。 + ## LM Studio のサーバーとモデル LM Studio の Developer タブまたは `lms` でサーバーを起動し、`/v1/models` が返す正確な @@ -162,6 +167,12 @@ tool call を安定して実装しない場合は `--tool-protocol text` を使 LM Studio は同じ transport を既存の `lm-studio`、`--lm-studio-host`、 `LM_STUDIO_API_TOKEN` 名で使います。これらは後方互換のまま維持され、汎用形式へ自動変換されません。 +対話型の `/provider` が受け付けるのは `ollama`、`lm-studio`、`openai`、`gemini` の 4 名だけで、 +この汎用 provider は選べません。役割に設定するには `--provider`、`--base-url`、 +`--api-key-env`、または preset を使い、変更する場合は CommandAgent を再起動してください。 +executor、planner、classifier のいずれかが `openai-compatible` に解決される場合、その base URL が +必要です。 + ## Ollama のホストとモデル Ollama には実行中の HTTP サーバーとローカルで利用可能なモデルが必要です。既定アドレスへの diff --git a/docs/guide/ja/slash-commands.md b/docs/guide/ja/slash-commands.md index f3073b67..dbaaed47 100644 --- a/docs/guide/ja/slash-commands.md +++ b/docs/guide/ja/slash-commands.md @@ -7,16 +7,18 @@ ## コマンド一覧 -レジストリには主コマンドが 23 件あります。`/quit` は `/exit` の別名として独立して受け付ける -コマンド名なので、受け付ける名前は合計 24 件です。 +一般コマンドレジストリには主コマンドが 23 件あります。`/quit` は `/exit` の別名として独立して +受け付けるコマンド名なので、受け付ける名前は合計 24 件です。さらに `/directive` と +`/confirm-directive` の 2 名はこのレジストリの外で処理されます。後述の Gate 3/4 専用コマンドの +節を参照してください。 | コマンド名 | `/help` に表示される使用法 | 動作 | | --- | --- | --- | | `/help` | `/help [command]` | グループ化したコマンドを表示するか、1 コマンドの詳しい使用法と例を表示します。 | | `/confirm` | `/confirm ` | 確認した Gate 1 カードを保存し、その依頼を直ちに実行します。strict confirmation が無効なら、8 桁以上の 16 進数を持つ一致した `sha256:` 前方一致も使えます。 | | `/status` | `/status` | 現在の実行を先に、その後で実効セッション設定と readiness を表示します。 | -| `/model` | `/model ` | この REPL セッションで新しい Gate 1 カードに使う executor model を設定します。 | -| `/provider` | `/provider ` | この REPL セッションで新しい Gate 1 カードに使う executor provider を設定します。 | +| `/model` | `/model ` | この REPL セッションで新しい Gate 1 カードに使う executor model を設定します。OpenAI の model ID は起動時と同じ方針で検証します。 | +| `/provider` | `/provider ` | この REPL セッションで新しい Gate 1 カードに使う executor provider を設定します。受け付けるのは `ollama`、`lm-studio`、`openai`、`gemini` の 4 名だけで、汎用 `openai-compatible` はここでは選べません。 | | `/profile` | `/profile ` | この REPL セッションで新しい Gate 1 カードに使う明示 profile を設定します。 | | `/clear` | `/clear` | 直近の結果を破棄せずに端末画面を消去します。 | | `/last` | `/last` | 直近の REPL 結果を再表示します。 | @@ -49,6 +51,25 @@ plan コマンドの失敗は REPL を終了せずに報告されます。 `COMMANDAGENT_STRICT_CONFIRM=1` で完全 hash 一致へ戻せます。保存と event 出力には前方一致では なく、固定済みの完全 hash を使います。 +`/provider` が切り替えられるのは組み込みの 4 名(`ollama`、`lm-studio`、`openai`、`gemini`) +だけです。汎用 `openai-compatible` は base URL やキー変数を含めて起動前に +`--provider`、`--base-url`、`--api-key-env` フラグまたは preset で設定します。変更する場合は +`/provider` を使わず、設定を見直して CommandAgent を再起動してください。 + +## Gate 3/4 専用コマンド + +`/directive` と `/confirm-directive` は一般コマンドレジストリの外で処理されます。そのため +`/help`、`/help `、上記のコマンド表には表示されません。確認済みの terminal run が +Gate 3 または Gate 4 に達した後だけ利用でき、それ以外のときは REPL が拒否します。 + +- `/directive ` は、現在の Gate 3 または Gate 4 の terminal run に対して + 限定された human directive を提案します。REPL は directive hash とともに提案を表示し、 + 確認するよう案内します。 +- `/confirm-directive ` は、確認待ちの提案を完全で正確な hash で確認し、継続を準備します。 + +表示された directive hash の完全一致が必要です。上記の Gate 1 の `sha256:` 前方一致は +directive には適用されず、この確認より前に directive が dispatch されることもありません。 + ## インラインフラグ スラッシュ入力では、残りの単語をゴールに結合する前に次の上書きを認識します。 diff --git a/docs/guide/ja/troubleshooting.md b/docs/guide/ja/troubleshooting.md index 143cfde3..be2f0cab 100644 --- a/docs/guide/ja/troubleshooting.md +++ b/docs/guide/ja/troubleshooting.md @@ -87,9 +87,13 @@ scrollback の breadcrumb は残ります。`--no-footer`、トップレベル ## Model ID が存在しない CommandAgent は通常、プロバイダ横断の catalog を持たず model ID を選択したプロバイダへ渡します。 -唯一警備するaliasはOpenAI executor ID `gpt-5.6`で、dispatch前に拒否します。厳密ID -`gpt-5.6-luna`または利用可能なsnapshot付きLuna/Sol IDを使ってください。それ以外の存在しない、 -利用できない、権限がない model はプロバイダ呼び出し時に失敗します。 +唯一警備するaliasは組み込み OpenAI の model ID `gpt-5.6` で、executor、planner、classifier の +すべての OpenAI 役割で dispatch 前に拒否します。厳密ID `gpt-5.6-luna`、`gpt-5.6-terra`、 +`gpt-5.6-sol`、または利用可能な snapshot 付き Luna/Sol ID を使ってください。この検査は +組み込み OpenAI provider だけに適用され、model ID をそのまま転送する汎用 `openai-compatible` +transport には適用しません。REPL では `/model ` と `/provider openai` への切替が +executor の検査を再実行します。それ以外の存在しない、利用できない、権限がない model は +プロバイダ呼び出し時に失敗します。 ### 失敗の見え方 diff --git a/src/provider_cli.rs b/src/provider_cli.rs index 1c63b466..48a38ea9 100644 --- a/src/provider_cli.rs +++ b/src/provider_cli.rs @@ -1,6 +1,7 @@ use std::ffi::{OsStr, OsString}; use clap::builder::{EnumValueParser, PossibleValue, TypedValueParser}; +use clap::error::{ContextKind, ContextValue}; use clap::{Arg, CommandFactory, FromArgMatches, ValueEnum}; use crate::cli::{Cli, ProviderArg}; @@ -29,7 +30,13 @@ impl TypedValueParser for ProviderValueParser { if value == OPENAI_COMPATIBLE { return Ok(ProviderArg::LmStudio); } - EnumValueParser::::new().parse_ref(command, argument, value) + match EnumValueParser::::new().parse_ref(command, argument, value) { + Ok(value) => Ok(value), + Err(mut error) => { + augment_possible_values(&mut error); + Err(error) + } + } } fn possible_values(&self) -> Option + '_>> { @@ -42,6 +49,22 @@ impl TypedValueParser for ProviderValueParser { } } +/// Report the generic `openai-compatible` value in an invalid-value error. +/// +/// `EnumValueParser` builds its candidate list from [`ProviderArg`], which has +/// four built-in variants, so an invalid `--provider` value would otherwise +/// omit `openai-compatible`. Only the rendered candidate list is extended; the +/// error kind, message, and exit code keep the parser's behavior. +fn augment_possible_values(error: &mut clap::Error) { + let Some(ContextValue::Strings(mut values)) = error.remove(ContextKind::ValidValue) else { + return; + }; + if !values.iter().any(|value| value == OPENAI_COMPATIBLE) { + values.push(OPENAI_COMPATIBLE.to_string()); + } + error.insert(ContextKind::ValidValue, ContextValue::Strings(values)); +} + pub fn command() -> clap::Command { Cli::command() .mut_arg("provider", |argument| { @@ -140,4 +163,24 @@ mod tests { assert!(help.contains(expected), "missing {expected} in {help}"); } } + + #[test] + fn invalid_provider_error_lists_generic_candidate() { + for flag in ["--provider", "--planner-provider"] { + let error = parse_from(["commandagent", flag, "not-a-provider"]).unwrap_err(); + assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); + assert_eq!(error.exit_code(), 2); + let rendered = error.to_string(); + assert!( + rendered.contains("invalid value 'not-a-provider'"), + "{rendered}" + ); + for candidate in ["ollama", "lm-studio", "openai", "gemini", OPENAI_COMPATIBLE] { + assert!( + rendered.contains(candidate), + "missing {candidate}: {rendered}" + ); + } + } + } } diff --git a/tests/issue519_docs_runtime_alignment.rs b/tests/issue519_docs_runtime_alignment.rs new file mode 100644 index 00000000..5414e189 --- /dev/null +++ b/tests/issue519_docs_runtime_alignment.rs @@ -0,0 +1,647 @@ +//! Issue #519 — align slash-command, provider, exclusivity, and environment +//! documentation with the shipped CLI, configuration, and boundary shell. +//! +//! The test pins only the four documented drifts (FIN-032..FIN-035). It does +//! not call a real provider: the CLI is parsed in-process, the binary cases use +//! an empty inherited environment and a temporary workspace, and the boundary +//! shell uses a temporary state directory. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use clap::Parser; +use clap::error::ErrorKind; + +use commandagent::cli::Cli; +use commandagent::planner::adjudication::contract::IntentId; +use commandagent::planner::profile::ProfileId; +use commandagent::tui::boundary_shell::ambiguity::{ + ClassifierProvenance, ProposalStatus, RouteProposal, +}; +use commandagent::tui::boundary_shell::confirmation::{ExecutionPins, PackSelection}; +use commandagent::tui::boundary_shell::family_catalog::TaskFamilyId; +use commandagent::tui::boundary_shell::route::{RouteBasis, RouteCandidate}; +use commandagent::tui::boundary_shell::{BoundaryShell, BoundaryState}; +use commandagent::tui::slash::{SLASH_COMMANDS, render_help}; + +const EN_CLI: &str = "docs/guide/en/cli-reference.md"; +const JA_CLI: &str = "docs/guide/ja/cli-reference.md"; +const EN_SLASH: &str = "docs/guide/en/slash-commands.md"; +const JA_SLASH: &str = "docs/guide/ja/slash-commands.md"; +const EN_PROVIDERS: &str = "docs/guide/en/providers.md"; +const JA_PROVIDERS: &str = "docs/guide/ja/providers.md"; +const EN_CONFIG: &str = "docs/guide/en/configuration.md"; +const JA_CONFIG: &str = "docs/guide/ja/configuration.md"; +const EN_TROUBLE: &str = "docs/guide/en/troubleshooting.md"; +const JA_TROUBLE: &str = "docs/guide/ja/troubleshooting.md"; +const GUIDE_INDEX: &str = "docs/guide/README.md"; + +const PROVIDER_CANDIDATES: [&str; 5] = [ + "ollama", + "lm-studio", + "openai", + "gemini", + "openai-compatible", +]; + +const ENV_VARIABLES: [&str; 6] = [ + "COMMANDAGENT_STEP_WALL_CLOCK_CAP_MS", + "COMMANDAGENT_PACK_DIRECTORY", + "COMMANDAGENT_PACK_ID", + "COMMANDAGENT_PACK_VERSION", + "COMMANDAGENT_PACK_HASH", + "COMMANDAGENT_UX_DEMO_FAST", +]; + +fn repo_path(relative: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(relative) +} + +fn read_repo_file(relative: &str) -> String { + fs::read_to_string(repo_path(relative)) + .unwrap_or_else(|err| panic!("failed to read {relative}: {err}")) +} + +fn markdown_section<'a>(markdown: &'a str, heading: &str, path: &str) -> &'a str { + let marker = format!("{heading}\n"); + let (_, after_heading) = markdown + .split_once(&marker) + .unwrap_or_else(|| panic!("missing heading '{heading}' in {path}")); + let end = after_heading.find("\n## ").unwrap_or(after_heading.len()); + &after_heading[..end] +} + +/// Create a temporary directory under the Cargo target tree so tests never +/// write under the OS temp locations other workers share. +fn test_tempdir() -> tempfile::TempDir { + tempfile::tempdir_in(env!("CARGO_TARGET_TMPDIR")).expect("temporary test directory") +} + +/// Run the shipped binary with no inherited environment beyond `PATH`, a +/// temporary `HOME`/state directory, and a temporary workspace. +fn run_commandagent(arguments: &[&str], workspace: &Path) -> Output { + let home = test_tempdir(); + Command::new(env!("CARGO_BIN_EXE_commandagent")) + .args(arguments) + .env_clear() + .env("PATH", std::env::var_os("PATH").unwrap_or_default()) + .env("HOME", home.path()) + .env("XDG_STATE_HOME", home.path().join("state")) + .current_dir(workspace) + .output() + .expect("run commandagent") +} + +fn stdout(output: &Output) -> String { + String::from_utf8_lossy(&output.stdout).into_owned() +} + +fn stderr(output: &Output) -> String { + String::from_utf8_lossy(&output.stderr).into_owned() +} + +fn action_selector_flags() -> BTreeSet { + commandagent::provider_cli::command() + .get_arguments() + .filter(|argument| !argument.is_hide_set()) + .filter(|argument| { + argument + .get_help_heading() + .map(|heading| heading.to_string()) + .as_deref() + == Some("Actions (use one)") + }) + .filter_map(|argument| argument.get_long().map(|long| format!("--{long}"))) + .collect() +} + +/// Collect the pure `--flag` names that appear inside Markdown code spans. +fn documented_flags(section: &str) -> BTreeSet { + section + .split('`') + .skip(1) + .step_by(2) + .filter_map(|token| { + let token = token.trim(); + (token.starts_with("--") + && token + .chars() + .all(|c| c == '-' || c.is_ascii_lowercase() || c.is_ascii_digit())) + .then(|| token.to_string()) + }) + .collect() +} + +#[test] +fn invalid_provider_value_lists_all_five_candidates_and_exits_two() { + for flag in ["--provider", "--planner-provider"] { + let error = + commandagent::provider_cli::parse_from(["commandagent", flag, "not-a-provider"]) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::InvalidValue, "{flag}"); + assert_eq!(error.exit_code(), 2, "{flag}"); + let rendered = error.to_string(); + assert!( + rendered.contains("invalid value 'not-a-provider'"), + "{flag}: {rendered}" + ); + for candidate in PROVIDER_CANDIDATES { + assert!( + rendered.contains(candidate), + "{flag} candidate list is missing {candidate}: {rendered}" + ); + } + } +} + +#[test] +fn all_five_provider_values_are_accepted_and_generic_extra_info_is_kept() { + for value in PROVIDER_CANDIDATES { + let parsed = commandagent::provider_cli::parse_from([ + "commandagent", + "--provider", + value, + "--planner-provider", + value, + ]) + .unwrap_or_else(|err| panic!("{value} must be accepted: {err}")); + assert!(parsed.cli.provider.is_some(), "{value}"); + assert!(parsed.cli.planner_provider.is_some(), "{value}"); + let generic = value == "openai-compatible"; + assert_eq!( + parsed.provider_options.executor_openai_compatible, generic, + "{value}" + ); + assert_eq!( + parsed.provider_options.planner_openai_compatible, generic, + "{value}" + ); + } +} + +#[test] +fn action_selector_flags_match_the_bilingual_invocation_lists() { + let actions = action_selector_flags(); + assert!( + actions.contains("--workflow"), + "the Clap `Actions (use one)` group must include --workflow" + ); + for (path, heading) in [(EN_CLI, "## Invocation"), (JA_CLI, "## 呼び出し方")] { + let markdown = read_repo_file(path); + let section = markdown_section(&markdown, heading, path); + let mut documented = documented_flags(section); + documented.remove("--help"); + documented.remove("--version"); + assert_eq!( + documented, actions, + "{path} {heading} must list exactly the `Actions (use one)` flags" + ); + } +} + +#[test] +fn clap_and_configuration_rejections_keep_their_routes_and_exit_codes() { + let workspace = test_tempdir(); + + for arguments in [ + ["--doctor", "--runs"].as_slice(), + ["--workflow", "workflow.yaml", "--intent", "create"].as_slice(), + ["--origin", "origin-run"].as_slice(), + ["--json"].as_slice(), + ] { + let output = run_commandagent(arguments, workspace.path()); + assert_eq!( + output.status.code(), + Some(2), + "{arguments:?} must be a Clap rejection: {}", + stderr(&output) + ); + assert!( + stdout(&output).is_empty(), + "{arguments:?} must not emit a run summary" + ); + } + + // A different planner provider without a planner model is a post-parse + // configuration rejection, not a Clap conflict. + let output = run_commandagent( + &["--planner-provider", "openai", "--prompt", "x"], + workspace.path(), + ); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + assert!(stdout(&output).is_empty()); + + let output = run_commandagent(&["--prompt", "x", "--ux-demo"], workspace.path()); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + assert!( + stderr(&output).contains("only one action selector"), + "{}", + stderr(&output) + ); + assert!(stdout(&output).is_empty()); + + let output = run_commandagent(&["--workflow", "workflow.yaml"], workspace.path()); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + assert!( + stderr(&output).contains("--workflow requires --origin"), + "{}", + stderr(&output) + ); + assert!(stdout(&output).is_empty()); +} + +#[test] +fn doctor_configuration_failure_exits_one_with_a_json_report() { + let workspace = test_tempdir(); + for arguments in [ + ["--doctor", "--json", "--planner-provider", "openai"].as_slice(), + [ + "--ux-demo", + "--doctor", + "--json", + "--planner-provider", + "openai", + ] + .as_slice(), + ] { + let output = run_commandagent(arguments, workspace.path()); + assert_eq!( + output.status.code(), + Some(1), + "doctor must exit 1 on a Configuration failure: {arguments:?}: {}", + stderr(&output) + ); + let report = stdout(&output); + assert!( + report.trim_start().starts_with('{'), + "{arguments:?} must print a JSON report: {report}" + ); + assert!( + report.contains("config.resolution"), + "{arguments:?} JSON report must record the Configuration failure: {report}" + ); + } +} + +#[test] +fn json_targets_and_workflow_dependencies_are_fixed() { + for arguments in [ + ["commandagent", "--doctor", "--json"].as_slice(), + ["commandagent", "--extensions", "--json"].as_slice(), + ["commandagent", "--runs", "--json"].as_slice(), + ] { + Cli::try_parse_from(arguments) + .unwrap_or_else(|err| panic!("{arguments:?} must accept --json: {err}")); + } + + let error = Cli::try_parse_from(["commandagent", "--json"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::MissingRequiredArgument, "{error}"); + + let error = Cli::try_parse_from(["commandagent", "--origin", "run"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::MissingRequiredArgument, "{error}"); + + let error = Cli::try_parse_from(["commandagent", "--workflow", "w.yaml", "--intent", "create"]) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::ArgumentConflict, "{error}"); + + // `--workflow` alone parses at the Clap layer; its `--origin` requirement + // is a resolved-configuration check. + Cli::try_parse_from(["commandagent", "--workflow", "w.yaml"]) + .expect("--workflow must parse without --origin"); +} + +#[test] +fn generic_provider_requires_a_base_url_for_any_role() { + let workspace = test_tempdir(); + + // Executor uses the generic provider without a base URL. + let output = run_commandagent( + &[ + "--provider", + "openai-compatible", + "--model", + "served-model", + "--prompt", + "x", + ], + workspace.path(), + ); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + assert!( + stderr(&output).contains("base_url") || stderr(&output).contains("--base-url"), + "{}", + stderr(&output) + ); + + // `--base-url`/`--api-key-env` without any generic role is rejected. + let output = run_commandagent( + &[ + "--base-url", + "http://127.0.0.1:9/v1", + "--api-key-env", + "SOME_KEY", + "--prompt", + "x", + ], + workspace.path(), + ); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + + // Only the classifier is generic; the base URL is still required. + let config_dir = workspace.path().join(".commandagent"); + fs::create_dir_all(&config_dir).unwrap(); + fs::write( + config_dir.join("config.toml"), + concat!( + "[preset.classifier_gateway]\n", + "provider = \"ollama\"\n", + "model = \"executor-model\"\n", + "planner_provider = \"ollama\"\n", + "planner_model = \"planner-model\"\n", + "classifier_provider = \"openai-compatible\"\n", + "classifier_model = \"classifier-model\"\n", + ), + ) + .unwrap(); + let cwd = workspace.path().to_string_lossy().to_string(); + let output = run_commandagent( + &[ + "--cwd", + &cwd, + "--preset", + "classifier_gateway", + "--prompt", + "x", + ], + workspace.path(), + ); + assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); + assert!( + stderr(&output).contains("base_url") || stderr(&output).contains("--base-url"), + "{}", + stderr(&output) + ); +} + +fn issue519_proposal() -> RouteProposal { + RouteProposal { + selected: Some(RouteCandidate { + profile: ProfileId::Ingest, + intent: IntentId::Create, + family: TaskFamilyId::List, + bases: vec![RouteBasis { + rule: "issue519", + observation: "list".to_string(), + }], + contract_ref: "docs/ingest-profile-contract.md", + }), + alternatives: Vec::new(), + classifier: ClassifierProvenance { + used: false, + provider: "ollama".to_string(), + model: "planner".to_string(), + prompt_version: "issue519-test", + candidate_keys: Vec::new(), + raw_response_hash: None, + parse_reason: "deterministic_unique".to_string(), + }, + status: ProposalStatus::AwaitingConfirmation, + confirmation_required: true, + } +} + +fn issue519_pins() -> ExecutionPins { + ExecutionPins { + planner_provider: "ollama".to_string(), + planner_model: "planner".to_string(), + executor_provider: "ollama".to_string(), + executor_model: "executor".to_string(), + preset: "profile".to_string(), + think: None, + } +} + +#[test] +fn gate_only_directives_stay_outside_the_registry_and_refuse_before_confirmation() { + let registry = SLASH_COMMANDS + .iter() + .flat_map(|spec| std::iter::once(spec.name).chain(spec.aliases.iter().copied())) + .collect::>(); + assert_eq!(SLASH_COMMANDS.len(), 23, "primary registry size"); + assert_eq!(registry.len(), 24, "accepted registry names"); + let help = render_help(); + for gate_only in ["/directive", "/confirm-directive"] { + assert!( + !registry.contains(gate_only), + "{gate_only} must stay outside the general registry" + ); + assert!( + !help.contains(gate_only), + "/help must not advertise the Gate-only command {gate_only}" + ); + } + + let dir = test_tempdir(); + let events = dir.path().join("events.jsonl"); + let mut shell = BoundaryShell::new(dir.path().join("confirmations"), Some(events)); + + // With no confirmed terminal run, both directive commands are refused. + let refused = shell + .begin_directive("repair README", "run-001", 1) + .unwrap_err(); + assert!( + refused.to_string().contains("Gate 3 or Gate 4"), + "{refused}" + ); + assert!(shell.confirm_directive("sha256:abc").is_err()); + + shell + .begin_gate_one( + issue519_proposal(), + "request", + dir.path(), + issue519_pins(), + PackSelection::None, + ) + .unwrap(); + let card_hash = match shell.state() { + BoundaryState::AwaitingConfirmation { card_hash, .. } => card_hash.clone(), + state => panic!("unexpected state: {state:?}"), + }; + shell.confirm(&card_hash).unwrap(); + shell.dispatch(|_| Ok("failed".to_string())).unwrap(); + shell + .present_terminal( + "# sheet\n\n## 5. Stop reason\nfailed".to_string(), + false, + Some("failed".to_string()), + ) + .unwrap(); + + let directive = shell + .begin_directive("repair README", "run-001", 1) + .unwrap() + .clone(); + assert_eq!(directive.artifact().issued_gate, "gate_4"); + + // Gate 1 prefix matching does not apply: only the exact hash is accepted. + assert!(shell.confirm_directive("sha256:wrong").is_err()); + assert!(shell.confirm_directive(directive.hash()).is_ok()); +} + +fn preset_config(provider_key: &str, model_key: &str, model_value: &str) -> String { + let mut lines = vec![ + "[preset.alias_role]".to_string(), + "provider = \"ollama\"".to_string(), + "model = \"executor-model\"".to_string(), + "planner_provider = \"ollama\"".to_string(), + "planner_model = \"planner-model\"".to_string(), + "classifier_provider = \"ollama\"".to_string(), + "classifier_model = \"classifier-model\"".to_string(), + ]; + lines.retain(|line| { + !line.starts_with(&format!("{provider_key} = ")) + && !line.starts_with(&format!("{model_key} = ")) + }); + lines.push(format!("{provider_key} = \"openai\"")); + lines.push(format!("{model_key} = \"{model_value}\"")); + lines.join("\n") + "\n" +} + +#[test] +fn openai_alias_is_rejected_for_all_three_roles_and_exact_ids_are_accepted() { + let workspace = test_tempdir(); + let config_dir = workspace.path().join(".commandagent"); + fs::create_dir_all(&config_dir).unwrap(); + let config_path = config_dir.join("config.toml"); + let cwd = workspace.path().to_string_lossy().to_string(); + + for (role, provider_key, model_key) in [ + ("executor", "provider", "model"), + ("planner", "planner_provider", "planner_model"), + ("classifier", "classifier_provider", "classifier_model"), + ] { + for model in ["gpt-5.6", "gpt-5.6-terra"] { + fs::write(&config_path, preset_config(provider_key, model_key, model)).unwrap(); + // `--runs` resolves the configuration without creating provider + // clients, so a resolved configuration exits 0 and an alias + // rejection still exits 2. + let output = run_commandagent( + &["--cwd", &cwd, "--preset", "alias_role", "--runs"], + workspace.path(), + ); + if model == "gpt-5.6" { + assert_eq!( + output.status.code(), + Some(2), + "{role} alias must be rejected: {}", + stderr(&output) + ); + assert!( + stderr(&output).contains("gpt-5.6"), + "{role}: {}", + stderr(&output) + ); + } else { + assert_eq!( + output.status.code(), + Some(0), + "{role} exact ID gpt-5.6-terra must resolve: {}", + stderr(&output) + ); + } + } + } +} + +#[test] +fn bilingual_docs_record_the_issue_519_contract() { + let slash_en = read_repo_file(EN_SLASH); + let slash_ja = read_repo_file(JA_SLASH); + for marker in [ + "Gate 3/4 dedicated commands", + "`/directive `", + "`/confirm-directive `", + "switches only among the four built-in providers", + ] { + assert!(slash_en.contains(marker), "{EN_SLASH} missing {marker:?}"); + } + for marker in [ + "Gate 3/4 専用コマンド", + "`/directive `", + "`/confirm-directive `", + "が切り替えられるのは", + ] { + assert!(slash_ja.contains(marker), "{JA_SLASH} missing {marker:?}"); + } + + let index = read_repo_file(GUIDE_INDEX); + for marker in [ + "all 24 accepted command names", + "plus the Gate 3/4 dedicated", + "受け付ける全 24 コマンド名", + "Gate 3/4 専用の", + ] { + assert!(index.contains(marker), "{GUIDE_INDEX} missing {marker:?}"); + } + + let cli_en = read_repo_file(EN_CLI); + let cli_ja = read_repo_file(JA_CLI); + for marker in [ + "resolved configuration accepts exactly one action selector", + "`--doctor` is the exception", + "`--json` applies to", + "needs its base URL", + "switches only among the four built-in providers", + ] { + assert!(cli_en.contains(marker), "{EN_CLI} missing {marker:?}"); + } + for marker in [ + "解決済み設定はアクション選択フラグを 1 つだけ受け付け", + "`--doctor` は例外", + "`--json` は `--doctor`", + "汎用プロバイダに解決される役割には base URL が必要", + "4 名 `ollama`、`lm-studio`、`openai`、`gemini`", + ] { + assert!(cli_ja.contains(marker), "{JA_CLI} missing {marker:?}"); + } + + let providers_en = read_repo_file(EN_PROVIDERS); + let providers_ja = read_repo_file(JA_PROVIDERS); + assert!(providers_en.contains("executor, planner, and classifier")); + assert!(providers_en.contains("not applied to the generic")); + assert!(providers_ja.contains("executor、planner、classifier")); + assert!(providers_ja.contains("transport には適用しません")); + + let trouble_en = read_repo_file(EN_TROUBLE); + let trouble_ja = read_repo_file(JA_TROUBLE); + assert!(trouble_en.contains("every OpenAI role")); + assert!(trouble_ja.contains("すべての OpenAI 役割")); + + let config_en = read_repo_file(EN_CONFIG); + let config_ja = read_repo_file(JA_CONFIG); + for variable in ENV_VARIABLES { + assert!( + config_en.contains(variable), + "{EN_CONFIG} missing {variable}" + ); + assert!( + config_ja.contains(variable), + "{JA_CONFIG} missing {variable}" + ); + } + for marker in [ + "no `ANVIL_*` legacy name", + "use `--pack` or a preset to select a pack", + ] { + assert!(config_en.contains(marker), "{EN_CONFIG} missing {marker:?}"); + } + for marker in [ + "`ANVIL_*` の旧名を持ちません", + "pack の選択には `--pack` または", + ] { + assert!(config_ja.contains(marker), "{JA_CONFIG} missing {marker:?}"); + } +}