From 0d8274ac3aa6422bcb91b35fba190755018762b1 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 13 Aug 2026 10:37:10 +0000 Subject: [PATCH 1/2] spec(SERVE-RECIPE-ARGS): a published recipe command must reach model load (#606) FOLLOWING_AGENTS_PROTOCOL vllm-serve rejects any argument it does not recognise (server_main.cpp:440). That is the right default, and it is why two flags that mean nothing to us stop the server before it loads a model: --enable-auto-tool-choice, which 89 of the 157 official vLLM recipes pass, and --trust-remote-code, which 82 pass. We parse tool calls whenever --tool-call-parser resolves, so there is no second gate to open; and we have no Python, so there is no remote code to trust. Both are inert, and both abort -- including for Qwen3.5-27B, which we ship token-exact and gated. The kernels are not what stands between a user and a running server here. The spec is deliberately narrow about what the seam may swallow. An enumerated table, one entry per flag with the reason it is inert; anything unlisted still aborts, because silently accepting --tensor-parallel-size would let a user believe they got tensor parallelism, and that is the failure mode this exists to prevent. Flags inert because we LACK the capability -- TP, EP, --mm-encoder-tp-mode -- are explicitly excluded and keep aborting. So is --language-model-only (#607), which is a real capability gap wearing the same costume. Two things the spec records rather than papers over. cli_args.py:395 makes --enable-auto-tool-choice without --tool-call-parser a TypeError upstream, so inert must not mean unvalidated and the mirrored failure is a named test case. And upstream defaults --tool-call-parser to None where we default it to hermes, so upstream's flag genuinely gates something ours cannot -- pre-existing, out of scope, and called out so the notice text does not overclaim parity. ENGINE_ROWS 152 -> 153 for the new row, with the justification the checker's own comment ledger asks for. No checker semantics change: the row claims no seam, no test and no USAGE entry, because none exists yet. Spec and records only, no product code touched. scripts/agent-preflight.sh --staged: exit 0. Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --- .agents/claims/CLAIM-SERVE-RECIPE-ARGS.md | 5 + .agents/engine-matrix.md | 5 +- .agents/specs/serve-recipe-args.md | 115 ++++++++++++++++++++++ scripts/check-agent-record.py | 9 +- 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 .agents/claims/CLAIM-SERVE-RECIPE-ARGS.md create mode 100644 .agents/specs/serve-recipe-args.md diff --git a/.agents/claims/CLAIM-SERVE-RECIPE-ARGS.md b/.agents/claims/CLAIM-SERVE-RECIPE-ARGS.md new file mode 100644 index 000000000..1ae48e07f --- /dev/null +++ b/.agents/claims/CLAIM-SERVE-RECIPE-ARGS.md @@ -0,0 +1,5 @@ +# CLAIM-SERVE-RECIPE-ARGS + +| Claim | Row IDs | Agent | Worktree / remote dir | Branch | Owned scope | State | Last update | +|---|---|---|---|---|---|---|---| +| `CLAIM-SERVE-RECIPE-ARGS` | `SERVE-RECIPE-ARGS` (`SPIKE`) | Claude Code (opus-5), operator role — spec only; implementation goes to a fresh implementer | isolated worktree; no GPU, no oracle run, no publication | `row/serve-recipe-args`, issue [#606](https://github.com/mudler/vllm.cpp/issues/606) | Owns ONLY: NEW `.agents/specs/serve-recipe-args.md`; the NEW `SERVE-RECIPE-ARGS` row plus the two summary counts it moves in `.agents/engine-matrix.md`; and, at implementation time, the accepted-and-inert table in `src/vllm/entrypoints/openai/server_main.cpp`, its `docs/USAGE.md` entries, and its RED-first tests. EXCLUDES `--language-model-only` (#607, a real capability gap), the `--tool-call-parser` default (`hermes` vs upstream `None`, pre-existing and out of scope), and every flag that is inert because we lack the capability — TP, EP, `--mm-encoder-tp-mode` — which must keep aborting | `SPIKE` | 2026-08-13 — spec committed; implementation not started | diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index 531cda073..5fc05f536 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -44,11 +44,11 @@ forensics: roadmap_v1.md and the parity ledger. | Sampling and generation | 15 | 4 | 2 | 0 | 0 | 4 | 0 | 1 | 4 | | Structured output and tools | 7 | 0 | 3 | 0 | 0 | 2 | 0 | 0 | 2 | | Speculative decoding | 21 | 0 | 0 | 1 | 0 | 5 | 0 | 4 | 10 | -| Serving, API, CLI, library | 32 | 10 | 2 | 0 | 2 | 9 | 2 | 3 | 4 | +| Serving, API, CLI, library | 33 | 10 | 2 | 1 | 2 | 9 | 2 | 3 | 4 | | LoRA and adapters | 2 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | | Long context and attention | 10 | 5 | 0 | 0 | 1 | 0 | 1 | 0 | 3 | | Loading, tokenizer, config | 10 | 3 | 3 | 0 | 0 | 1 | 1 | 1 | 1 | -| **Total** | **152** | **35** | **16** | **4** | **9** | **29** | **8** | **11** | **39** | +| **Total** | **153** | **35** | **16** | **5** | **9** | **29** | **8** | **11** | **39** | ## Engine core and scheduling @@ -219,6 +219,7 @@ claims it. | `ENG-FORGE-COAUTHOR` | The forbidden-AI-trailer rule was catching ATTRIBUTION rather than an authorship claim, which is why bot-opened PRs red `main` on merge. GitHub composes the squash message itself and appends the account that opened the PR — `Co-authored-by: localai-org-maint-bot <...@users.noreply.github.com>` — and most PRs here are opened by a bot, so nearly every squash trips the AI-identity check. Real instance `f64f2b71`, invisible until #406 repaired the parse, which is why it reads as a new failure and is not one. The rule exists so an AI cannot claim it WROTE the code, and that stays; GitHub is recording who pressed the button, and the AI-involvement claim is already carried separately by `AI-Assisted` and `Assisted-by` in the same block. FIX: accept a `Co-authored-by` at a GitHub account noreply address even when the name matches an AI identity token, keyed on the FORGE'S OWN DOMAIN rather than the name so the exemption cannot be borrowed. A hand-written `Co-authored-by: Claude ` still fails; `Signed-off-by` is excluded from the exemption entirely, because a sign-off is a legal assertion about provenance rather than attribution. `AGENTS.md` records the same distinction in the same change so prose and checker cannot drift | T0 | NO vLLM analogue -- local protocol machinery, so the mirror rule does not apply and there is no upstream `file:line` to port from. Governed by `AGENTS.md` §"Changing the rules or a checker" | `scripts/check-commit-trailers.py:38` (`FORGE_ACCOUNT_EMAIL` and the forbidden-trailer skip) | `tests/scripts/test_check_commit_trailers.py:1` 25 cases -- the RED-BEFORE forge-bot case plus THREE guards that matter more than the relaxation because this LOOSENS a rule: a hand-written AI co-author still fails, `Signed-off-by` at the same noreply address still fails, and a human co-author still passes; all three green before and after. Real commit `f64f2b71` re-verified per commit | [forge-coauthor-attribution.md](specs/forge-coauthor-attribution.md); issue [#418](https://github.com/mudler/vllm.cpp/issues/418) | `ACTIVE` | `CLAIM-ENG-FORGE-COAUTHOR` | | `ENG-NOW-DERIVED` | W1-W5 remove the per-row `.agents/NOW.md` write: each moved row's own spec carries `## Now`, `scripts/now.py` renders the live roster offline-first, and the digest cannot regrow a row table. Implementation merge `dbd0d51c`; progressive legacy-spec backfill is the selected compatibility policy, not remaining work. Runtime/performance/parity are `VOID` because this is local protocol machinery | T0 | No vLLM analogue; governed by `AGENTS.md` §Changing the rules or a checker | `scripts/now.py:163`; `scripts/check-doc-checkpoint.py:181`; `scripts/check-now-current.py:57` | `tests/scripts/test_doc_checkpoint.py:142`; `tests/scripts/test_now_render.py:34`; `tests/scripts/test_check_now_current.py:1`; closure [parity-ledger.md#L939](parity-ledger.md#L939) | [now-derived.md](specs/now-derived.md); issue [#374](https://github.com/mudler/vllm.cpp/issues/374) | `DONE` | `dbd0d51c` | | `SERVE-CLI-CHAT` | Interactive `chat` and `complete` commands against a running OpenAI-compatible server, plus preservation of the existing local-model completion invocation | T1 | registration `vllm/entrypoints/cli/main.py:17-37,73-98`; client/model resolution + stream shaping `vllm/entrypoints/cli/openai.py:30-100`; chat `:155-234`; complete `:237-312` at `5559679229` | current in-process completion only `examples/cli/main.cpp:1-207`; remote command implementation absent | C-ABI stream baseline `tests/capi/test_capi.cpp:567-711`; chat-template baseline `tests/capi/test_chat_prompt.cpp:37-89`; command/fake-server tests absent | [cli-chat-complete.md](specs/cli-chat-complete.md) | `ANCHOR-BACKFILL` | `CLAIM-SERVE-CLI-CHAT-SPIKE` | +| `SERVE-RECIPE-ARGS` | Accepted-and-inert serve arguments: an enumerated table of flags that published recipes pass, that are no-ops for this engine, and that must therefore not abort argument parsing. Not a catch-all — anything unlisted still aborts, and mirrored validation still fires. Found by the 2026-08-13 recipe-surface sweep: `vllm-serve` rejects unknown arguments (`src/vllm/entrypoints/openai/server_main.cpp:440`), so `--enable-auto-tool-choice` (89/157 official recipes) and `--trust-remote-code` (82/157) stop the server before model load even though neither means anything here — including for models we ship token-exact and gated | T1 | `vllm/entrypoints/openai/cli_args.py:105` (`enable_auto_tool_choice` default), `:395` (requires `--tool-call-parser`, a `TypeError` otherwise — mirrored, not dropped); threading `vllm/entrypoints/openai/api_server.py:426,441,529,544` at `5559679229` | absent — `server_main.cpp:440` aborts on any unrecognized flag; no accepted-and-inert seam exists | none yet; RED-first cases named in the spec, including the mutation that must turn the "unlisted flag still aborts" test RED | [serve-recipe-args.md](specs/serve-recipe-args.md) | `SPIKE` | `CLAIM-SERVE-RECIPE-ARGS` | | `SERVE-POOLING-ENDPOINTS` | Embeddings, pooling, score, rerank, classify HTTP surface (`/v1/embeddings`, `/pooling`, `/score`, `/rerank`, `/classify`). **SPIKED 2026-07-28 (`CLAIM-POOLING`):** the whole pooling task class is scoped in [pooling-task-class.md](specs/pooling-task-class.md). **`/v1/embeddings` LIVE 2026-08-08 (ARCH-ONE-SURFACE ROW 6, `CLAIM-EMBEDDINGS-ONE-SURFACE`):** task-conditional registration (embed/api_router.py:22-28 mirror; the route exists ONLY on a pooling-model server, and the generate routes do not — both directions socket-404-pinned), OpenAI request/response shape (string-or-array input; `dimensions`/base64/token-arrays are named-residual 400s), handler drives the ONE engine path (`LoadedEngine -> LLMEngine::embed -> registry forward -> PoolingRunner`) — the same path `vllm_embed` (ABI v15) drives. RESIDUALS: `/pooling`, `/score`, `/rerank`, `/classify` (need a classify arch) | T2 | `vllm/entrypoints/pooling/embed/api_router.py:28`; `vllm/entrypoints/pooling/embed/protocol.py:34,173-185`; `vllm/entrypoints/pooling/scoring/api_router.py:37,71`; `vllm/entrypoints/pooling/classify/api_router.py:26` | `src/vllm/entrypoints/openai/api_server.cpp` `handle_embeddings` + the `if (embedder_)` route gate; `examples/server/main.cpp` pooling task dispatch | `tests/vllm/entrypoints/openai/test_api_server.cpp` embeddings section (dispatch shape + socket smoke + BOTH-direction 404 pins) | [embeddings-one-surface.md](specs/embeddings-one-surface.md) | `ACTIVE` | `CLAIM-EMBEDDINGS-ONE-SURFACE` | | `ENG-POOLER-SEQ` | The non-generative POOLER OP — turn hidden states into a pooled embedding/logit row instead of a sampled token. **W1 LANDED + CPU-GATED 2026-07-28 (`CLAIM-POOLING`, NOT pushed):** the sequence pooling methods `CLSPool`/`LastPool`/`MeanPool` (+ `GetSeqPoolingMethod` factory) over a packed `[num_tokens, hidden]` CPU buffer keyed by a minimal `PoolingCursor` (CLS/MEAN reject partial prefill, LAST allows it, MeanPool upcasts to float32) and the activation heads `PoolerIdentity`/`PoolerNormalize` (L2 `F.normalize`)/`PoolerMultiLabelClassify` (sigmoid)/`PoolerClassify` (sigmoid if `num_labels<2` else `softmax`). Unit-gated vs DOUBLE-PRECISION references, RED-first. **W2 LANDED + CPU-GATED 2026-07-29 (`CLAIM-POOLING`, NOT pushed):** the pooler HEADS composite (`EmbeddingPoolerHead` = projector→matryoshka→normalize; `ClassifierPoolerHead` = classifier→`(logit-mean)/sigma`→activation), the `SequencePooler` (method∩head task intersection) + `PoolerForEmbed`/`PoolerForClassify` factories, the `DispatchPooler` groupby-task routing (`ForEmbedding`/`ForSeqCls` + a mixed embed+classify batch + ctor task-support validation), and the `PoolerConfig`/`PoolingParams`/`PoolingParamsUpdate` structs; `test_pooler_heads` 27/27 (240 asserts) vs double-precision refs, RED-first (disable matryoshka slice + logit_mean calibration → 8 cases / 50 asserts fail). RESIDUALS (named, spec §Work breakdown): the endpoints (W4), tokwise `AllPool`/`StepPool` (W5), a concrete pooling MODEL + real-oracle cosine gate (W3-model — see `ENG-POOLING-RUNNER`) | T2 | `vllm/model_executor/layers/pooler/seqwise/methods.py:35-121`; `vllm/model_executor/layers/pooler/activations.py:106-158`; `vllm/model_executor/layers/pooler/seqwise/heads.py:19-196`; `vllm/model_executor/layers/pooler/seqwise/poolers.py:41-138`; `vllm/model_executor/layers/pooler/special.py:23-140`; `vllm/model_executor/layers/pooler/common.py:12-30`; `vllm/pooling_params.py:35-70`; `vllm/config/pooler.py:16-90`; `vllm/v1/pool/metadata.py:13-71`; `tests/model_executor/layers/test_pooler_methods.py`, `tests/model_executor/layers/test_pooler_activations.py`, `tests/model_executor/layers/test_pooler_heads.py` | `include/vllm/model_executor/layers/pooler/{methods,activations,pooling_metadata,common,pooling_params,pooler_config,heads,poolers,dispatch_pooler}.h` + `src/vllm/model_executor/layers/pooler/{methods,activations,heads,poolers,dispatch_pooler}.cpp` — anchor `src/vllm/model_executor/layers/pooler/dispatch_pooler.cpp:13` | `tests/vllm/model_executor/layers/pooler/test_pooler.cpp` (CLS/LAST/MEAN + factory + activations, 50 asserts) + `test_pooler_heads.cpp` (Embedding/Classifier heads + SequencePooler + DispatchPooler, 240 asserts) — anchor `tests/vllm/model_executor/layers/pooler/test_pooler.cpp:81` | [pooling-task-class.md](specs/pooling-task-class.md) | `ACTIVE` | `CLAIM-POOLING` | | `ENG-POOLING-RUNNER` | The pooling RUNNER path — where the generation runner SAMPLES a token, the pooling runner applies the model's `Pooler` to the last hidden state and returns the POOLED DATA (embedding vector / classification logit row). **W3 LANDED + CPU-GATED 2026-07-29 (`CLAIM-POOLING`, NOT pushed):** `PoolingRunner` over a packed `[num_tokens, hidden]` last-hidden-state buffer + a `PoolingMetadata` — `Pool()` delegates to the model pooler (`DispatchPooler.ForEmbedding`), `GetSupportedTasks()`, `ComputeValid()` (`seq_lens==prompt_len`). GATE: a STRUCTURAL cosine-parity gate — the runner's embedding vs an independent double-precision LAST+normalize reference is cosine≈1 (5 cases / 14 asserts), RED-first (CLS-instead-of-LAST drops cosine <0.5; disable normalize → 2 unit-L2 asserts fail). GENERALIZATION DEVIATION: upstream `pooling_runner.py` hardcodes LAST+normalize; we route through the model `Pooler` (the general bert.py path), strictly more capable. HONEST RESIDUAL (named): the REAL-model oracle cosine gate (`vllm.LLM(task="embed").encode`) needs a registered concrete embedding model's forward — no such model is registered yet (W3-model), so no cosine-vs-oracle number is fabricated. **LIVE IN THE ENGINE STEP 2026-08-08 (ARCH-ONE-SURFACE ROW 6, `CLAIM-EMBEDDINGS-ONE-SURFACE`):** `GPUModelRunner` builds a `PoolingRunner` iff the loaded model registration declares `is_pooling_model` (gpu/model_runner.py:368-369 mirror) and `sample_tokens` routes to `pool_tokens()` — pooled data instead of sampled tokens (model_runner.py:1586-1607), validity = the discard predicate (`seq_len < num_tokens` == upstream is_valid, pooling_runner.py:40-41); the scheduler finishes a pooling request on pooled output (scheduler.py:1718-1721) and `EngineCoreOutput.pooling_output` carries it out; async scheduling resolves OFF for pooling models (config/vllm.py:1068-1073, the landed ResolveAsyncScheduling arm now WIRED at model_loader.cpp). First registered pooling arch: `LlamaModel` (`MODEL-EMBED-llama-llama-for-causal-lm`). The fold gate re-anchors the lane's cosine gate THROUGH the registry/runner path: engine path == direct `ModelRegistry::Forward`+`PoolingRunner` path, identical vectors + f64 LAST+normalize reference + chunked-prefill arm (`test_llama_embedding_fold` 4/4-231). REMAINING RESIDUAL: the REAL-model `vllm.LLM(task="embed").encode` oracle cosine (synthetic fixture only — no number fabricated) | T2 | `vllm/v1/worker/gpu/pool/pooling_runner.py:18-46`; `vllm/v1/worker/gpu/model_runner.py:368-369,1586-1607`; `vllm/v1/core/sched/scheduler.py:1718-1721,1837`; `vllm/tasks.py:10`; `tests/models/language/pooling/test_embedding.py` (real-oracle gate, DEFERRED) | `include/vllm/v1/worker/gpu/pool/pooling_runner.h` + `src/vllm/v1/worker/gpu/pool/pooling_runner.cpp:11`; live invocation `src/vllm/v1/worker/gpu/runner.cpp` `pool_tokens` + the `pooling_runner_` ctor gate; scheduler stop `src/vllm/v1/core/sched/scheduler.cpp` pooling elif | `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp:136` (structural cosine gate) + `tests/vllm/models/test_llama_embedding_fold.cpp:206` (registry/engine-path arm, 4/4-231, mutation-killed x9) | [pooling-task-class.md](specs/pooling-task-class.md) + [embeddings-one-surface.md](specs/embeddings-one-surface.md) | `ACTIVE` | `CLAIM-EMBEDDINGS-ONE-SURFACE` | diff --git a/.agents/specs/serve-recipe-args.md b/.agents/specs/serve-recipe-args.md new file mode 100644 index 000000000..8e2f3d0e7 --- /dev/null +++ b/.agents/specs/serve-recipe-args.md @@ -0,0 +1,115 @@ +# SERVE-RECIPE-ARGS — a published recipe command must reach model load + +Issue: [#606](https://github.com/mudler/vllm.cpp/issues/606) +Row: `SERVE-RECIPE-ARGS` ([engine-matrix.md](../engine-matrix.md)) +Sweep that found it: `roadmap_v1.md` § Recipe-surface sweep (2026-08-13) + +## The defect + +`vllm-serve` rejects any argument it does not recognise +(`src/vllm/entrypoints/openai/server_main.cpp:440`): + +```cpp +std::cerr << "server: unknown argument '" << flag << "'\n"; +``` + +That is the right default. It is also why two flags that mean *nothing* to us stop +the server before it loads a model. Measured over `vllm-project/recipes` +@ `86c7777aa699482ef1ebd0c5da9fc540ccc00a40`, 157 official model recipes: + +| Flag | Recipes passing it | Why it is inert for us | +|---|---:|---| +| `--enable-auto-tool-choice` | **89 / 157** | we parse tool calls whenever `--tool-call-parser` resolves; there is no second gate to open | +| `--trust-remote-code` | **82 / 157** | authorizes executing Python from the checkpoint; we have no Python, so there is nothing to authorize | + +The reproducer is a recipe's own copy-paste block — +`recipes/models/Qwen/Qwen3.5-27B.yaml`, `features.tool_calling`: + +```bash +vllm serve Qwen/Qwen3.5-27B-FP8 --enable-auto-tool-choice --tool-call-parser qwen3_coder +# ^ server: unknown argument '--enable-auto-tool-choice' +``` + +Qwen3.5-27B is a model we ship **token-exact and gated**. The kernels are not the +thing standing between a user and a running server; argument parsing is. + +## Upstream anchors + +- `vllm/entrypoints/openai/cli_args.py:105` — `enable_auto_tool_choice: bool = False`. +- `vllm/entrypoints/openai/cli_args.py:395` — **`--enable-auto-tool-choice` + without `--tool-call-parser` is a `TypeError`.** This validation is part of the + behaviour and must be mirrored, not dropped. +- `vllm/entrypoints/openai/api_server.py:426,441,529,544` — threaded onward as + `enable_auto_tools`. + +## Design + +Add an **accepted-and-inert** table to `server_main.cpp`. Not a catch-all: a fixed +list, one entry per flag, each carrying the reason it is inert and whether it takes +a value. + +``` +{"--enable-auto-tool-choice", kNoValue, "tool parsing is already unconditional once --tool-call-parser resolves"}, +{"--trust-remote-code", kNoValue, "no Python runtime: there is no remote code to trust"}, +``` + +Three rules the table must obey: + +1. **A flag not in the table still aborts.** Silently swallowing + `--tensor-parallel-size` would let a user believe they got tensor parallelism. + The whole value of the seam is that it is enumerated. +2. **Accepting is announced.** On use, emit one notice per accepted flag naming it + and its reason, so a user reading the log learns the flag did nothing rather + than inferring that it worked. +3. **Mirrored validation still fires.** `--enable-auto-tool-choice` with + `--tool-call-parser none` must fail exactly as `cli_args.py:395` does. Inert is + not the same as unvalidated. + +### The one place we knowingly differ + +Upstream defaults `--tool-call-parser` to `None`; we default it to `hermes` +(`docs/USAGE.md:878`). So upstream's flag genuinely gates something and ours cannot. +This spec does **not** change that default — it is pre-existing, out of scope here, +and worth its own issue if a reviewer wants it reconciled. The notice text must not +imply our behaviour matches upstream's when the parser is unset. + +## Tests + +RED-first, in `tests/vllm/entrypoints/openai/`: + +| Case | Asserts | +|---|---| +| each listed flag starts the server | argument parsing succeeds and the engine reaches load | +| an unlisted unknown flag | still aborts with the existing message — the guard is not widened | +| `--enable-auto-tool-choice` + `--tool-call-parser none` | fails, mirroring `cli_args.py:395` | +| notice emission | each accepted flag names itself and its reason on stderr | + +The second row is the one that matters. A mutation that turns the table into a +catch-all must turn that test RED; if it does not, the test is not defending the +guarantee. Prove it by mutating in a scratch copy, then restore the tree +byte-for-byte. + +## Gates + +Focused: the new test file. Full: `scripts/agent-preflight.sh --staged` plus the +serve conformance suite. No GPU, no oracle run — this is an argument-parsing +change and does not touch a forward pass. + +## Risks / decisions + +- **Risk**: the table becomes a dumping ground for anything that fails to parse. + Mitigated by rule 1 plus the per-entry reason string — an entry with no honest + reason cannot be written. +- **Decision**: `--trust-remote-code` is accepted rather than rejected-with-advice. + Rejecting it with "we don't need this" would still abort the recipe command, + which is the entire defect. +- **Decision**: flags that are inert *because we lack the capability* — TP, EP, + `--mm-encoder-tp-mode` — are **NOT** in this table. They keep aborting. Accepting + them would be the failure mode this seam exists to prevent. +- **Out of scope**: `--language-model-only` (#607) is a real capability gap, not an + inert flag, and must not be quietly added here. + +## Now + +`SPIKE` — spec committed, implementation not started. Next: a fresh implementer +takes the table, the four tests, and the mutation proof. diff --git a/scripts/check-agent-record.py b/scripts/check-agent-record.py index 4817336c2..88e2e37c6 100644 --- a/scripts/check-agent-record.py +++ b/scripts/check-agent-record.py @@ -372,8 +372,15 @@ # while its committed specification awaits implementation and hosted evidence. # No build, artifact, runtime evidence, workflow, or publication is claimed by # this row-count bump. +# 153 since 2026-08-13: +`SERVE-RECIPE-ARGS` (accepted-and-inert serve arguments). +# `vllm-serve` aborts on any unrecognized flag, so `--enable-auto-tool-choice` +# (89 of 157 official vLLM recipes) and `--trust-remote-code` (82 of 157) stop the +# server before model load even though neither means anything to this engine — +# including for models we ship token-exact and gated. Found by the 2026-08-13 +# recipe-surface sweep, issue #606; `SPIKE` on its committed spec. The row claims +# no implementation: no seam, no test, and no `docs/USAGE.md` entry exists yet. # Bumped for a real new row, never to make a failing state transition pass. -ENGINE_ROWS = 152 +ENGINE_ROWS = 153 ENGINE_SUMMARY_SECTIONS = ( ("Engine and scheduling", "Engine core and scheduling"), From c95ba2886a65d2e548589192aee4e90f54d05e1c Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 13 Aug 2026 10:41:27 +0000 Subject: [PATCH 2/2] test(agent-record): pin the SERVE-RECIPE-ARGS row to its ratchet bump (#606) FOLLOWING_AGENTS_PROTOCOL check-pr-size.py classifies scripts/check-agent-record.py as a governance checker and requires companion evidence in tests/scripts/test_agent_record.py for any change to it -- including the ENGINE_ROWS 152 -> 153 bump that carries the new row. That rule is right: without it, a bump made to silence a broken count looks identical to a bump made for a real row. Mirrors test_windows_release_row_is_inside_the_engine_ratchet, added for the same reason when the constant last moved. Two mutations were run in the worktree and BOTH were caught, but neither by this assertion: deleting the row is caught by the count ratchet in setUpClass (152 engine rows; expected 153), and renaming it while keeping the count is caught by the claim cross-check (owner CLAIM-SERVE-RECIPE-ARGS does not claim active row SERVE-RECIPE-ARGZ). So this test is a third pin on an invariant two existing guards already defend, not an independent guard -- the same is true of the Windows test it mirrors, and it is worth saying plainly rather than reporting a mutation proof that was actually someone else's assertion firing. The tree was restored byte-for-byte after each mutation; git diff HEAD over .agents/ is empty. python3 -m unittest tests.scripts.test_agent_record: 50 tests, OK. Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --- tests/scripts/test_agent_record.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/scripts/test_agent_record.py b/tests/scripts/test_agent_record.py index bfbb37563..676d37a8d 100644 --- a/tests/scripts/test_agent_record.py +++ b/tests/scripts/test_agent_record.py @@ -251,6 +251,22 @@ def test_windows_release_row_is_inside_the_engine_ratchet(self) -> None: self.assertEqual(len(windows), 1) self.assertEqual(windows[0].path.name, "engine-matrix.md") + def test_recipe_args_row_is_inside_the_engine_ratchet(self) -> None: + """The #606 row and its ratchet bump are one semantic change. + + `SERVE-RECIPE-ARGS` is the reason `ENGINE_ROWS` moved 152 -> 153. Pinning + the row here is what separates a bump that carries a real row from a bump + that silences a count someone broke: if the row is ever dropped while the + constant stays, this fails rather than the arithmetic quietly agreeing. + """ + + errors: list[str] = [] + rows, _ = agent_record.check_matrices(errors) + self.assertEqual([error for error in errors if "engine rows" in error], []) + recipe_args = [row for row in rows if row.item_id == "SERVE-RECIPE-ARGS"] + self.assertEqual(len(recipe_args), 1) + self.assertEqual(recipe_args[0].path.name, "engine-matrix.md") + def test_model_row_ratchet_is_load_bearing(self) -> None: """The MODEL row pin must catch a row appearing or vanishing.