From 716f37d4be3fc7813568da9c51eb6bc3b1621697 Mon Sep 17 00:00:00 2001 From: osprey-d90d Date: Mon, 24 Aug 2026 03:53:09 +0200 Subject: [PATCH 1/3] fix(coord): bind tier-4 engine to wrapper manifest --- registry/repos.lock | 2 +- scripts/fsgg-coord | 94 ++++++++++++-------- tests/coord-engine-parity/shim.sh | 139 +++++++++++++++--------------- 3 files changed, 131 insertions(+), 104 deletions(-) diff --git a/registry/repos.lock b/registry/repos.lock index a157171f3..aab965004 100644 --- a/registry/repos.lock +++ b/registry/repos.lock @@ -10,7 +10,7 @@ f7de20e04981ed72ce6bfe886a1776adb78b3926c2a83f315069a3ce8e4fa3f3 .claude/skills bc040e2ba93b490a31f868497e46eeb2c6a2eea6f27a3fc90bb21361887aefe0 .claude/skills/cross-repo-coordination 7bee25b83b422cf0654178a741de4e90a424c24931ae119004843c5fc74b784f .claude/skills/intra-repo-parallel-work 44c2aacecbdbbb4f80f7b2f07379ec6ee38502ac84b5c991ea472b09baba29f3 .claude/skills/pnext-item -d8a2c754a16bc298843297dc0b6cc49e7d2502446d9d4a9880bee7aabb2b34f3 scripts/fsgg-coord +6085a6540f874cfd3c70e367e219eb5548180eca3e28434b0da81efb040664b2 scripts/fsgg-coord e1f63cda5b499f4fdd4396626c0bac013c49019daf182336df8c7513d24918ba scripts/fsgg-coord-report 1f2a70607268d549aa59c56e612e654e52021e9b7015e26c8fdaa97a4a3a869e scripts/lib/args.sh fbdedf5e5bb4f85ef048807a430218001f489172210f87acd9bf8b3fbbca0330 scripts/lib/roots.sh diff --git a/scripts/fsgg-coord b/scripts/fsgg-coord index 1b2f8e9dd..53059be61 100755 --- a/scripts/fsgg-coord +++ b/scripts/fsgg-coord @@ -36,10 +36,10 @@ # therefore cannot prevent the coordination tool from being fixed."* So where a # source build exists it is the AUTHORITY, and it outranks both packaged forms. # 3. a global tool on PATH — `dotnet tool install -g`, or any binary an operator put on PATH. -# 4. a LOCAL tool manifest — the receivers' shape: `.config/dotnet-tools.json` + `dotnet tool run`. A -# restored local tool is NOT on PATH (it answers only to `dotnet tool run`), -# so a manifest that DECLARES the engine but has not been restored is restored -# here — a manifest is a wish until `dotnet tool restore` grants it (#655). +# 4. a LOCAL tool manifest — the receivers' shape: `.config/dotnet-tools.json` + its exact restored +# assembly. A restored local tool is NOT on PATH, so the wrapper-root manifest +# is resolved through the SDK cache and that assembly is exec'd from the caller's +# cwd. A declaration not restored yet is restored here (#655, #2908). # # THE NUMBERS MOVED, AND #1018 IS WHY (old → new: 4 → 2, 2 → 3, 3 → 4; tier 1 is unchanged). The source # build used to be LAST, beneath both packaged forms — an order "lifted verbatim from bash's proven @@ -330,23 +330,53 @@ if command -v fsgg-coord-engine >/dev/null 2>&1; then exec fsgg-coord-engine "$@" fi -if [ -n "$TOP" ]; then +WRAPPER_TOP="$(git -C "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" rev-parse --show-toplevel 2>/dev/null || true)" +if [ -n "$WRAPPER_TOP" ]; then # TIER 4 — THE RECEIVERS' SHAPE. Check the MANIFEST before probing `dotnet tool run` blindly — a `run` on # a repo with no manifest is a slow, noisy failure, and `.github` (whose source build is tier 2 above) has # no root manifest at all. - MANIFEST="$TOP/.config/dotnet-tools.json" + MANIFEST="$WRAPPER_TOP/.config/dotnet-tools.json" if [ -f "$MANIFEST" ] && command -v dotnet >/dev/null 2>&1 \ && jq -e '.tools | to_entries[] | select(.value.commands // [] | index("fsgg-coord-engine"))' \ >/dev/null 2>&1 <"$MANIFEST"; then - # A MANIFEST IS A DECLARATION, NOT AN INSTALLATION (#655). If the tool is not yet restored, `dotnet tool - # run` prints a restore hint and exits 1 — so restore it once (bookkeeping, in a subshell), then exec. - # Asking a worker to restore has a measured hit rate of zero; do it for them. - # `--` here is PROPHYLACTIC, not a fix: measured on dotnet 10.0.302, `tool run` does NOT consume - # `--version` (unlike `--help`/`-h`, which it does — see the exec below, #1029). It is separated anyway - # because this probe FAILS OPEN if that ever changes: a consumed `--version` would answer 0 from dotnet - # itself, the shim would read "already restored" and skip the restore, and the exec would then fail with - # the very hint this branch exists to pre-empt. One separator costs nothing and removes the whole class. - if ! ( cd "$TOP" && dotnet tool run fsgg-coord-engine -- --version ) >/dev/null 2>&1; then + # `dotnet tool run` has NO explicit-manifest option: it always searches from the process cwd. Calling it + # from $WRAPPER_TOP selects the right manifest but silently gives repository-sensitive commands the wrong + # cwd; calling it here preserves command scope but can select a different checkout's pin (.github#2908). + # Keep those two decisions separate. The manifest chooses one package+pin, the SDK's resolver cache names + # that restored pin's exact assembly, and `dotnet ` preserves the caller's cwd. + manifest_entry="$(jq -er ' + [.tools | to_entries[] + | select(.value.commands // [] | index("fsgg-coord-engine")) + | [.key, .value.version]] as $entries + | if ($entries | length) == 1 + then $entries[0] | @tsv + else error("expected exactly one fsgg-coord-engine declaration") + end' "$MANIFEST" 2>/dev/null)" \ + || die "the engine manifest is unreadable or ambiguous: $MANIFEST" + TOOL_PACKAGE="${manifest_entry%% *}" + TOOL_PIN="${manifest_entry#* }" + TOOL_CACHE_KEY="$(printf '%s' "$TOOL_PACKAGE" | tr '[:upper:]' '[:lower:]')" + TOOL_RESOLVER_ROOT="${DOTNET_CLI_HOME:-${HOME:-}}/.dotnet/toolResolverCache" + + resolve_manifest_engine() { + local resolver_file candidate resolved="" + [ -n "${DOTNET_CLI_HOME:-${HOME:-}}" ] || return 1 + for resolver_file in "$TOOL_RESOLVER_ROOT"/*/"$TOOL_CACHE_KEY"; do + [ -f "$resolver_file" ] || continue + candidate="$(jq -er --arg pin "$TOOL_PIN" ' + [.[] | select(.Version == $pin and .Name == "fsgg-coord-engine") | .PathToExecutable] + | if length == 1 then .[0] else empty end' "$resolver_file" 2>/dev/null)" || continue + [ -n "$resolved" ] && [ "$resolved" != "$candidate" ] && return 1 + resolved="$candidate" + done + [ -n "$resolved" ] && [ -f "$resolved" ] || return 1 + printf '%s' "$resolved" + } + + TOOL_ASSEMBLY="$(resolve_manifest_engine || true)" + # A MANIFEST IS A DECLARATION, NOT AN INSTALLATION (#655). If its exact package+pin does not resolve + # to a present assembly, restore THAT explicit manifest; never let restore rediscover from caller cwd. + if [ -z "$TOOL_ASSEMBLY" ]; then # THE RESTORE'S OWN DIAGNOSIS IS THE EVIDENCE — DO NOT DISCARD IT AND THEN GUESS (#957). # # This was `>/dev/null 2>&1 || die "… the feed is unreachable or the version is unpublished"`: @@ -358,7 +388,7 @@ if [ -n "$TOP" ]; then # checks their network; told "the version is unpublished" they go read the release workflow. # Neither is the remedy, and the real one was named nowhere. It cost the reporter a debugging # pass on an otherwise-clean checkout. - if ! restore_out="$( cd "$TOP" && dotnet tool restore 2>&1 )"; then + if ! restore_out="$(dotnet tool restore --tool-manifest "$MANIFEST" 2>&1)"; then # A HINT IS KEYED OFF THE OUTPUT, NEVER ASSERTED BLIND — that asymmetry IS the fix. Output we # do not recognise gets NO cause: the restore's own words are the finding, and inventing one on # top of them is the bug being removed. The two original causes survive here, demoted from @@ -397,29 +427,23 @@ if [ -n "$TOP" ]; then The version declared in the manifest is not on the feed. This is the 'unpublished' case, claimed here only because the restore reported it." ;; esac - die "the engine is declared in $MANIFEST but 'dotnet tool restore' failed: + die "the engine is declared in $MANIFEST (pin=$TOOL_PACKAGE@$TOOL_PIN) but 'dotnet tool restore --tool-manifest $MANIFEST' failed: $restore_out$hint" fi + TOOL_ASSEMBLY="$(resolve_manifest_engine || true)" + [ -n "$TOOL_ASSEMBLY" ] \ + || die "the engine manifest $MANIFEST pins $TOOL_PACKAGE@$TOOL_PIN, but restore produced no unique present resolver-cache assembly" fi - # exec from the ORIGINAL cwd, not $TOP: `dotnet tool run` finds the manifest by walking up from here, - # and the engine is cwd-sensitive (reap scopes to the checkout you stand in, #480; the git-remote repo - # default reads it, #430). A cd to $TOP would silently rescope those commands. - # - # `--` IS LOAD-BEARING, AND ITS ABSENCE IS WHY THIS SHIM WAS NOT THE TRANSPARENT PIPE ITS HEADER - # PROMISES (#1029). `dotnet tool run` has its OWN `-?, -h, --help` option, so without a separator it - # CONSUMES those flags and answers for itself — the engine is never reached. Measured on dotnet - # 10.0.302, and it is not position-dependent (`… fsgg-coord-engine next --help` is eaten too): - # - # dotnet tool run fsgg-coord-engine --help -> dotnet's own help, EXIT 0 - # dotnet tool run fsgg-coord-engine next --help -> dotnet's own help, EXIT 0 - # dotnet tool run fsgg-coord-engine -- next --help -> reaches the engine - # - # Exit 0 is what made it silent: the worker asked the coordination tool for help, got dotnet's, and - # nothing reported a failure. THIS repo cannot reproduce it — `.github` builds from source and resolves - # at tier 2, which is a real `exec` — so the only repo that owns the shim is the one repo where the bug - # is invisible, and every RECEIVER got dotnet's help instead (ADR-0034 decision 2). - exec dotnet tool run fsgg-coord-engine -- "$@" + RESOLVED_VERSION="$(dotnet "$TOOL_ASSEMBLY" --version 2>/dev/null || true)" + case "$RESOLVED_VERSION" in + "$TOOL_PIN"|"$TOOL_PIN.0") ;; + *) die "engine pin mismatch: manifest=$MANIFEST pin=$TOOL_PACKAGE@$TOOL_PIN resolvedAssembly=$TOOL_ASSEMBLY resolvedVersion=${RESOLVED_VERSION:-unreadable}" ;; + esac + + # No `cd`: repository-sensitive engine behaviour still observes the caller's working directory. Direct + # assembly execution also keeps argv transparent without `dotnet tool run` consuming -h/--help (#1029). + exec dotnet "$TOOL_ASSEMBLY" "$@" fi fi diff --git a/tests/coord-engine-parity/shim.sh b/tests/coord-engine-parity/shim.sh index b84c93955..d696392bb 100755 --- a/tests/coord-engine-parity/shim.sh +++ b/tests/coord-engine-parity/shim.sh @@ -1513,87 +1513,90 @@ else fi rm -rf "$NOHEAD" "$D" "$DWT" -# ---- 7. TIER 4 IS A PIPE TOO: `dotnet tool run` EATS --help (#1029) ------------------------------- +# ---- 7. TIER 4 BINDS THE WRAPPER MANIFEST WITHOUT CHANGING CALLER CWD (#2908, #1029) ------------- # -# The shim's header promises a transparent pipe that "adds no output of its own" and passes "argv through -# unchanged". On TIER 4 — the RECEIVERS' shape (`.config/dotnet-tools.json` + `dotnet tool run`) — it was -# not one. `dotnet tool run` has its OWN `-?, -h, --help` option and CONSUMES those flags before the tool -# is ever reached, answering with its own help and EXIT 0. So on every receiver repo a worker who asked the -# coordination tool for help got dotnet's, and nothing reported a failure. `--help` is how a worker -# discovers a verb or a flag (#891) — what was undeliverable was the whole usage, not one line of it. -# -# WHY IT SURVIVED IS STRUCTURAL, AND IT IS THIS FILE'S FAULT. `.github` builds the engine from source and -# resolves at TIER 2 (ADR-0034 decision 2), which is a genuine `exec` and genuinely transparent — so the ONE -# repo that owns the shim is the one repo where the bug cannot reproduce, and not one leg above drove tier 4 -# at all. Every tier the owner can reach was pinned; the tier only the receivers reach was not. -# -# WHY `dotnet` IS FAKED. Driving the real `dotnet tool run` needs the tool RESTORED from the auth-walled org -# feed, which no leg here may depend on. So the fake emulates exactly ONE behaviour, MEASURED against the -# real dotnet 10.0.302 rather than assumed — and nothing else: -# -# dotnet tool run fsgg-coord-engine --help -> dotnet's own help, exit 0 (eaten) -# dotnet tool run fsgg-coord-engine next --help -> dotnet's own help, exit 0 (eaten; NOT positional) -# dotnet tool run fsgg-coord-engine --version -> reaches the tool (NOT eaten) -# dotnet tool run fsgg-coord-engine -- next --help -> reaches the tool -# -# Leg (a) needs no model at all: it asserts what the SHIM HANDED dotnet, which is the header's contract -# stated as an argv. Legs (b)-(d) show why that matters, against a dotnet that eats what the real one eats. -T4="$(mktemp -d)"; DOTNETDIR="$(mktemp -d)"; ARGV_LOG="$T4/argv.log" +# A receiver wrapper can be invoked while the caller stands in another checkout. `dotnet tool run` has no +# explicit-manifest option and searches from that cwd, so the old probe/restore (wrapper tree) and execution +# (caller tree) could select different pins. This fixture gives BOTH trees a manifest and makes their versions +# observably different. The fake models the production resolver cache written by `dotnet tool restore`, then +# executes the exact assembly path the shim selected; it never talks to a feed or GitHub. +T4_BASE="$(mktemp -d)"; T4="$T4_BASE/wrapper"; CALLER="$T4_BASE/caller" +DOTNETDIR="$(mktemp -d)"; DOTNET_HOME="$(mktemp -d)"; ARGV_LOG="$T4_BASE/argv.log" +mkdir -p "$T4/scripts" "$T4/.config" "$CALLER/.config" "$DOTNET_HOME/.dotnet/toolResolverCache/1" "$DOTNET_HOME/assemblies/0.71.0" "$DOTNET_HOME/assemblies/0.72.0" ( cd "$T4" && git init -q . ) >/dev/null 2>&1 -mkdir -p "$T4/.config" +( cd "$CALLER" && git init -q . ) >/dev/null 2>&1 +cp "$SHIM" "$T4/scripts/fsgg-coord"; chmod +x "$T4/scripts/fsgg-coord" +T4_SHIM="$T4/scripts/fsgg-coord" cat >"$T4/.config/dotnet-tools.json" <<'JSON' { "version": 1, "isRoot": true, - "tools": { "fs.gg.coord.cli": { "version": "0.3.0", "commands": ["fsgg-coord-engine"] } } } + "tools": { "fs.gg.coord.cli": { "version": "0.72.0", "commands": ["fsgg-coord-engine"] } } } +JSON +cat >"$CALLER/.config/dotnet-tools.json" <<'JSON' +{ "version": 1, "isRoot": true, + "tools": { "fs.gg.coord.cli": { "version": "0.71.0", "commands": ["fsgg-coord-engine"] } } } +JSON +: >"$DOTNET_HOME/assemblies/0.71.0/fsgg-coord-engine.dll" +: >"$DOTNET_HOME/assemblies/0.72.0/fsgg-coord-engine.dll" +cat >"$DOTNET_HOME/.dotnet/toolResolverCache/1/fs.gg.coord.cli" <"$DOTNETDIR/dotnet" <<'SH' #!/usr/bin/env bash -# A fake `dotnet`. It emulates ONE measured behaviour of `dotnet tool run`: its own -?/-h/--help is -# consumed BEFORE any `--`, and answered by dotnet itself with exit 0. Everything after a `--` is the -# tool's. It NEVER talks to GitHub — a leg that can reach the real board is the bug, not the test for it. -if [ "${1:-}" = "tool" ] && [ "${2:-}" = "run" ]; then - shift 2 - printf '%s\n' "$*" >>"$ARGV_LOG_PATH" - shift # the tool name - for a in "$@"; do - case "$a" in - --) break ;; - -h|-\?|--help) echo "Description:"; echo " Run a local tool."; exit 0 ;; - esac - done - [ "${1:-}" = "--" ] && shift - echo "ENGINE RAN: $*" +if [ "${1:-}" = "tool" ] && [ "${2:-}" = "restore" ]; then + printf 'RESTORE cwd=%s args=%s\n' "$PWD" "$*" >>"$ARGV_LOG_PATH" + cp "$RESTORED_CACHE_FIXTURE" "$DOTNET_CLI_HOME/.dotnet/toolResolverCache/1/fs.gg.coord.cli" + exit 0 +fi +assembly="${1:-}"; shift || true +printf 'EXEC assembly=%s cwd=%s args=%s\n' "$assembly" "$PWD" "$*" >>"$ARGV_LOG_PATH" +pin="$(basename "$(dirname "$assembly")")" +if [ "${1:-}" = "--version" ]; then + printf '%s\n' "${FORCE_RESOLVED_VERSION:-$pin.0}" exit 0 fi -echo "fake dotnet: unhandled $*" >&2; exit 64 +printf 'ENGINE RAN pin=%s assembly=%s cwd=%s args=%s\n' "$pin" "$assembly" "$PWD" "$*" +exit 0 SH chmod +x "$DOTNETDIR/dotnet" -t4run() { ( cd "$T4" && env -u FSGG_COORD_ENGINE_BIN ARGV_LOG_PATH="$ARGV_LOG" PATH="$DOTNETDIR:$PATH" "$SHIM" "$@" 2>&1 ); } +t4run() { ( cd "$CALLER" && env -u FSGG_COORD_ENGINE_BIN DOTNET_CLI_HOME="$DOTNET_HOME" ARGV_LOG_PATH="$ARGV_LOG" PATH="$DOTNETDIR:/usr/bin:/bin" "$T4_SHIM" "$@" 2>&1 ); } -# a. THE CONTRACT, AS AN ARGV. What the shim hands dotnet must separate the caller's argv from dotnet's own -# options. This leg models nothing about dotnet — it reads what was handed over. +# a. POSITIVE + INVERSION: wrapper-root 0.72 wins over caller-root 0.71, while the engine still runs in +# CALLER. Output names the pin, assembly, cwd and argv, so every half is observable in one execution. : >"$ARGV_LOG"; out="$(t4run next --help)"; rc=$? -handed="$(tail -1 "$ARGV_LOG" 2>/dev/null)" -[ "$handed" = "fsgg-coord-engine -- next --help" ] \ - && ok "#1029: tier 4 hands dotnet a '--' separator, so the caller's argv is dotnet's tool args, not dotnet's own" \ - || bad "#1029: the shim must separate the caller's argv from dotnet's options" "handed: $handed" - -# b. AND SO THE ENGINE ANSWERS. The same call, against a dotnet that eats what the real one eats. -{ [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q 'ENGINE RAN: next --help'; } \ - && ok "#1029: ...so '--help' reaches the ENGINE on a receiver — the pipe is transparent at tier 4" \ - || bad "#1029: --help must reach the engine through tier 4" "rc=$rc out=$out" - -# c. THE NEGATIVE, and it is the whole bug: dotnet must not be the thing that answers. Exit 0 is what made -# this silent — a wrong answer that reported success. -printf '%s' "$out" | grep -q 'Run a local tool' \ - && bad "#1029: dotnet answered the help request — the shim is not a pipe at tier 4" "out: $out" \ - || ok "#1029: ...and dotnet does NOT answer for the engine (no 'Run a local tool', which exited 0 and read as success)" - -# d. `-h` IS THE SAME OPTION, and a fix that only separated `--help` would leave it eaten. -: >"$ARGV_LOG"; out="$(t4run -h)" -{ printf '%s' "$out" | grep -q 'ENGINE RAN: -h' && ! printf '%s' "$out" | grep -q 'Run a local tool'; } \ - && ok "#1029: '-h' reaches the engine too — dotnet's short form is separated by the same '--'" \ - || bad "#1029: -h must reach the engine as well" "out: $out" -rm -rf "$T4" "$DOTNETDIR" +{ [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -Fq "ENGINE RAN pin=0.72.0 assembly=$DOTNET_HOME/assemblies/0.72.0/fsgg-coord-engine.dll cwd=$CALLER args=next --help"; } \ + && ok ".github#2908: tier 4 binds execution to the WRAPPER manifest's 0.72.0 assembly while preserving caller cwd and argv" \ + || bad ".github#2908: wrapper manifest selection and command working context must be separate" "rc=$rc out=$out log=$(cat "$ARGV_LOG")" + +grep -q 'pin=0.71.0' <<<"$out" \ + && bad ".github#2908 inversion: caller manifest 0.71.0 silently won" "out=$out" \ + || ok ".github#2908 inversion: the conflicting CALLER manifest cannot select its 0.71.0 engine" + +# b. Direct assembly execution retains #1029: dotnet's tool-run parser never gets a chance to consume help. +{ grep -q 'args=next --help' <<<"$out" && ! grep -q 'tool run' "$ARGV_LOG"; } \ + && ok "#1029: tier 4 remains an argv-transparent pipe; --help reaches the engine, not dotnet tool run" \ + || bad "#1029: direct tier-4 execution must preserve engine argv" "out=$out log=$(cat "$ARGV_LOG")" + +# c. A missing exact wrapper pin restores the EXPLICIT wrapper manifest even though cwd carries another. +RESTORED_CACHE="$T4_BASE/restored-cache.json" +cp "$DOTNET_HOME/.dotnet/toolResolverCache/1/fs.gg.coord.cli" "$RESTORED_CACHE" +jq '[.[] | select(.Version != "0.72.0")]' "$RESTORED_CACHE" >"$DOTNET_HOME/.dotnet/toolResolverCache/1/fs.gg.coord.cli" +: >"$ARGV_LOG" +out="$(RESTORED_CACHE_FIXTURE="$RESTORED_CACHE" t4run budget)"; rc=$? +{ [ "$rc" -eq 0 ] && grep -Fq "tool restore --tool-manifest $T4/.config/dotnet-tools.json" "$ARGV_LOG" \ + && grep -q 'ENGINE RAN pin=0.72.0' <<<"$out"; } \ + && ok ".github#2908: restore is explicitly bound to the wrapper manifest, then that exact pin executes" \ + || bad ".github#2908: restore must not rediscover the caller manifest" "rc=$rc out=$out log=$(cat "$ARGV_LOG")" + +# d. A resolver record whose assembly reports another version REFUSES and names all three authorities. +: >"$ARGV_LOG" +out="$(FORCE_RESOLVED_VERSION=0.71.0.0 t4run budget)"; rc=$? +{ [ "$rc" -eq 69 ] && printf '%s' "$out" | grep -Fq "manifest=$T4/.config/dotnet-tools.json pin=fs.gg.coord.cli@0.72.0" \ + && printf '%s' "$out" | grep -Fq "resolvedAssembly=$DOTNET_HOME/assemblies/0.72.0/fsgg-coord-engine.dll resolvedVersion=0.71.0.0"; } \ + && ok ".github#2908 inversion: a pin/resolved-version mismatch fails closed with manifest, pin and assembly evidence" \ + || bad ".github#2908: a wrong resolved assembly must never run silently" "rc=$rc out=$out log=$(cat "$ARGV_LOG")" + +rm -rf "$T4_BASE" "$DOTNETDIR" "$DOTNET_HOME" echo total=$((pass+failcount)) From 7c74e571d5a18d2b41986f246ec7e89fd3f8e2ba Mon Sep 17 00:00:00 2001 From: osprey-d90d Date: Mon, 24 Aug 2026 04:37:01 +0200 Subject: [PATCH 2/3] fix(coord): expose tier-4 resolution receipt --- registry/repos.lock | 2 +- scripts/fsgg-coord | 12 ++++++++++-- tests/coord-engine-parity/shim.sh | 17 +++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/registry/repos.lock b/registry/repos.lock index aab965004..d9bc0913e 100644 --- a/registry/repos.lock +++ b/registry/repos.lock @@ -10,7 +10,7 @@ f7de20e04981ed72ce6bfe886a1776adb78b3926c2a83f315069a3ce8e4fa3f3 .claude/skills bc040e2ba93b490a31f868497e46eeb2c6a2eea6f27a3fc90bb21361887aefe0 .claude/skills/cross-repo-coordination 7bee25b83b422cf0654178a741de4e90a424c24931ae119004843c5fc74b784f .claude/skills/intra-repo-parallel-work 44c2aacecbdbbb4f80f7b2f07379ec6ee38502ac84b5c991ea472b09baba29f3 .claude/skills/pnext-item -6085a6540f874cfd3c70e367e219eb5548180eca3e28434b0da81efb040664b2 scripts/fsgg-coord +a0689f0ba8cd0180fd1ca60a63bba55e93513732b36e099c541847336ddb3ccf scripts/fsgg-coord e1f63cda5b499f4fdd4396626c0bac013c49019daf182336df8c7513d24918ba scripts/fsgg-coord-report 1f2a70607268d549aa59c56e612e654e52021e9b7015e26c8fdaa97a4a3a869e scripts/lib/args.sh fbdedf5e5bb4f85ef048807a430218001f489172210f87acd9bf8b3fbbca0330 scripts/lib/roots.sh diff --git a/scripts/fsgg-coord b/scripts/fsgg-coord index 53059be61..a6901902e 100755 --- a/scripts/fsgg-coord +++ b/scripts/fsgg-coord @@ -22,8 +22,9 @@ # written here today would simply start rotting tomorrow. If you want the count, run `wc -l`. # # WHAT IT DOES. It resolves ONE engine and execs it, passing argv through unchanged and returning its exit -# code unchanged. It is a transparent pipe — it adds no output of its own on the happy path and holds no -# state. The resolution order is: +# code unchanged. It preserves engine stdout and exit status and holds no state. Tier 4 additionally emits +# one stable JSON resolution receipt on stderr, because its manifest pin and executed assembly can otherwise +# diverge invisibly with caller cwd (#2908). The resolution order is: # # 1. THE EXPLICIT BIN — `FSGG_COORD_ENGINE_BIN`. An instruction. Honoured or REFUSED, never fallen # back from: a caller who named an engine that is not there must be told, not @@ -441,6 +442,13 @@ $restore_out$hint" *) die "engine pin mismatch: manifest=$MANIFEST pin=$TOOL_PACKAGE@$TOOL_PIN resolvedAssembly=$TOOL_ASSEMBLY resolvedVersion=${RESOLVED_VERSION:-unreadable}" ;; esac + # STDERR, never stdout: stdout is the engine's machine contract (`next` is one bare ref, JSON verbs are + # one JSON value). The receipt is itself one deterministic JSON object so paths containing spaces or + # quotes remain unambiguous, and it is emitted only after the exact assembly reports the pinned version. + jq -cn --arg manifest "$MANIFEST" --arg package "$TOOL_PACKAGE" --arg pin "$TOOL_PIN" --arg assembly "$TOOL_ASSEMBLY" \ + '{schema:"fsgg.coord.engine-resolution/v1",tier:4,manifest:$manifest,pin:($package+"@"+$pin),resolvedAssembly:$assembly}' >&2 \ + || die "could not render the tier-4 engine-resolution receipt for manifest=$MANIFEST pin=$TOOL_PACKAGE@$TOOL_PIN resolvedAssembly=$TOOL_ASSEMBLY" + # No `cd`: repository-sensitive engine behaviour still observes the caller's working directory. Direct # assembly execution also keeps argv transparent without `dotnet tool run` consuming -h/--help (#1029). exec dotnet "$TOOL_ASSEMBLY" "$@" diff --git a/tests/coord-engine-parity/shim.sh b/tests/coord-engine-parity/shim.sh index d696392bb..3ca3bb52d 100755 --- a/tests/coord-engine-parity/shim.sh +++ b/tests/coord-engine-parity/shim.sh @@ -1555,20 +1555,25 @@ if [ "${1:-}" = "--version" ]; then printf '%s\n' "${FORCE_RESOLVED_VERSION:-$pin.0}" exit 0 fi -printf 'ENGINE RAN pin=%s assembly=%s cwd=%s args=%s\n' "$pin" "$assembly" "$PWD" "$*" +printf 'ENGINE RAN cwd=%s args=%s\n' "$PWD" "$*" exit 0 SH chmod +x "$DOTNETDIR/dotnet" t4run() { ( cd "$CALLER" && env -u FSGG_COORD_ENGINE_BIN DOTNET_CLI_HOME="$DOTNET_HOME" ARGV_LOG_PATH="$ARGV_LOG" PATH="$DOTNETDIR:/usr/bin:/bin" "$T4_SHIM" "$@" 2>&1 ); } # a. POSITIVE + INVERSION: wrapper-root 0.72 wins over caller-root 0.71, while the engine still runs in -# CALLER. Output names the pin, assembly, cwd and argv, so every half is observable in one execution. +# CALLER. Resolution evidence must come from the copied PRODUCTION wrapper, not from the fake engine: +# the fake deliberately prints only cwd and argv. : >"$ARGV_LOG"; out="$(t4run next --help)"; rc=$? -{ [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -Fq "ENGINE RAN pin=0.72.0 assembly=$DOTNET_HOME/assemblies/0.72.0/fsgg-coord-engine.dll cwd=$CALLER args=next --help"; } \ - && ok ".github#2908: tier 4 binds execution to the WRAPPER manifest's 0.72.0 assembly while preserving caller cwd and argv" \ +resolution="$(jq -Rrc 'fromjson? | select(.schema == "fsgg.coord.engine-resolution/v1")' <<<"$out" 2>/dev/null || true)" +{ [ "$rc" -eq 0 ] \ + && jq -e --arg manifest "$T4/.config/dotnet-tools.json" --arg assembly "$DOTNET_HOME/assemblies/0.72.0/fsgg-coord-engine.dll" \ + '.schema == "fsgg.coord.engine-resolution/v1" and .tier == 4 and .manifest == $manifest and .pin == "fs.gg.coord.cli@0.72.0" and .resolvedAssembly == $assembly' <<<"$resolution" >/dev/null \ + && printf '%s' "$out" | grep -Fq "ENGINE RAN cwd=$CALLER args=next --help"; } \ + && ok ".github#2908: production tier 4 reports wrapper manifest/pin/assembly while the engine preserves caller cwd and argv" \ || bad ".github#2908: wrapper manifest selection and command working context must be separate" "rc=$rc out=$out log=$(cat "$ARGV_LOG")" -grep -q 'pin=0.71.0' <<<"$out" \ +grep -q 'fs.gg.coord.cli@0.71.0' <<<"$out" \ && bad ".github#2908 inversion: caller manifest 0.71.0 silently won" "out=$out" \ || ok ".github#2908 inversion: the conflicting CALLER manifest cannot select its 0.71.0 engine" @@ -1584,7 +1589,7 @@ jq '[.[] | select(.Version != "0.72.0")]' "$RESTORED_CACHE" >"$DOTNET_HOME/.dotn : >"$ARGV_LOG" out="$(RESTORED_CACHE_FIXTURE="$RESTORED_CACHE" t4run budget)"; rc=$? { [ "$rc" -eq 0 ] && grep -Fq "tool restore --tool-manifest $T4/.config/dotnet-tools.json" "$ARGV_LOG" \ - && grep -q 'ENGINE RAN pin=0.72.0' <<<"$out"; } \ + && grep -q '"pin":"fs.gg.coord.cli@0.72.0"' <<<"$out"; } \ && ok ".github#2908: restore is explicitly bound to the wrapper manifest, then that exact pin executes" \ || bad ".github#2908: restore must not rediscover the caller manifest" "rc=$rc out=$out log=$(cat "$ARGV_LOG")" From 9d6a8629a255798edb96a9071d028d88ce9192b8 Mon Sep 17 00:00:00 2001 From: osprey-d90d Date: Mon, 24 Aug 2026 07:00:47 +0200 Subject: [PATCH 3/3] chore(release): prepare coherent set 0.75.2 --- Directory.Build.props | 6 +++++- src/FS.GG.Coord.Cli/FS.GG.Coord.Cli.fsproj | 17 +++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 675ef302c..620f70ac6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -459,8 +459,12 @@ canonical generation-token contract, and makes structured-record completion evidence unambiguous. The engine adds the matching derived-entry and normalization controls. Additive repair; no command, package, or member identity is removed. --> + - 0.75.1 + 0.75.2 diff --git a/src/FS.GG.Coord.Cli/FS.GG.Coord.Cli.fsproj b/src/FS.GG.Coord.Cli/FS.GG.Coord.Cli.fsproj index 4ec666c1a..6ccbf3713 100644 --- a/src/FS.GG.Coord.Cli/FS.GG.Coord.Cli.fsproj +++ b/src/FS.GG.Coord.Cli/FS.GG.Coord.Cli.fsproj @@ -130,14 +130,15 @@ bytes under 0.52.0 would make the same version identify different artifacts. Ado coherent set cut from corrected source. All three partial sets are recorded in `registry/dependencies.yml`. - 0.75.1 — host-owned review waits and unambiguous completion evidence. - -WHAT MOVED, AND WHY IT IS A PATCH. Review queue entry now derives the current claim generation, PR -head, review kind, and round through the engine rather than a caller-authored token. Completion accepts -the required structured review record's canonical URL, comment id, or digest, normalizes it to the URL, -and refuses prose-marker or ambiguous references before append. The packed Kit documents that same -host-owned route while preserving the exact canonical generation-token contract. This is an additive -repair with no removed command, package, member, or identity (.github#2859; parent .github#2903). + 0.75.2 — caller-CWD-independent tier-4 engine resolution. + +WHAT MOVED, AND WHY IT IS A PATCH. The receiver wrapper now resolves tier 4 from its own tool manifest +rather than whichever manifest is discoverable from the caller's current directory. It restores that +exact manifest when needed, verifies the pinned assembly's reported version, and executes the assembly +directly while preserving caller cwd and argv. A successful production execution emits one stable JSON +resolution receipt on stderr with the manifest path, package pin, and resolved assembly. This is an +additive routing and observability repair with no removed command, package, member, or identity +(.github#2908; parent .github#2903). $(FsggStandingAdvisories)