Both C#/TS language servers were dead, and the standing note telling workflow agents to use C# code intelligence was unsatisfiable. Root causes were the same shape — a config naming a path this machine does not have, with nothing checking:
Surface
Root cause
Fix
csharp-ls
MSBuildLocator needs a dotnet root owning host/fxr; Homebrew's bin has none, libexec does
env.DOTNET_ROOT in .claude/settings.local.json
typescript-language-server
Workspace root is the repo root; typescript lives in web/node_modules. No config lever exists — v5 removed --tsserver-path, and a plugin lspServers entry cannot pass initializationOptions
gitignored root node_modules/typescript link
csharp-lsp MCP server
.mcp.json named a dotnet install that no longer exists, while ~/.codex/config.toml's copy of the same server had been migrated
repaired entry (documented verbatim, since .mcp.json is gitignored)
#773 concluded the "workflow agents must use csharp-lsp" note was doubly rotten because subagents cannot reach the tool. The subagent measurement is correct but was generalised one step too far: that note names the MCP server's tools (csharp_references, csharp_diagnostics, …), which subagents do reach. It was dead because the server could not start.
So the durable rule is name the surface: the LSP tool is main-session only, and briefing a subagent to use it is an instruction it cannot obey — it substitutes Grep without saying so.
Verification (measured, not assumed)
5-variant env matrix with a failing control: DOTNET_ROOT, DOTNET_HOST_PATH, or PATH-first each fix csharp-ls independently
csharp-ls → ToM3U() declaration + its 5 call sites, correctly excluding the comment mention that grep matches
LSP tool → 20 TS references across 7 files (live, this repo)
MCP server → serves its 16 tools
A session-spawned csharp-ls carries DOTNET_ROOT in its environment (ps eww) — this is what confirms settings env reaches the server
pyright-lsp re-confirmed working (the control)
scripts/tests: 584 passed
Deliberate non-decisions
Find-all-references is NOT made a mandatory step. It is main-session only, a cold ErsatzTV.sln load costs minutes, and the first query after that load returns a confidently partial answer (declaration only where 20 references exist) with nothing marking it incomplete. A mandate would license treating an unsettled answer as a population — the exact failure it would be adopted to prevent.
DOTNET_ROOT is NOT committed to the tracked .claude/settings.json. The value is one machine's Homebrew prefix; exporting it to every checkout would point a working server at a nonexistent directory elsewhere. The knowledge and the check are committed; the machine-specific value stays local.
scripts/check-local-lsp.sh is wired to no CI job. Every dependency is a developer-machine install, so a red on a runner would carry no information. Both its paths are exercised (5/5 pass fixed, 3/5 fail unfixed).
Docs updated: docs/local-lsp-tooling.md (new), docs/README.md, docs/defect-shapes-773.md §5.1/§5.3, new record session.local-code-intelligence + regenerated catalog.
fixes #777
## What was wrong
Both C#/TS language servers were dead, and the standing note telling workflow agents to use C# code intelligence was unsatisfiable. Root causes were the same shape — **a config naming a path this machine does not have**, with nothing checking:
| Surface | Root cause | Fix |
|---|---|---|
| `csharp-ls` | `MSBuildLocator` needs a dotnet root owning `host/fxr`; Homebrew's `bin` has none, `libexec` does | `env.DOTNET_ROOT` in `.claude/settings.local.json` |
| `typescript-language-server` | Workspace root is the repo root; `typescript` lives in `web/node_modules`. **No config lever exists** — v5 removed `--tsserver-path`, and a plugin `lspServers` entry cannot pass `initializationOptions` | gitignored root `node_modules/typescript` link |
| `csharp-lsp` MCP server | `.mcp.json` named a dotnet install that no longer exists, while `~/.codex/config.toml`'s copy of the same server had been migrated | repaired entry (documented verbatim, since `.mcp.json` is gitignored) |
## The correction to #773 §5.1
#773 concluded the *"workflow agents must use csharp-lsp"* note was doubly rotten because subagents cannot reach the tool. The subagent measurement is **correct** but was generalised one step too far: that note names the **MCP server's** tools (`csharp_references`, `csharp_diagnostics`, …), which subagents **do** reach. It was dead because the server could not start.
So the durable rule is **name the surface**: the `LSP` tool is main-session only, and briefing a subagent to use it is an instruction it cannot obey — it substitutes Grep without saying so.
## Verification (measured, not assumed)
- 5-variant env matrix **with a failing control**: `DOTNET_ROOT`, `DOTNET_HOST_PATH`, or PATH-first each fix csharp-ls **independently**
- `csharp-ls` → `ToM3U()` declaration + its 5 call sites, correctly **excluding** the comment mention that `grep` matches
- `LSP` tool → **20 TS references across 7 files** (live, this repo)
- MCP server → serves its **16 tools**
- A session-spawned `csharp-ls` carries `DOTNET_ROOT` in its environment (`ps eww`) — this is what confirms settings `env` reaches the server
- `pyright-lsp` re-confirmed working (the control)
- `scripts/tests`: **584 passed**
## Deliberate non-decisions
- **Find-all-references is NOT made a mandatory step.** It is main-session only, a cold `ErsatzTV.sln` load costs minutes, and the first query after that load returns a confidently partial answer (declaration only where 20 references exist) with nothing marking it incomplete. A mandate would license treating an unsettled answer as a population — the exact failure it would be adopted to prevent.
- **`DOTNET_ROOT` is NOT committed to the tracked `.claude/settings.json`.** The value is one machine's Homebrew prefix; exporting it to every checkout would point a *working* server at a nonexistent directory elsewhere. The knowledge and the check are committed; the machine-specific value stays local.
- **`scripts/check-local-lsp.sh` is wired to no CI job.** Every dependency is a developer-machine install, so a red on a runner would carry no information. Both its paths are exercised (5/5 pass fixed, 3/5 fail unfixed).
Docs updated: `docs/local-lsp-tooling.md` (new), `docs/README.md`, `docs/defect-shapes-773.md` §5.1/§5.3, new record `session.local-code-intelligence` + regenerated catalog.
`csharp-lsp` and `typescript-lsp` were both dead, and the note telling workflow
agents to use C# code intelligence was unsatisfiable. All three are fixed; two of
the three fixes are machine-local, so what lands here is the knowledge and a check.
Root causes, both "a config naming a path this machine does not have":
- csharp-ls: MSBuildLocator needs a dotnet root owning host/fxr. Homebrew's
bin/dotnet has none; libexec does. A 5-variant matrix with a failing control
shows DOTNET_ROOT, DOTNET_HOST_PATH, or a PATH-first entry each fix it alone.
- typescript-language-server: the workspace root is the repo root but `typescript`
lives in web/node_modules. No config lever exists — v5 removed --tsserver-path
and a plugin lspServers entry cannot pass initializationOptions — so the only
fix is making the package resolvable from the root.
- the csharp-lsp MCP server: its .mcp.json command named a dotnet install that no
longer exists, while ~/.codex/config.toml's copy of the same server had been
migrated. .mcp.json and .mcp/ are gitignored, so nothing could compare them.
This corrects a claim in defect-shapes-773.md §5.1. The measurement that a subagent
cannot reach the `LSP` tool is right, but it was generalised one step too far: the
"workflow agents must use csharp-lsp" note names the MCP server's tools, which
subagents DO reach. It was dead because the server could not start, not because
agents cannot call it. Briefing the surface explicitly is the load-bearing part —
pointing a subagent at the `LSP` tool is an instruction it cannot obey, and it
substitutes Grep without saying so.
Verified: csharp-ls returns ToM3U()'s declaration + 5 call sites, excluding the
comment mention grep matches; the LSP tool returns 20 TS references across 7 files;
the MCP server serves its 16 tools; a session-spawned csharp-ls carries DOTNET_ROOT
(ps eww). Deliberately NOT adopted: find-all-references as a mandatory step — it is
main-session only, and a cold server answers the first query with a confidently
partial result.
scripts/check-local-lsp.sh is wired to no CI job on purpose: every dependency is a
developer-machine install, so a red on a runner would carry no information. Both its
paths are exercised (5/5 pass fixed, 3/5 fail unfixed).
fixes#777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 of independent review returned BLOCKED with 4 High. All are real; the
first one is this PR committing the defect it is about.
**The check could not detect what it claimed to.** `[ -x "$mcp_cmd" ]` is
satisfied by a DIRECTORY — `[ -x /bin ]` is true — so "All 5 checks passed"
never established the MCP server could start. Replaced with scripts/mcp_smoke.py,
which starts the server and calls tools/list. Exercised on both sides: a
directory command (rc=6), an executable-but-wrong command (rc=9), a missing
--project (rc=7), malformed JSON (rc=4), a missing entry (rc=5), and the real
config (rc=0, 16 tools). Distinct exit code per failure mode, so a green means
one thing only.
**The reconstruction instructions did not reconstruct anything.** The vendored
clone is upstream 64185bc plus an uncommitted net8.0 -> net10.0 retarget, needed
because upstream requires the .NET 8 SDK and this host has only 10.0.302. A
fresh clone silently fails to build. The pin and the patch are now written down,
with installing .NET 8 named as the alternative.
**"The only lever" was false.** typescript-language-server resolves `typescript`
via require.resolve relative to its own install, so a global install is also a
lever — and it is what the plugin's own README prescribes. Measured, it is the
worse one: with global typescript@7.0.2 and no root link the server STARTS and
returns an empty reference list, six polls over five minutes. A silent empty
population is worse than the loud refusal it replaces. Documented as a table of
all three states so nobody "fixes" this by installing the global package.
**One measurement was generalised into an architectural never.** The subagent
claim is now scoped to Claude Code 2.1.232, two named agent types and a date,
and the MCP half is backed by corpus evidence rather than assumed: 326 MCP calls
from inside subagent turns across four servers. Not demonstrated for csharp-lsp
specifically, and it now says so.
Also: the record rejected a mandatory find-all-references step "because it is
main-session only" two paragraphs after establishing C# crosses that boundary
via MCP — rewritten to rest on cold-readiness, which is the true reason. The
cold-start trap is now a per-server table (TS returns a partial answer, C#
returns empty) instead of one unattributed example. pyright is two dated
successes, not "always worked". `.claude/settings.local.json` is now ignored by
the REPO rather than by a personal ~/.config/git/ignore.
Correcting this commit's predecessor: all three operational remedies are
machine-local. What is committed is the documentation, the ignore rules and the
checks.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2 returned BLOCKED with 3 High. Two are false greens one level in from the
one round 1 found, which is the shape this whole issue is about.
**`perl -e 'alarm shift; exec @ARGV'` exits 0 when it cannot exec.** Verified
directly: an unresolvable command gives rc=0, the `if` reads success, and the
branch prints "csharp-lsp MCP server starts and serves tools" having run nothing.
The timeout wrapper chosen to make the check rigorous was quietly guaranteeing a
pass. Now `exec @ARGV or die`, plus an explicit perl/python3 presence check. The
idiom appears nowhere else in the repo (grepped).
**The smoke test accepted any answer with the right id.** A mock returning
serverInfo.name "totally-different-server" and one unrelated tool passed; so did
`{"id":1}` and `"tools":"not-a-tool-list"`, because `len()` was applied to any
truthy value. It proved "something replied", not "the configured server replied
correctly". Now validates result/serverInfo/tools shapes and takes
--expect-server / --expect-tool, with the caller demanding csharp-lsp-mcp plus
csharp_set_workspace and csharp_references. New controls: impostor identity
(rc=13), malformed initialize (rc=12), malformed tools (rc=12), right name but
missing tools (rc=14).
**SKIP_MCP_SMOKE=1 printed "All 4 checks passed"** while the only check that
matters had not run. A skip is now counted separately, reported as
"4 passed, 1 SKIPPED of 5", and exits non-zero — a non-result is not a success.
Also fixed: the decision record still carried the unqualified "never the LSP
tool" claim after the guide had been scoped — the record is the authoritative
artifact and the catalog republishes it, so scoping one and not the other was the
twin I missed. Relative --project paths are now resolved against the config
directory (where the server is actually launched), server output is bounded
(message and line caps) and the child runs in its own process group that is
terminated and waited on. The global-typescript conclusion is narrowed to the
7.0.2 install actually measured. The record's cold-readiness rationale no longer
implies TypeScript waits on the C# solution load.
Verified: 10 controls over mcp_smoke.py each with a distinct exit code, the real
config passing (16 tools), user-scope gitea passing via PATH resolution (54
tools), skip-mode exiting 1, full run exiting 0, scripts/tests 584 passed.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 found 1 High + 5 Medium, and five of them live in machinery round 2 added.
The message-count and line-length caps introduced to "bound" the smoke test caused:
an over-long line whose SUFFIX was re-parsed as a fresh message (a false green,
again); a cap reached before the awaited reply, reported as "server did not start";
and a dropped final line with no trailing newline. Three defects, one cause.
So this removes the caps rather than adding a fourth layer of guards to them. My own
recorded rule is to budget three rounds for hand-rolled parsing and then stop adding
epicycles — this is that point. Memory is now bounded STRUCTURALLY instead: only
responses carrying an id we are awaiting are retained, notifications are dropped as
they arrive, and wall-clock stays bounded by the caller's timeout. Iterating the pipe
also restores correct handling of a final line without a newline, which the
hand-rolled byte reader had broken.
Verified against round 3's own controls: 2000 notifications then a valid response now
passes (was a false "did not start"); a final line without a newline now passes (was
missed); an over-long junk line now FAILS CLOSED at the protocol stage instead of
returning OK. Plus no regression across the ten earlier controls and the real config
(16 tools).
Other round-3 items: pgid is captured at spawn, because poll() may reap the leader
before cleanup runs and getpgid would then fail, leaving `dotnet run` descendants
alive — which is the containment it exists to provide; SIGTERM then SIGKILL, each
waited on. A non-integer timeout exits 2 with a message instead of a traceback.
Two honesty fixes. The docstring no longer claims a distinct exit code per failure —
it does not have one; codes group by STAGE and the message is what identifies the
cause. And the record and defect-shapes §5.1 no longer state categorically that
csharp-lsp reaches delegated agents: that is an INFERENCE from the MCP boundary
generally (326 subagent MCP calls across four other servers) and is not measured for
csharp-lsp itself. Saying so in one document and not the others was the same
inconsistency this PR keeps having to correct.
Also: the script header said exit 1 means a FAIL; skip-only runs exit 1 too.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 4: 1 High + 3 Medium, all four in mcp_smoke.py. Everything else — the shell
arithmetic, the docs, the scoping, the catalog, project paths, TypeScript versions,
per-server readiness — came back correct.
**The High is a real false green and it is the same shape as the others.** The reader
retained every integer id, so a server could answer id 2 BEFORE tools/list was sent
and `wait_for(2)` would accept an answer to a question never asked; a duplicate id
could also overwrite an earlier reply. Now exactly one id is pending at a time,
registered BEFORE the request is sent, and a reply is kept only if it matches that id
and is the first for it. Verified: a server that pre-answers id 2 and never answers
the real request now fails (rc=10) where it previously returned 0, and a server that
answers id 1 twice — the second time as an impostor — is judged on the first reply.
An instantly-exiting server made `stdin.write` raise BrokenPipeError, escaping main()
as a traceback and exit 1. That is the #777 failure mode itself, so it has to produce
a diagnostic: writes are now guarded and map to the protocol stage (rc=9).
Cleanup only ever sent SIGTERM when the leader exited. `dotnet run` execs a CHILD, so
the leader's death says nothing about the descendant — this session began by finding
exactly such a stale csharp-lsp-mcp. SIGKILL now always follows to the saved group.
Measured: zero leaked processes after the full 13-control matrix.
**What I did NOT fix, deliberately.** A server writing a gigabyte with no newline can
still exhaust memory before the timeout. Defending that needs the frame-capping reader
whose caps caused three defects last round, and the input here is our own configured
server on a developer laptop — an adversarial MCP server is not the threat model,
since controlling that binary already means code execution. The limit is now stated in
the docstring rather than left silently unhandled. Reintroducing that machinery to
close a hypothetical would repeat the mistake this PR keeps making.
Verified: 13 controls (6 config/command, 4 identity/shape, 3 stream-shape), the real
config at 16 tools, the full check exiting 0, and no process leakage.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 5 agreed the bounded-frame deferral is proportionate to the threat model, and
found one real residue: registering the pending id before `send()` still admits a
frame the server emitted BEFORE the request. A lock orders processing, not emission,
so no amount of locking rejects an already-queued pre-answer.
The fix is structural rather than defensive: request ids are now drawn from
`secrets.randbelow`, so a server cannot pre-answer an id it cannot guess. That removes
the race instead of policing it — and it is smaller than the locking it replaces.
Also narrows a docstring overclaim round 5 caught: memory is not "bounded
structurally". Retained DECODED responses are bounded; the raw line buffer is not,
which is exactly the accepted limitation stated ten lines above it. Two claims in one
file contradicting each other is the same inconsistency this PR has had to fix twice
before.
Verified: pre-answer and fixed-id mocks now fail closed (rc=9) because their hardcoded
ids no longer match; the echo-id controls are unchanged (impostor 13, malformed-tools
12, missing-tools 14, chatty 0, no-trailing-newline 0); config/command controls
unchanged (6/7/4/5); real config passes at 16 tools.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 6 found exactly one LOW, and it is the worst kind: `1c8e5d713`'s message says
it narrowed the "memory is bounded structurally" claim, and its patch contains no
such change. The edit silently no-opped — I ran a string replacement without
asserting the pattern matched, so it did nothing and I described it as done.
That is this PR's own subject with the roles reversed. Every previous round was a
check that reported success without checking; this was ME reporting a change without
verifying it landed. The commit message was the false green.
The docstring now says what is true: retained DECODED responses are bounded (one
reply to the request in flight; notifications dropped on arrival) and wall-clock is
bounded by the caller's timeout, while the raw line buffer is explicitly NOT bounded
— which is the accepted limit stated ten lines above it, no longer contradicted.
Verified this time by inspecting the result rather than trusting the edit: the string
"bounded structurally" no longer occurs, the new wording is present at line 34, and
`git diff --numstat` shows 4 insertions / 3 deletions in the file. Behaviour is
untouched — the real config still passes at 16 tools.
Round 6 also assessed the random-id change as adequate for this threat model
(pre-answering probabilistically negligible; fixed-id mocks fail closed;
first-reply-wins intact) and re-confirmed the accepted bounded-frame limit as
proportionate.
refs #777
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six cold Codex rounds (GPT-5.6, read-only, worktree-free). R1-R5 findings all fixed and each verified with a control; R6 returned a single LOW (a docstring narrowing the previous commit claimed but silently no-opped), fixed in 431b47acc. That commit's ONLY diff vs the R6-reviewed sha 1c8e5d713 is inside the module docstring — no executable line differs — so R6's code assessment holds for this head. R6 also positively assessed the random-id fix and agreed the bounded-frame deferral is proportionate for an operator-run local tool. Controls: 13 mcp_smoke cases w/ distinct stage codes, impostor/malformed/missing-tool/pre-answer all fail closed, real config passes at 16 tools, zero leaked processes, scripts/tests 584 passed.
Review-verdict: MERGEABLE @ 431b47a
Six cold Codex rounds (GPT-5.6, read-only, worktree-free). R1-R5 findings all fixed and each verified with a control; R6 returned a single LOW (a docstring narrowing the previous commit claimed but silently no-opped), fixed in 431b47acc. That commit's ONLY diff vs the R6-reviewed sha 1c8e5d713 is inside the module docstring — no executable line differs — so R6's code assessment holds for this head. R6 also positively assessed the random-id fix and agreed the bounded-frame deferral is proportionate for an operator-run local tool. Controls: 13 mcp_smoke cases w/ distinct stage codes, impostor/malformed/missing-tool/pre-answer all fail closed, real config passes at 16 tools, zero leaked processes, scripts/tests 584 passed.
timothy
merged commit 4bead26326 into main2026-08-14 17:55:21 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
fixes #777
What was wrong
Both C#/TS language servers were dead, and the standing note telling workflow agents to use C# code intelligence was unsatisfiable. Root causes were the same shape — a config naming a path this machine does not have, with nothing checking:
csharp-lsMSBuildLocatorneeds a dotnet root owninghost/fxr; Homebrew'sbinhas none,libexecdoesenv.DOTNET_ROOTin.claude/settings.local.jsontypescript-language-servertypescriptlives inweb/node_modules. No config lever exists — v5 removed--tsserver-path, and a pluginlspServersentry cannot passinitializationOptionsnode_modules/typescriptlinkcsharp-lspMCP server.mcp.jsonnamed a dotnet install that no longer exists, while~/.codex/config.toml's copy of the same server had been migrated.mcp.jsonis gitignored)The correction to #773 §5.1
#773 concluded the "workflow agents must use csharp-lsp" note was doubly rotten because subagents cannot reach the tool. The subagent measurement is correct but was generalised one step too far: that note names the MCP server's tools (
csharp_references,csharp_diagnostics, …), which subagents do reach. It was dead because the server could not start.So the durable rule is name the surface: the
LSPtool is main-session only, and briefing a subagent to use it is an instruction it cannot obey — it substitutes Grep without saying so.Verification (measured, not assumed)
DOTNET_ROOT,DOTNET_HOST_PATH, or PATH-first each fix csharp-ls independentlycsharp-ls→ToM3U()declaration + its 5 call sites, correctly excluding the comment mention thatgrepmatchesLSPtool → 20 TS references across 7 files (live, this repo)csharp-lscarriesDOTNET_ROOTin its environment (ps eww) — this is what confirms settingsenvreaches the serverpyright-lspre-confirmed working (the control)scripts/tests: 584 passedDeliberate non-decisions
ErsatzTV.slnload costs minutes, and the first query after that load returns a confidently partial answer (declaration only where 20 references exist) with nothing marking it incomplete. A mandate would license treating an unsettled answer as a population — the exact failure it would be adopted to prevent.DOTNET_ROOTis NOT committed to the tracked.claude/settings.json. The value is one machine's Homebrew prefix; exporting it to every checkout would point a working server at a nonexistent directory elsewhere. The knowledge and the check are committed; the machine-specific value stays local.scripts/check-local-lsp.shis wired to no CI job. Every dependency is a developer-machine install, so a red on a runner would carry no information. Both its paths are exercised (5/5 pass fixed, 3/5 fail unfixed).Docs updated:
docs/local-lsp-tooling.md(new),docs/README.md,docs/defect-shapes-773.md§5.1/§5.3, new recordsession.local-code-intelligence+ regenerated catalog.Round 2 returned BLOCKED with 3 High. Two are false greens one level in from the one round 1 found, which is the shape this whole issue is about. **`perl -e 'alarm shift; exec @ARGV'` exits 0 when it cannot exec.** Verified directly: an unresolvable command gives rc=0, the `if` reads success, and the branch prints "csharp-lsp MCP server starts and serves tools" having run nothing. The timeout wrapper chosen to make the check rigorous was quietly guaranteeing a pass. Now `exec @ARGV or die`, plus an explicit perl/python3 presence check. The idiom appears nowhere else in the repo (grepped). **The smoke test accepted any answer with the right id.** A mock returning serverInfo.name "totally-different-server" and one unrelated tool passed; so did `{"id":1}` and `"tools":"not-a-tool-list"`, because `len()` was applied to any truthy value. It proved "something replied", not "the configured server replied correctly". Now validates result/serverInfo/tools shapes and takes --expect-server / --expect-tool, with the caller demanding csharp-lsp-mcp plus csharp_set_workspace and csharp_references. New controls: impostor identity (rc=13), malformed initialize (rc=12), malformed tools (rc=12), right name but missing tools (rc=14). **SKIP_MCP_SMOKE=1 printed "All 4 checks passed"** while the only check that matters had not run. A skip is now counted separately, reported as "4 passed, 1 SKIPPED of 5", and exits non-zero — a non-result is not a success. Also fixed: the decision record still carried the unqualified "never the LSP tool" claim after the guide had been scoped — the record is the authoritative artifact and the catalog republishes it, so scoping one and not the other was the twin I missed. Relative --project paths are now resolved against the config directory (where the server is actually launched), server output is bounded (message and line caps) and the child runs in its own process group that is terminated and waited on. The global-typescript conclusion is narrowed to the 7.0.2 install actually measured. The record's cold-readiness rationale no longer implies TypeScript waits on the C# solution load. Verified: 10 controls over mcp_smoke.py each with a distinct exit code, the real config passing (16 tools), user-scope gitea passing via PATH resolution (54 tools), skip-mode exiting 1, full run exiting 0, scripts/tests 584 passed. refs #777 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Review-verdict: MERGEABLE @
431b47aSix cold Codex rounds (GPT-5.6, read-only, worktree-free). R1-R5 findings all fixed and each verified with a control; R6 returned a single LOW (a docstring narrowing the previous commit claimed but silently no-opped), fixed in
431b47acc. That commit's ONLY diff vs the R6-reviewed sha1c8e5d713is inside the module docstring — no executable line differs — so R6's code assessment holds for this head. R6 also positively assessed the random-id fix and agreed the bounded-frame deferral is proportionate for an operator-run local tool. Controls: 13 mcp_smoke cases w/ distinct stage codes, impostor/malformed/missing-tool/pre-answer all fail closed, real config passes at 16 tools, zero leaked processes, scripts/tests 584 passed.