diff --git a/docs/shell-completion.md b/docs/shell-completion.md
index 385e9a04..e02a1635 100644
--- a/docs/shell-completion.md
+++ b/docs/shell-completion.md
@@ -254,3 +254,17 @@ $env.config.completions.external.completer = { |spans| _repl_nu_dispatch_complet
 
 - PowerShell 7+ is recommended. In Windows PowerShell 5.1, native completion registration for external executables is limited and may not trigger reliably.
 - Nushell uses one global `completions.external.completer`; Repl manages a shared dispatcher block to route completions per app command head.
+
+## Relationship with interactive autocomplete
+
+Shell completion and the interactive REPL autocomplete draw option-name candidates from the
+same source, normalize prior tokens through the same parser profile (option values consumed,
+POSIX `--` honored, response files never expanded), and use the same option-prefix gate — a
+single dash already surfaces short aliases such as `-f`, while signed numeric literals
+(`-42`) stay positional. Two deliberate differences remain:
+
+- On an **empty** token after a complete command, shell completion lists option names (a
+  dump-style list is cheap there), while the interactive menu shows parameter placeholders —
+  options appear from the first typed `-`.
+- Shell completion also completes **enum values** for a pending option; the interactive menu
+  does not yet.
diff --git a/src/Repl.Core/Autocomplete/AutocompleteEngine.cs b/src/Repl.Core/Autocomplete/AutocompleteEngine.cs
index 9611499e..150f640b 100644
--- a/src/Repl.Core/Autocomplete/AutocompleteEngine.cs
+++ b/src/Repl.Core/Autocomplete/AutocompleteEngine.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
+using Repl.Internal.Options;
 
 namespace Repl;
 
@@ -84,7 +85,9 @@ internal ConsoleLineReader.AutocompleteColorStyles ResolveAutocompleteColorStyle
 		IServiceProvider serviceProvider,
 		CancellationToken cancellationToken)
 	{
-		var activeGraph = app.ResolveActiveRoutingGraph();
+		// Completion must not poison the durable routing cache: its module-presence view can
+		// differ from execution's (the line's globals are not applied here).
+		var activeGraph = app.ResolveActiveRoutingGraph(useDurableCache: false);
 		var comparer = app.OptionsSnapshot.Interactive.Autocomplete.CaseSensitive
 			? StringComparer.Ordinal
 			: StringComparer.OrdinalIgnoreCase;
@@ -101,6 +104,9 @@ internal ConsoleLineReader.AutocompleteColorStyles ResolveAutocompleteColorStyle
 				matchingRoutes,
 				state.CommandPrefix,
 				state.CurrentTokenPrefix,
+				state.TerminalRoute,
+				state.OptionsTerminated,
+				state.PendingOptionValue,
 				scopeTokens.Count,
 				activeGraph,
 				prefixComparison,
@@ -116,21 +122,13 @@ internal ConsoleLineReader.AutocompleteColorStyles ResolveAutocompleteColorStyle
 				state.CurrentTokenPrefix,
 				app.OptionsSnapshot.Interactive.Autocomplete.LiveHintMaxAlternatives)
 			: null;
-		var discoverableRoutes = app.ResolveDiscoverableRoutes(
-			activeGraph.Routes,
-			activeGraph.Contexts,
-			scopeTokens,
-			prefixComparison);
-		var discoverableContexts = app.ResolveDiscoverableContexts(
-			activeGraph.Contexts,
-			scopeTokens,
-			prefixComparison);
 		var tokenClassifications = BuildTokenClassifications(
 			request.Input,
 			scopeTokens,
 			prefixComparison,
-			discoverableRoutes,
-			discoverableContexts);
+			activeGraph,
+			app.ResolveDiscoverableRoutes(activeGraph.Routes, activeGraph.Contexts, scopeTokens, prefixComparison),
+			app.ResolveDiscoverableContexts(activeGraph.Contexts, scopeTokens, prefixComparison));
 		return new ConsoleLineReader.AutocompleteResult(
 			state.ReplaceStart,
 			state.ReplaceLength,
@@ -146,38 +144,222 @@ private AutocompleteResolutionState ResolveAutocompleteState(
 		ActiveRoutingGraph activeGraph)
 	{
 		var state = AnalyzeAutocompleteInput(request.Input, request.Cursor);
-		var commandPrefix = scopeTokens.Concat(state.PriorTokens).ToArray();
+		var committed = new string[scopeTokens.Count + state.PriorTokens.Length];
+		for (var i = 0; i < scopeTokens.Count; i++)
+		{
+			committed[i] = scopeTokens[i];
+		}
+
+		state.PriorTokens.CopyTo(committed, scopeTokens.Count);
+
+		var resolution = ResolveCommitted(committed, activeGraph);
+		// When the last committed token is a valued option still awaiting its value, the
+		// current token is that VALUE — not a command or another option. Suggesting either
+		// would produce a different parse (e.g. "--tenant install" binds "install" as the
+		// value), so the whole menu is suppressed for this position.
+		if (IsPendingOptionValue(committed, resolution.TerminalRoute, resolution.OptionsTerminated, state.CurrentTokenPrefix))
+		{
+			return new AutocompleteResolutionState(
+				resolution.CommandPrefix,
+				state.CurrentTokenPrefix,
+				state.ReplaceStart,
+				state.ReplaceLength,
+				resolution.TerminalRoute,
+				resolution.OptionsTerminated)
+			{
+				PendingOptionValue = true,
+			};
+		}
+
 		var currentTokenPrefix = state.CurrentTokenPrefix;
-		var replaceStart = state.ReplaceStart;
-		var replaceLength = state.ReplaceLength;
 		if (!ShouldAdvanceToNextToken(
-				commandPrefix,
+				resolution.CommandPrefix,
 				currentTokenPrefix,
-				replaceStart,
-				replaceLength,
+				state.ReplaceStart,
+				state.ReplaceLength,
 				request.Cursor,
 				comparison,
 				activeGraph.Routes,
 				activeGraph.Contexts))
 		{
 			return new AutocompleteResolutionState(
-				commandPrefix,
+				resolution.CommandPrefix,
 				currentTokenPrefix,
-				replaceStart,
-				replaceLength);
+				state.ReplaceStart,
+				state.ReplaceLength,
+				resolution.TerminalRoute,
+				resolution.OptionsTerminated);
 		}
 
+		// The current token is complete (whitespace follows): commit it and re-resolve so the
+		// route, option boundary, and terminated state describe the advanced prefix.
+		var advanced = ResolveCommitted([.. committed, currentTokenPrefix], activeGraph);
 		return new AutocompleteResolutionState(
-			commandPrefix.Concat([currentTokenPrefix]).ToArray(),
+			advanced.CommandPrefix,
 			string.Empty,
 			request.Cursor,
-			0);
+			0,
+			advanced.TerminalRoute,
+			advanced.OptionsTerminated);
+	}
+
+	// Single source of truth for a completion pass: resolves the committed tokens the way
+	// execution does (global options stripped by the arity-aware parser, then RouteResolver
+	// on the remaining tokens — route resolution runs BEFORE option parsing, so dash tokens
+	// and the bare "--" bind to segments as positionals). Everything downstream — the command
+	// prefix, route-option sourcing, provider gating, token classification, and the
+	// end-of-options state — is derived from this one result against the one captured graph,
+	// so the surfaces cannot drift from each other or from the executor.
+	private CommittedResolution ResolveCommitted(string[] committedTokens, ActiveRoutingGraph activeGraph)
+	{
+		if (committedTokens.Length == 0)
+		{
+			return new CommittedResolution([], TerminalRoute: null, OptionsTerminated: false);
+		}
+
+		var stripped = GlobalOptionParser
+			.Parse(committedTokens, app.OptionsSnapshot.Output, app.OptionsSnapshot.Parsing)
+			.RemainingTokens;
+		var expanded = ExpandUniquePrefixes(stripped as string[] ?? [.. stripped], activeGraph);
+		if (app.Resolve(expanded, activeGraph.Routes) is { } match)
+		{
+			var segmentCount = Math.Min(match.Route.Template.Segments.Count, expanded.Length);
+			// The end-of-options separator only counts when it lands among the route's
+			// trailing option tokens; a "--" consumed as a positional segment value does not
+			// terminate options.
+			var optionsTerminated = false;
+			foreach (var trailing in match.RemainingTokens)
+			{
+				if (string.Equals(trailing, "--", StringComparison.Ordinal))
+				{
+					optionsTerminated = true;
+					break;
+				}
+			}
+
+			return new CommittedResolution(expanded[..segmentCount], match, optionsTerminated);
+		}
+
+		// No terminal route yet (still typing command words, or a positional is unfilled):
+		// keep the tokens as typed so a dash-prefixed positional still satisfies its segment.
+		return new CommittedResolution(expanded, TerminalRoute: null, OptionsTerminated: false);
+	}
+
+	private readonly record struct CommittedResolution(
+		string[] CommandPrefix,
+		RouteMatch? TerminalRoute,
+		bool OptionsTerminated);
+
+	// True when the last committed token is a valued option still awaiting its value, so the
+	// current token is that value. Shared by the interactive engine and shell completion.
+	internal bool IsPendingOptionValue(
+		string[] committedTokens,
+		RouteMatch? terminalRoute,
+		bool optionsTerminated,
+		string currentTokenPrefix)
+	{
+		if (committedTokens.Length == 0)
+		{
+			return false;
+		}
+
+		var last = committedTokens[^1];
+		if (!IsOptionPrefixToken(last))
+		{
+			return false;
+		}
+
+		// A valued ROUTE option in the terminal route's trailing region is pending regardless
+		// of the current token — the route parser will not consume a dash-prefixed token as
+		// its value, so "run --channel --force" would leave --channel empty. But only BEFORE a
+		// POSIX "--": after the separator the token is positional, not a route option.
+		if (!optionsTerminated && IsPendingRouteOptionValue(last, terminalRoute))
+		{
+			return true;
+		}
+
+		// Global-scoped valued options (custom globals and built-in --result: suboptions) take
+		// a SEPARATE value only when the next token is not option-like — the global parser skips
+		// a dash-prefixed follower (so "--tenant --" is not pending; "--tenant acme" is).
+		return !IsOptionPrefixToken(currentTokenPrefix) && IsPendingGlobalOptionValue(last);
+	}
+
+	private bool IsPendingRouteOptionValue(string last, RouteMatch? terminalRoute) =>
+		!last.Contains('=', StringComparison.Ordinal)
+		&& terminalRoute is { } match
+		&& match.RemainingTokens.Count > 0
+		&& string.Equals(match.RemainingTokens[^1], last, StringComparison.Ordinal)
+		&& match.Route.OptionSchema
+			.ResolveToken(last, app.OptionsSnapshot.Parsing.OptionCaseSensitivity)
+			.Any(static entry => entry.TokenKind == OptionSchemaTokenKind.NamedOption);
+
+	private bool IsPendingGlobalOptionValue(string last)
+	{
+		if (IsPendingResultFlowOption(last))
+		{
+			return true;
+		}
+
+		if (last.Contains('=', StringComparison.Ordinal) || last.Contains(':', StringComparison.Ordinal))
+		{
+			return false;
+		}
+
+		// Resolve through the parser's own token map so a colliding alias picks the SAME
+		// (last-registered) definition the parser would — scanning definitions independently
+		// could pick a shadowed one with a different arity.
+		return GlobalOptionParser.TryResolveCustomGlobalDefinition(last, app.OptionsSnapshot.Parsing, out var definition)
+			&& definition.ValueType != typeof(bool);
+	}
+
+	// The built-in --result: suboptions that take a separate value (page-size/cursor/pager);
+	// --result:all is a flag. An inline value (--result:page-size=8) is already satisfied.
+	private static bool IsPendingResultFlowOption(string token) =>
+		(string.Equals(token, ReplResultFlowOptionNames.PageSize, StringComparison.Ordinal)
+			|| string.Equals(token, ReplResultFlowOptionNames.Cursor, StringComparison.Ordinal)
+			|| string.Equals(token, ReplResultFlowOptionNames.Pager, StringComparison.Ordinal))
+		&& !token.Contains('=', StringComparison.Ordinal);
+
+	// Unique-prefix/alias expansion, bounded by the deepest template: beyond that depth no
+	// literal can match, and the per-index candidate derivation must not scale with the
+	// token count of a remote-controlled line (hosted sessions autocomplete per keystroke).
+	private string[] ExpandUniquePrefixes(string[] tokens, ActiveRoutingGraph activeGraph)
+	{
+		if (tokens.Length == 0)
+		{
+			return tokens;
+		}
+
+		var expansionDepth = 0;
+		foreach (var route in activeGraph.Routes)
+		{
+			expansionDepth = Math.Max(expansionDepth, route.Template.Segments.Count);
+		}
+
+		foreach (var context in activeGraph.Contexts)
+		{
+			expansionDepth = Math.Max(expansionDepth, context.Template.Segments.Count);
+		}
+
+		if (tokens.Length <= expansionDepth)
+		{
+			var resolution = app.ResolveUniquePrefixes(tokens, activeGraph);
+			return resolution.IsAmbiguous ? tokens : resolution.Tokens;
+		}
+
+		var headResolution = app.ResolveUniquePrefixes(tokens[..expansionDepth], activeGraph);
+		return headResolution.IsAmbiguous
+			? tokens
+			: [.. headResolution.Tokens, .. tokens[expansionDepth..]];
 	}
 
 	private async ValueTask<ConsoleLineReader.AutocompleteSuggestion[]> CollectAutocompleteSuggestionsAsync(
 		IReadOnlyList<RouteDefinition> matchingRoutes,
 		string[] commandPrefix,
 		string currentTokenPrefix,
+		RouteMatch? terminalRoute,
+		bool optionsTerminated,
+		bool pendingOptionValue,
 		int scopeTokenCount,
 		ActiveRoutingGraph activeGraph,
 		StringComparison prefixComparison,
@@ -185,20 +367,40 @@ private AutocompleteResolutionState ResolveAutocompleteState(
 		IServiceProvider serviceProvider,
 		CancellationToken cancellationToken)
 	{
-		var commandCandidates = CollectRouteAutocompleteCandidates(
-			matchingRoutes,
-			commandPrefix,
-			currentTokenPrefix,
-			prefixComparison);
 		var dynamicCandidates = await CollectDynamicAutocompleteCandidatesAsync(
 				matchingRoutes,
 				commandPrefix,
 				currentTokenPrefix,
+				optionsTerminated,
 				prefixComparison,
 				app.OptionsSnapshot.Parsing,
 				serviceProvider,
 				cancellationToken)
 			.ConfigureAwait(false);
+
+		// A valued option awaiting its value: the current token is that value. Keep the
+		// pending option's value provider (WithCompletion / enum values), but suppress
+		// command names and option names — offering either would misparse.
+		if (pendingOptionValue)
+		{
+			return DeduplicateSuggestions(dynamicCandidates, comparer);
+		}
+
+		// Once a terminal route has trailing option tokens, the command path is complete — no
+		// subcommand can follow (a later literal can't match past an option-occupied position,
+		// and a bool flag's zero-or-one arity would otherwise swallow the suggested word).
+		var commandCandidates = terminalRoute is { RemainingTokens.Count: > 0 }
+			? []
+			: CollectRouteAutocompleteCandidates(
+				matchingRoutes,
+				commandPrefix,
+				currentTokenPrefix,
+				prefixComparison);
+		var optionCandidates = CollectOptionAutocompleteCandidates(
+			commandPrefix,
+			currentTokenPrefix,
+			terminalRoute,
+			optionsTerminated);
 		var contextCandidates = app.OptionsSnapshot.Interactive.Autocomplete.ShowContextAlternatives
 			? CollectContextAutocompleteCandidates(commandPrefix, currentTokenPrefix, prefixComparison, activeGraph.Contexts)
 			: [];
@@ -216,10 +418,20 @@ private AutocompleteResolutionState ResolveAutocompleteState(
 		var candidates = DeduplicateSuggestions(
 			commandCandidates
 				.Concat(dynamicCandidates)
+				.Concat(optionCandidates)
 				.Concat(contextCandidates)
 				.Concat(ambientCandidates)
 				.Concat(ambientContinuationCandidates),
 			comparer);
+		return AppendInvalidAlternativeIfNeeded(candidates, currentTokenPrefix);
+	}
+
+	// When nothing selectable matched and invalid alternatives are shown, append the current
+	// token as an explicit "(invalid)" entry so the user sees why the menu is empty.
+	private ConsoleLineReader.AutocompleteSuggestion[] AppendInvalidAlternativeIfNeeded(
+		ConsoleLineReader.AutocompleteSuggestion[] candidates,
+		string currentTokenPrefix)
+	{
 		if (!app.OptionsSnapshot.Interactive.Autocomplete.ShowInvalidAlternatives
 			|| string.IsNullOrWhiteSpace(currentTokenPrefix)
 			|| candidates.Any(static candidate => candidate.IsSelectable))
@@ -308,6 +520,56 @@ private AutocompleteResolutionState ResolveAutocompleteState(
 		return suggestions;
 	}
 
+
+	private List<ConsoleLineReader.AutocompleteSuggestion> CollectOptionAutocompleteCandidates(
+		string[] commandPrefix,
+		string currentTokenPrefix,
+		RouteMatch? terminalRoute,
+		bool optionsTerminated)
+	{
+		if (optionsTerminated || !IsOptionPrefixToken(currentTokenPrefix))
+		{
+			return [];
+		}
+
+		var comparison = ResolveOptionStringComparison();
+		var comparer = StringComparer.FromComparison(comparison);
+		var tokens = new List<string>();
+		var dedupe = new HashSet<string>(comparer);
+		OptionTokenCompletionSource.CollectGlobalOptionTokens(
+			app.OptionsSnapshot, currentTokenPrefix, comparison, dedupe, tokens);
+
+		// Source route options from the single route this prefix resolves to (already
+		// computed for the whole pass), and only when EVERY positional segment — required or
+		// optional — is present: an unfilled trailing segment would capture the accepted
+		// option token as its value before option parsing runs, so the completed command
+		// would not behave as the menu implies.
+		if (terminalRoute is { } match
+			&& commandPrefix.Length == match.Route.Template.Segments.Count)
+		{
+			OptionTokenCompletionSource.CollectRouteOptionTokens(
+				match.Route,
+				currentTokenPrefix,
+				app.OptionsSnapshot.Parsing.OptionCaseSensitivity,
+				dedupe,
+				tokens);
+		}
+
+		tokens.Sort(comparer);
+		var suggestions = new List<ConsoleLineReader.AutocompleteSuggestion>(tokens.Count);
+		foreach (var token in tokens)
+		{
+			suggestions.Add(new ConsoleLineReader.AutocompleteSuggestion(
+				token,
+				Kind: ConsoleLineReader.AutocompleteSuggestionKind.Parameter));
+		}
+
+		return suggestions;
+	}
+
+	private StringComparison ResolveOptionStringComparison() =>
+		app.OptionsSnapshot.Parsing.OptionCaseSensitivity.ToStringComparison();
+
 	private List<ConsoleLineReader.AutocompleteSuggestion> CollectAmbientContinuationAutocompleteCandidates(
 		string[] commandPrefix,
 		string currentTokenPrefix,
@@ -445,7 +707,7 @@ private bool ShouldAdvanceToNextToken(
 				continue;
 			}
 
-			if (!MatchesRoutePrefix(route.Template, commandPrefix, comparison, app.OptionsSnapshot.Parsing))
+			if (!MatchesRoutePrefix(route, commandPrefix, comparison, app.OptionsSnapshot.Parsing))
 			{
 				continue;
 			}
@@ -635,7 +897,7 @@ internal List<RouteDefinition> CollectVisibleMatchingRoutes(
 			.Where(route =>
 				!route.Command.IsHidden
 				&& !app.IsRouteSuppressedForDiscovery(route.Template, contexts, commandPrefix, prefixComparison)
-				&& MatchesRoutePrefix(route.Template, commandPrefix, prefixComparison, app.OptionsSnapshot.Parsing))
+				&& MatchesRoutePrefix(route, commandPrefix, prefixComparison, app.OptionsSnapshot.Parsing))
 			.ToList();
 		if (commandPrefix.Length == 0)
 		{
@@ -677,11 +939,6 @@ private static bool MatchesLiteralPrefix(
 		string currentTokenPrefix,
 		int maxAlternatives)
 	{
-		if (IsGlobalOptionToken(currentTokenPrefix))
-		{
-			return null;
-		}
-
 		var selectable = suggestions.Where(static suggestion => suggestion.IsSelectable).ToArray();
 		var hintAlternatives = suggestions
 			.Where(static suggestion =>
@@ -698,8 +955,12 @@ private static bool MatchesLiteralPrefix(
 				?? (string.IsNullOrWhiteSpace(currentTokenPrefix) ? null : $"Invalid: {currentTokenPrefix}");
 		}
 
+		// Option suggestions share AutocompleteSuggestionKind.Parameter, so without this
+		// guard the parameter shortcut would hint the pending positional ("Param skillName")
+		// while the menu is listing option names — the hint must describe what is shown.
 		var segmentIndex = commandPrefix.Length;
-		if (TryBuildParameterHint(matchingRoutes, segmentIndex, out var parameterHint)
+		if (!IsOptionPrefixToken(currentTokenPrefix)
+			&& TryBuildParameterHint(matchingRoutes, segmentIndex, out var parameterHint)
 			&& selectable.All(static suggestion =>
 				suggestion.Kind is ConsoleLineReader.AutocompleteSuggestionKind.Parameter
 					or ConsoleLineReader.AutocompleteSuggestionKind.Invalid))
@@ -809,15 +1070,25 @@ private static bool TryBuildParameterHint(
 		IReadOnlyList<RouteDefinition> matchingRoutes,
 		string[] commandPrefix,
 		string currentTokenPrefix,
+		bool optionsTerminated,
 		StringComparison prefixComparison,
 		ParsingOptions parsingOptions,
 		IServiceProvider serviceProvider,
 		CancellationToken cancellationToken)
 	{
+		// Providers complete parameter VALUES; an option-prefix token is asking for option
+		// names, and provider output would only pollute that menu. After the POSIX "--"
+		// separator a dash-prefixed token is a positional argument again, so the provider
+		// must run for it.
+		if (!optionsTerminated && IsOptionPrefixToken(currentTokenPrefix))
+		{
+			return [];
+		}
+
 		var exactRoute = matchingRoutes.FirstOrDefault(route =>
 			route.Template.Segments.Count == commandPrefix.Length
-			&& MatchesTemplatePrefix(
-				route.Template,
+			&& MatchesRoutePrefix(
+				route,
 				commandPrefix,
 				prefixComparison,
 				parsingOptions));
@@ -843,10 +1114,22 @@ private static ConsoleLineReader.AutocompleteSuggestion[] DeduplicateSuggestions
 		StringComparer comparer)
 	{
 		var seen = new HashSet<string>(comparer);
+		// Option tokens dedupe case-sensitively so that, under case-sensitive option parsing,
+		// distinct executable aliases like "-m" and "-M" both survive the UI-level dedupe
+		// (which otherwise uses the case-insensitive autocomplete comparer).
+		var seenOptions = new HashSet<string>(StringComparer.Ordinal);
 		var distinct = new List<ConsoleLineReader.AutocompleteSuggestion>();
 		foreach (var suggestion in suggestions)
 		{
-			if (string.IsNullOrWhiteSpace(suggestion.DisplayText) || !seen.Add(suggestion.DisplayText))
+			if (string.IsNullOrWhiteSpace(suggestion.DisplayText))
+			{
+				continue;
+			}
+
+			var isOptionToken = IsOptionPrefixToken(suggestion.DisplayText);
+			if (isOptionToken
+				? !seenOptions.Add(suggestion.DisplayText)
+				: !seen.Add(suggestion.DisplayText))
 			{
 				continue;
 			}
@@ -861,6 +1144,7 @@ private static ConsoleLineReader.AutocompleteSuggestion[] DeduplicateSuggestions
 		string input,
 		IReadOnlyList<string> scopeTokens,
 		StringComparison comparison,
+		ActiveRoutingGraph activeGraph,
 		IReadOnlyList<RouteDefinition> routes,
 		IReadOnlyList<ContextDefinition> contexts)
 	{
@@ -875,41 +1159,130 @@ private static ConsoleLineReader.AutocompleteSuggestion[] DeduplicateSuggestions
 			return [];
 		}
 
+		var rawValues = new string[tokenSpans.Count];
+		for (var i = 0; i < tokenSpans.Count; i++)
+		{
+			rawValues[i] = tokenSpans[i].Value;
+		}
+
+		// Global-option tokens colour as options regardless of the route region (a leading
+		// "--no-logo" is executable before the command).
+		var isGlobal = MarkGlobalOptionTokens(rawValues);
+
+		// Resolve the positional path ONCE (not per token): route resolution gives the segment
+		// boundary that separates positionals from the trailing option region. Everything below
+		// derives from this single result, so classification cannot drift from completion and
+		// the per-keystroke cost stays flat instead of scaling with the token count.
+		var positional = new List<string>(scopeTokens.Count + tokenSpans.Count);
+		positional.AddRange(scopeTokens);
+		for (var i = 0; i < tokenSpans.Count; i++)
+		{
+			if (!isGlobal[i])
+			{
+				positional.Add(rawValues[i]);
+			}
+		}
+
+		var resolution = ResolveCommitted([.. positional], activeGraph);
+		return EmitTokenClassifications(
+			tokenSpans, rawValues, isGlobal, scopeTokens, comparison, routes, contexts, resolution);
+	}
+
+	private List<ConsoleLineReader.TokenClassification> EmitTokenClassifications(
+		List<TokenSpan> tokenSpans,
+		string[] rawValues,
+		bool[] isGlobal,
+		IReadOnlyList<string> scopeTokens,
+		StringComparison comparison,
+		IReadOnlyList<RouteDefinition> routes,
+		IReadOnlyList<ContextDefinition> contexts,
+		CommittedResolution resolution)
+	{
+		var segmentBoundary = resolution.CommandPrefix.Length;
 		var output = new List<ConsoleLineReader.TokenClassification>(tokenSpans.Count);
+		var runningPrefix = new List<string>(scopeTokens);
+		var positionalIndex = scopeTokens.Count;
+		// The end-of-options separator is per-position, not a whole-input flag: options BEFORE
+		// a "--" remain options; only tokens after it leave the option region. (Using the
+		// resolution's whole-input OptionsTerminated here would retroactively invalidate a
+		// valid earlier "--force".)
+		var separatorSeen = false;
 		for (var i = 0; i < tokenSpans.Count; i++)
 		{
-			if (IsGlobalOptionToken(tokenSpans[i].Value))
+			var start = tokenSpans[i].Start;
+			var length = tokenSpans[i].End - start;
+			if (isGlobal[i])
 			{
 				output.Add(new ConsoleLineReader.TokenClassification(
-					tokenSpans[i].Start,
-					tokenSpans[i].End - tokenSpans[i].Start,
-					ConsoleLineReader.AutocompleteSuggestionKind.Parameter));
+					start, length, ConsoleLineReader.AutocompleteSuggestionKind.Parameter));
 				continue;
 			}
 
-			var prefix = scopeTokens.Concat(
-				tokenSpans.Take(i)
-					.Where(static token => !IsGlobalOptionToken(token.Value))
-					.Select(static token => token.Value)).ToArray();
-			var kind = ClassifyToken(
-				prefix,
-				tokenSpans[i].Value,
-				comparison,
-				routes,
-				contexts,
-				scopeTokenCount: scopeTokens.Count,
-				isFirstInputToken: i == 0);
-			output.Add(new ConsoleLineReader.TokenClassification(
-				tokenSpans[i].Start,
-				tokenSpans[i].End - tokenSpans[i].Start,
-				kind));
+			var value = rawValues[i];
+			var isTrailing = resolution.TerminalRoute is not null && positionalIndex >= segmentBoundary;
+
+			// Past the matched segments a dash token is a route option, not a positional.
+			// (A dash token still within the segments falls through to ClassifyToken and is
+			// classified as the positional it binds to.)
+			var kind = isTrailing && !separatorSeen && IsOptionPrefixToken(value)
+				? ConsoleLineReader.AutocompleteSuggestionKind.Parameter
+				: ClassifyToken(
+					[.. runningPrefix],
+					value,
+					comparison,
+					routes,
+					contexts,
+					scopeTokenCount: scopeTokens.Count,
+					isFirstInputToken: positionalIndex == scopeTokens.Count);
+			output.Add(new ConsoleLineReader.TokenClassification(start, length, kind));
+
+			// A bare "--" in the trailing option region is the separator; tokens after it are
+			// positional again.
+			if (isTrailing && string.Equals(value, "--", StringComparison.Ordinal))
+			{
+				separatorSeen = true;
+			}
+
+			runningPrefix.Add(value);
+			positionalIndex++;
 		}
 
 		return output;
 	}
 
+	// Marks which raw tokens the global-option parser consumes (a global flag or its value),
+	// so classification treats them as options and keeps them out of the positional command
+	// path. Uses the parser's exact surviving INDICES — not string-value matching, which
+	// cannot tell which occurrence of a duplicate value ("--tenant show show") was consumed.
+	private bool[] MarkGlobalOptionTokens(string[] rawValues)
+	{
+		var survivingIndices = GlobalOptionParser
+			.Parse(rawValues, app.OptionsSnapshot.Output, app.OptionsSnapshot.Parsing)
+			.RemainingTokenIndices;
+		var isGlobal = new bool[rawValues.Length];
+		Array.Fill(isGlobal, value: true);
+		foreach (var index in survivingIndices)
+		{
+			isGlobal[index] = false;
+		}
+
+		return isGlobal;
+	}
+
 	internal static bool IsGlobalOptionToken(string token) =>
-		token.StartsWith("--", StringComparison.Ordinal) && token.Length >= 2;
+		token.StartsWith("--", StringComparison.Ordinal);
+
+	/// <summary>
+	/// True when <paramref name="token"/> reads as the beginning of an option for
+	/// SUGGESTION purposes — distinct from <see cref="IsGlobalOptionToken"/> (the parser's
+	/// classifier for double-dash options): a single dash already announces an option
+	/// (short aliases like <c>-f</c>), but a signed numeric literal (<c>-42</c>) is a
+	/// positional argument, mirroring the invocation parser's rule.
+	/// </summary>
+	internal static bool IsOptionPrefixToken(string token) =>
+		token.Length > 0
+		&& token[0] == '-'
+		&& !InvocationOptionParser.IsSignedNumericLiteral(token);
 
 	[SuppressMessage(
 		"Maintainability",
@@ -1087,15 +1460,47 @@ private static bool TryClassifyTemplateSegment(
 		return false;
 	}
 
+	// Alias-aware variant for ROUTE prefixes: the router accepts command aliases on the
+	// terminal literal segment (RouteResolver.IsLiteralMatch), so autocomplete must too —
+	// otherwise an alias invocation loses its route's option suggestions.
 	private static bool MatchesRoutePrefix(
-		RouteTemplate template,
+		RouteDefinition route,
 		string[] prefixTokens,
 		StringComparison comparison,
 		ParsingOptions parsingOptions)
 	{
-		return MatchesTemplatePrefix(template, prefixTokens, comparison, parsingOptions);
+		var segments = route.Template.Segments;
+		if (prefixTokens.Length > segments.Count)
+		{
+			return false;
+		}
+
+		for (var i = 0; i < prefixTokens.Length; i++)
+		{
+			var token = prefixTokens[i];
+			switch (segments[i])
+			{
+				case LiteralRouteSegment literal
+					when !IsRouteLiteralMatch(route, literal, token, i, comparison):
+					return false;
+				case DynamicRouteSegment dynamic
+					when !RouteConstraintEvaluator.IsMatch(dynamic, token, parsingOptions):
+					return false;
+			}
+		}
+
+		return true;
 	}
 
+	// Delegates to the router's own literal/alias rule so the two can never drift.
+	private static bool IsRouteLiteralMatch(
+		RouteDefinition route,
+		LiteralRouteSegment literal,
+		string token,
+		int segmentIndex,
+		StringComparison comparison) =>
+		RouteResolver.IsLiteralMatch(route, literal, token, segmentIndex, route.Template.Segments.Count, comparison);
+
 	private static bool MatchesContextPrefix(
 		RouteTemplate template,
 		string[] prefixTokens,
@@ -1205,9 +1610,7 @@ private static AutocompleteInputState AnalyzeAutocompleteInput(string input, int
 			}
 
 			var prefix = input[token.Start..cursor];
-			var prior = tokens.Take(i)
-				.Where(static tokenSpan => !IsGlobalOptionToken(tokenSpan.Value))
-				.Select(static tokenSpan => tokenSpan.Value).ToArray();
+			var prior = CollectPriorTokenValues(tokens, i);
 			return new AutocompleteInputState(
 				prior,
 				prefix,
@@ -1215,9 +1618,16 @@ private static AutocompleteInputState AnalyzeAutocompleteInput(string input, int
 				token.End - token.Start);
 		}
 
-		var trailingPrior = tokens
-			.Where(token => token.End <= cursor && !IsGlobalOptionToken(token.Value))
-			.Select(static token => token.Value).ToArray();
+		var trailingCount = 0;
+		for (var i = 0; i < tokens.Count; i++)
+		{
+			if (tokens[i].End <= cursor)
+			{
+				trailingCount++;
+			}
+		}
+
+		var trailingPrior = CollectPriorTokenValues(tokens, trailingCount);
 		return new AutocompleteInputState(
 			trailingPrior,
 			CurrentTokenPrefix: string.Empty,
@@ -1225,6 +1635,25 @@ private static AutocompleteInputState AnalyzeAutocompleteInput(string input, int
 			ReplaceLength: 0);
 	}
 
+	// Collects the raw token VALUES before the cursor. Normalization into a command prefix
+	// (and the end-of-options state) is deferred to ResolveCommitted, which resolves the
+	// route rather than guessing option arities or pre-classifying dash tokens.
+	private static string[] CollectPriorTokenValues(List<TokenSpan> tokens, int count)
+	{
+		if (count == 0)
+		{
+			return [];
+		}
+
+		var values = new string[count];
+		for (var i = 0; i < count; i++)
+		{
+			values[i] = tokens[i].Value;
+		}
+
+		return values;
+	}
+
 	private readonly record struct AutocompleteInputState(
 		string[] PriorTokens,
 		string CurrentTokenPrefix,
@@ -1235,7 +1664,14 @@ private readonly record struct AutocompleteResolutionState(
 		string[] CommandPrefix,
 		string CurrentTokenPrefix,
 		int ReplaceStart,
-		int ReplaceLength);
+		int ReplaceLength,
+		RouteMatch? TerminalRoute,
+		bool OptionsTerminated)
+	{
+		// True when the current token is the value of a valued option still awaiting one:
+		// no command or option-name suggestions may be offered for this position.
+		public bool PendingOptionValue { get; init; }
+	}
 
 	internal readonly record struct TokenSpan(string Value, int Start, int End);
 }
diff --git a/src/Repl.Core/CoreReplApp.cs b/src/Repl.Core/CoreReplApp.cs
index 8542474d..178627c0 100644
--- a/src/Repl.Core/CoreReplApp.cs
+++ b/src/Repl.Core/CoreReplApp.cs
@@ -432,14 +432,24 @@ private ReplRuntimeChannel ResolveCurrentRuntimeChannel()
 			: ReplRuntimeChannel.Cli;
 	}
 
-	internal ActiveRoutingGraph ResolveActiveRoutingGraph()
+	internal ActiveRoutingGraph ResolveActiveRoutingGraph() => ResolveActiveRoutingGraph(useDurableCache: true);
+
+	// Module presence can depend on mutable global state (a global option like --env gating a
+	// module) that is NOT part of the cache key. Execution applies the invocation's globals
+	// before resolving, so its graph is authoritative and may be cached. Completion evaluates
+	// the graph at a different moment (the line's globals are not applied), so it must NOT
+	// write its provisional graph into the durable cache — otherwise a committed, valid command
+	// could later read a completion-time module-absent graph and fail as "Unknown command".
+	// Completion passes useDurableCache: false: it neither reads nor writes the shared cache,
+	// computing a fresh graph scoped to this pass.
+	internal ActiveRoutingGraph ResolveActiveRoutingGraph(bool useDurableCache)
 	{
 		var runtime = _runtimeState.Value;
 		var serviceProvider = runtime?.ServiceProvider ?? _services;
 		var channel = ResolveCurrentRuntimeChannel();
 		var cacheVersion = Interlocked.Read(ref _routingCacheVersion);
 		var cacheBucket = _routingCacheByServiceProvider.GetOrCreateValue(serviceProvider);
-		if (cacheBucket.TryGet(channel, cacheVersion, out var cached))
+		if (useDurableCache && cacheBucket.TryGet(channel, cacheVersion, out var cached))
 		{
 			return cached;
 		}
@@ -449,7 +459,11 @@ internal ActiveRoutingGraph ResolveActiveRoutingGraph()
 		var routes = ResolveActiveRoutes(activeModuleIds);
 		var contexts = ResolveActiveContexts(activeModuleIds);
 		var computed = new ActiveRoutingGraph(routes, contexts, channel);
-		cacheBucket.Set(channel, cacheVersion, computed);
+		if (useDurableCache)
+		{
+			cacheBucket.Set(channel, cacheVersion, computed);
+		}
+
 		return computed;
 	}
 
diff --git a/src/Repl.Core/Internal/Options/OptionSchema.cs b/src/Repl.Core/Internal/Options/OptionSchema.cs
index e1a56ad8..060410c9 100644
--- a/src/Repl.Core/Internal/Options/OptionSchema.cs
+++ b/src/Repl.Core/Internal/Options/OptionSchema.cs
@@ -17,8 +17,13 @@ public OptionSchema(
 
 	public IReadOnlyDictionary<string, OptionSchemaParameter> Parameters { get; }
 
+	// Lazily materialized once: Entries is immutable and completion paths read this per
+	// keystroke — recomputing the Distinct projection on every access was pure waste.
+	// Benign race: concurrent first reads compute the same array.
+	private string[]? _knownTokens;
+
 	public IReadOnlyCollection<string> KnownTokens =>
-		Entries.Select(entry => entry.Token).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+		_knownTokens ??= [.. Entries.Select(entry => entry.Token).Distinct(StringComparer.OrdinalIgnoreCase)];
 
 	public IReadOnlyList<OptionSchemaEntry> ResolveToken(string token, ReplCaseSensitivity globalCaseSensitivity)
 	{
diff --git a/src/Repl.Core/Internal/Options/OptionTokenCompletionSource.cs b/src/Repl.Core/Internal/Options/OptionTokenCompletionSource.cs
new file mode 100644
index 00000000..501b88ce
--- /dev/null
+++ b/src/Repl.Core/Internal/Options/OptionTokenCompletionSource.cs
@@ -0,0 +1,158 @@
+namespace Repl.Internal.Options;
+
+/// <summary>
+/// Single source of option-name completion candidates, shared by the interactive
+/// autocomplete engine and the shell-completion engine so the two surfaces can never
+/// drift: a global option or output format added here reaches both at once.
+/// </summary>
+internal static class OptionTokenCompletionSource
+{
+	/// <summary>
+	/// Parser profile for normalizing completion-time tokens. Response files must NEVER be
+	/// expanded on a completion path: interactive autocomplete runs per keystroke on lines
+	/// that are remote-controlled in hosted sessions, so a '@file' token would mean
+	/// server-side file reads (UNC probes included) driven by keystrokes — the execution
+	/// pipeline gates expansion on !isInteractiveSession for exactly that reason. Shared by
+	/// both completion engines; never mutated.
+	/// </summary>
+	internal static readonly ParsingOptions CompletionParsingOptions = new()
+	{
+		AllowUnknownOptions = true,
+		AllowResponseFiles = false,
+	};
+
+	/// <summary>Framework-level options that exist regardless of app configuration.</summary>
+	internal static readonly string[] StaticGlobalOptionTokens =
+	[
+		"--help",
+		"--interactive",
+		"--no-interactive",
+		"--no-logo",
+		"--output:",
+		ReplResultFlowOptionNames.All,
+		ReplResultFlowOptionNames.PageSize,
+		ReplResultFlowOptionNames.Cursor,
+		ReplResultFlowOptionNames.Pager,
+	];
+
+	/// <summary>
+	/// Collects the global option tokens matching <paramref name="currentTokenPrefix"/>:
+	/// static framework options, output-format aliases, <c>--output:&lt;format&gt;</c>
+	/// selectors, and custom global options with their aliases.
+	/// </summary>
+	internal static void CollectGlobalOptionTokens(
+		ReplOptions options,
+		string currentTokenPrefix,
+		StringComparison comparison,
+		HashSet<string> dedupe,
+		List<string> results)
+	{
+		foreach (var option in StaticGlobalOptionTokens)
+		{
+			TryAdd(option, currentTokenPrefix, comparison, dedupe, results);
+		}
+
+		// Output-format aliases resolve through OutputOptions.Aliases, a case-insensitive
+		// dictionary, so GlobalOptionParser accepts "--JSON" whatever the global option
+		// case setting — completion must offer them on a differently-cased prefix too.
+		foreach (var alias in options.Output.Aliases.Keys)
+		{
+			TryAddComposed("--", alias, currentTokenPrefix, StringComparison.OrdinalIgnoreCase, dedupe, results);
+		}
+
+		// Transformer names are also looked up through a case-insensitive dictionary, so
+		// GlobalOptionParser accepts "--output:JSON" whatever the global option case setting.
+		foreach (var format in options.Output.Transformers.Keys)
+		{
+			TryAddComposed("--output:", format, currentTokenPrefix, StringComparison.OrdinalIgnoreCase, dedupe, results);
+		}
+
+		foreach (var custom in options.Parsing.GlobalOptions.Values)
+		{
+			TryAdd(custom.CanonicalToken, currentTokenPrefix, comparison, dedupe, results);
+
+			foreach (var alias in custom.Aliases)
+			{
+				TryAdd(alias, currentTokenPrefix, comparison, dedupe, results);
+			}
+		}
+	}
+
+	/// <summary>Collects the route's declared option tokens matching the prefix.</summary>
+	internal static void CollectRouteOptionTokens(
+		RouteDefinition route,
+		string currentTokenPrefix,
+		ReplCaseSensitivity globalCaseSensitivity,
+		HashSet<string> dedupe,
+		List<string> results) =>
+		CollectRouteOptionTokens(route.OptionSchema, currentTokenPrefix, globalCaseSensitivity, dedupe, results);
+
+	// Filters against the schema entries — not the flattened KnownTokens — so each option's
+	// own case sensitivity is honored: an entry declared case-insensitive is offered for a
+	// differently-cased prefix even under a case-sensitive global default (and vice versa),
+	// matching exactly what the invocation parser accepts.
+	internal static void CollectRouteOptionTokens(
+		OptionSchema schema,
+		string currentTokenPrefix,
+		ReplCaseSensitivity globalCaseSensitivity,
+		HashSet<string> dedupe,
+		List<string> results)
+	{
+		foreach (var entry in schema.Entries)
+		{
+			var comparison = (entry.CaseSensitivity ?? globalCaseSensitivity) == ReplCaseSensitivity.CaseInsensitive
+				? StringComparison.OrdinalIgnoreCase
+				: StringComparison.Ordinal;
+			TryAdd(entry.Token, currentTokenPrefix, comparison, dedupe, results);
+		}
+	}
+
+	private static void TryAdd(
+		string token,
+		string currentTokenPrefix,
+		StringComparison comparison,
+		HashSet<string> dedupe,
+		List<string> results)
+	{
+		if (!token.StartsWith(currentTokenPrefix, comparison) || !dedupe.Add(token))
+		{
+			return;
+		}
+
+		results.Add(token);
+	}
+
+	// Composes "head + tail" candidates (e.g. "--" + alias) only when they can match:
+	// the prefix test runs over the two parts so non-matching candidates never allocate
+	// a throwaway string in this per-keystroke path.
+	private static void TryAddComposed(
+		string head,
+		string tail,
+		string currentTokenPrefix,
+		StringComparison comparison,
+		HashSet<string> dedupe,
+		List<string> results)
+	{
+		if (!ComposedStartsWith(head, tail, currentTokenPrefix, comparison))
+		{
+			return;
+		}
+
+		var token = head + tail;
+		if (dedupe.Add(token))
+		{
+			results.Add(token);
+		}
+	}
+
+	private static bool ComposedStartsWith(string head, string tail, string prefix, StringComparison comparison)
+	{
+		if (prefix.Length <= head.Length)
+		{
+			return head.StartsWith(prefix, comparison);
+		}
+
+		return prefix.AsSpan(0, head.Length).Equals(head, comparison)
+			&& tail.AsSpan().StartsWith(prefix.AsSpan(head.Length), comparison);
+	}
+}
diff --git a/src/Repl.Core/Parsing/GlobalInvocationOptions.cs b/src/Repl.Core/Parsing/GlobalInvocationOptions.cs
index b1506740..4baaaba0 100644
--- a/src/Repl.Core/Parsing/GlobalInvocationOptions.cs
+++ b/src/Repl.Core/Parsing/GlobalInvocationOptions.cs
@@ -23,5 +23,10 @@ internal sealed record GlobalInvocationOptions(
 
 	public IReadOnlyList<ParseDiagnostic> Diagnostics { get; init; } = [];
 
+	// The original input index of each surviving token in <see cref="RemainingTokens"/>.
+	// Lets callers identify EXACTLY which input tokens the parser consumed (a global flag or
+	// its value) instead of reconstructing identity from equal string values.
+	public IReadOnlyList<int> RemainingTokenIndices { get; init; } = [];
+
 	public bool HasErrors => Diagnostics.Any(static diagnostic => diagnostic.Severity == ParseDiagnosticSeverity.Error);
 }
diff --git a/src/Repl.Core/Parsing/GlobalOptionParser.cs b/src/Repl.Core/Parsing/GlobalOptionParser.cs
index 4546798a..65743ca3 100644
--- a/src/Repl.Core/Parsing/GlobalOptionParser.cs
+++ b/src/Repl.Core/Parsing/GlobalOptionParser.cs
@@ -21,6 +21,7 @@ public static GlobalInvocationOptions Parse(
 			? StringComparer.OrdinalIgnoreCase
 			: StringComparer.Ordinal;
 		var remaining = new List<string>(args.Count);
+		var remainingIndices = new List<int>(args.Count);
 		var promptAnswers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 		var customGlobalValues = new Dictionary<string, List<string>>(tokenComparer);
 		var diagnostics = new List<ParseDiagnostic>();
@@ -100,6 +101,7 @@ public static GlobalInvocationOptions Parse(
 			}
 
 			remaining.Add(argument);
+			remainingIndices.Add(index);
 		}
 
 		var readonlyCustomGlobalValues = customGlobalValues.ToDictionary(
@@ -111,6 +113,7 @@ public static GlobalInvocationOptions Parse(
 			PromptAnswers = promptAnswers,
 			CustomGlobalNamedOptions = readonlyCustomGlobalValues,
 			Diagnostics = diagnostics,
+			RemainingTokenIndices = remainingIndices,
 		};
 	}
 
@@ -271,6 +274,27 @@ private static int ClampPageSize(int pageSize, int maxPageSize) =>
 	private static void AddResultFlowDiagnostic(List<ParseDiagnostic> diagnostics, string message) =>
 		diagnostics.Add(new ParseDiagnostic(ParseDiagnosticSeverity.Error, message));
 
+	// Resolves a custom-global token to the definition the parser would actually use — the
+	// LAST registered definition wins a token/alias collision (BuildCustomTokenMap overwrites),
+	// so callers must not scan definitions independently and pick a different one.
+	internal static bool TryResolveCustomGlobalDefinition(
+		string token,
+		ParsingOptions parsingOptions,
+		out GlobalOptionDefinition definition)
+	{
+		definition = null!;
+		var comparer = parsingOptions.OptionCaseSensitivity == ReplCaseSensitivity.CaseInsensitive
+			? StringComparer.OrdinalIgnoreCase
+			: StringComparer.Ordinal;
+		var tokenMap = BuildCustomTokenMap(parsingOptions.GlobalOptions, comparer);
+		if (!TryResolveCustomGlobalName(token, tokenMap, out var optionName, out _))
+		{
+			return false;
+		}
+
+		return parsingOptions.GlobalOptions.TryGetValue(optionName, out definition!);
+	}
+
 	private static Dictionary<string, string> BuildCustomTokenMap(
 		IReadOnlyDictionary<string, GlobalOptionDefinition> definitions,
 		StringComparer comparer)
@@ -351,19 +375,8 @@ private static bool TryResolveCustomGlobalName(
 		return true;
 	}
 
-	private static bool IsSignedNumericLiteral(string token)
-	{
-		if (token.Length < 2 || token[0] != '-')
-		{
-			return false;
-		}
-
-		return double.TryParse(
-			token,
-			System.Globalization.NumberStyles.Float,
-			System.Globalization.CultureInfo.InvariantCulture,
-			out _);
-	}
+	private static bool IsSignedNumericLiteral(string token) =>
+		InvocationOptionParser.IsSignedNumericLiteral(token);
 
 	private static bool TrySplitToken(
 		string token,
diff --git a/src/Repl.Core/Parsing/InvocationOptionParser.cs b/src/Repl.Core/Parsing/InvocationOptionParser.cs
index c52b27e5..e4589768 100644
--- a/src/Repl.Core/Parsing/InvocationOptionParser.cs
+++ b/src/Repl.Core/Parsing/InvocationOptionParser.cs
@@ -213,7 +213,9 @@ public static OptionParsingResult Parse(
 	private static bool LooksLikeOptionToken(string token) =>
 		token.Length >= 2 && token[0] == '-';
 
-	private static bool IsSignedNumericLiteral(string token)
+	// Shared with the completion engines: a signed numeric literal (-42) is a positional
+	// argument, never an option token.
+	internal static bool IsSignedNumericLiteral(string token)
 	{
 		if (token.Length < 2 || token[0] != '-')
 		{
diff --git a/src/Repl.Core/ReplCaseSensitivityExtensions.cs b/src/Repl.Core/ReplCaseSensitivityExtensions.cs
new file mode 100644
index 00000000..ede0ae0c
--- /dev/null
+++ b/src/Repl.Core/ReplCaseSensitivityExtensions.cs
@@ -0,0 +1,13 @@
+namespace Repl;
+
+/// <summary>
+/// Shared mapping from <see cref="ReplCaseSensitivity"/> to string comparison primitives —
+/// one place instead of a ternary re-derived at every call site.
+/// </summary>
+internal static class ReplCaseSensitivityExtensions
+{
+	internal static StringComparison ToStringComparison(this ReplCaseSensitivity caseSensitivity) =>
+		caseSensitivity == ReplCaseSensitivity.CaseInsensitive
+			? StringComparison.OrdinalIgnoreCase
+			: StringComparison.Ordinal;
+}
diff --git a/src/Repl.Core/Routing/CommittedInputResolver.cs b/src/Repl.Core/Routing/CommittedInputResolver.cs
index ceabd202..d4e16950 100644
--- a/src/Repl.Core/Routing/CommittedInputResolver.cs
+++ b/src/Repl.Core/Routing/CommittedInputResolver.cs
@@ -2,7 +2,7 @@ namespace Repl;
 
 /// <summary>
 /// Resolves one committed interactive input line against a single
-/// <see cref="CoreReplApp.ResolveActiveRoutingGraph"/> snapshot — prefix expansion, help
+/// <see cref="CoreReplApp.ResolveActiveRoutingGraph()"/> snapshot — prefix expansion, help
 /// scoping, and the route match all use that one snapshot, so the passthrough
 /// classification and the eventual execution can never diverge. Routing-resolution
 /// concern extracted from the interactive loop; ambient classification stays with the
diff --git a/src/Repl.Core/Routing/RouteResolver.cs b/src/Repl.Core/Routing/RouteResolver.cs
index 2bab92a5..a92b5e56 100644
--- a/src/Repl.Core/Routing/RouteResolver.cs
+++ b/src/Repl.Core/Routing/RouteResolver.cs
@@ -142,7 +142,7 @@ private static bool IsBetterMissingArgumentsFailure(
 
 			if (segment is LiteralRouteSegment literal)
 			{
-				if (!IsLiteralMatch(route, literal, token, i, segments.Count))
+				if (!IsLiteralMatch(route, literal, token, i, segments.Count, StringComparison.OrdinalIgnoreCase))
 				{
 					return null;
 				}
@@ -187,7 +187,7 @@ private static bool IsBetterMissingArgumentsFailure(
 
 			if (segment is LiteralRouteSegment literal)
 			{
-				if (!IsLiteralMatch(route, literal, token, i, segments.Count))
+				if (!IsLiteralMatch(route, literal, token, i, segments.Count, StringComparison.OrdinalIgnoreCase))
 				{
 					return null;
 				}
@@ -223,14 +223,18 @@ private static bool IsBetterMissingArgumentsFailure(
 		return null;
 	}
 
-	private static bool IsLiteralMatch(
+	// Shared with the autocomplete engine so the alias rule cannot drift between routing
+	// and completion; the comparison is a parameter because autocomplete honors its own
+	// configurable case sensitivity.
+	internal static bool IsLiteralMatch(
 		RouteDefinition route,
 		LiteralRouteSegment segment,
 		string token,
 		int segmentIndex,
-		int segmentCount)
+		int segmentCount,
+		StringComparison comparison)
 	{
-		if (string.Equals(segment.Value, token, StringComparison.OrdinalIgnoreCase))
+		if (string.Equals(segment.Value, token, comparison))
 		{
 			return true;
 		}
@@ -242,6 +246,6 @@ private static bool IsLiteralMatch(
 		}
 
 		return route.Command.Aliases.Any(alias =>
-			string.Equals(alias, token, StringComparison.OrdinalIgnoreCase));
+			string.Equals(alias, token, comparison));
 	}
 }
diff --git a/src/Repl.Core/ShellCompletion/ShellCompletionEngine.cs b/src/Repl.Core/ShellCompletion/ShellCompletionEngine.cs
index 7ece0d99..81c0222b 100644
--- a/src/Repl.Core/ShellCompletion/ShellCompletionEngine.cs
+++ b/src/Repl.Core/ShellCompletion/ShellCompletionEngine.cs
@@ -7,42 +7,48 @@ namespace Repl;
 /// </summary>
 internal sealed class ShellCompletionEngine(CoreReplApp app)
 {
-	private static readonly string[] StaticShellGlobalOptions =
-	[
-		"--help",
-		"--interactive",
-		"--no-interactive",
-		"--no-logo",
-		"--output:",
-	];
 
 	public string[] ResolveShellCompletionCandidates(string line, int cursor)
 	{
-		var activeGraph = app.ResolveActiveRoutingGraph();
+		// Completion must not poison the durable routing cache (see the interactive path).
+		var activeGraph = app.ResolveActiveRoutingGraph(useDurableCache: false);
 		var state = AnalyzeShellCompletionInput(line, cursor);
 		if (state.PriorTokens.Length == 0)
 		{
 			return [];
 		}
 
-		var parsed = state.PriorTokens.Length <= 1
-			? InvocationOptionParser.Parse(Array.Empty<string>())
-			: InvocationOptionParser.Parse(new ArraySegment<string>(
-				state.PriorTokens,
-				offset: 1,
-				count: state.PriorTokens.Length - 1));
-		var commandPrefix = parsed.PositionalArguments as string[] ?? [.. parsed.PositionalArguments];
+		var resolution = ResolveShellCommitted(state.PriorTokens, activeGraph);
+		var commandPrefix = resolution.CommandPrefix;
+		var optionsTerminated = resolution.OptionsTerminated;
+		var routeMatch = resolution.Match;
 		var currentTokenPrefix = state.CurrentTokenPrefix;
-		var currentTokenIsOption = AutocompleteEngine.IsGlobalOptionToken(currentTokenPrefix);
-		var routeMatch = app.Resolve(commandPrefix, activeGraph.Routes);
-		var hasTerminalRoute = routeMatch is not null && routeMatch.RemainingTokens.Count == 0;
+		// Same gate as the interactive menu: single-dash prefixes surface short option
+		// aliases (-f); signed numeric literals stay positional. After the POSIX "--"
+		// separator no option names may be offered — everything is positional.
+		var currentTokenIsOption = !optionsTerminated && AutocompleteEngine.IsOptionPrefixToken(currentTokenPrefix);
+		// Terminal-for-options only when every positional segment (required or optional) is
+		// filled — an unfilled trailing segment would capture the accepted option/value.
+		var hasTerminalRoute = routeMatch is not null
+			&& commandPrefix.Length == routeMatch.Route.Template.Segments.Count;
 		var dedupe = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 		var candidates = new List<string>(capacity: 16);
+
+		// The pending option is whatever the LAST committed token is (the first prior token is
+		// the executable name). Enum-value completion only applies when that pending option is
+		// the route's own trailing option — not a valued global that the resolution stripped
+		// (e.g. "run --mode --tenant " is pending on --tenant, not --mode).
+		var afterExecutable = state.PriorTokens.Length > 1 ? state.PriorTokens[1..] : [];
+		var routeOptionIsLastCommitted = routeMatch is not null
+			&& routeMatch.RemainingTokens.Count > 0
+			&& afterExecutable.Length > 0
+			&& string.Equals(afterExecutable[^1], routeMatch.RemainingTokens[^1], StringComparison.Ordinal);
 		if (!currentTokenIsOption
+			&& !optionsTerminated
 			&& hasTerminalRoute
+			&& routeOptionIsLastCommitted
 			&& TryAddRouteEnumValueCandidates(
-				routeMatch!.Route,
-				state.PriorTokens,
+				routeMatch!,
 				currentTokenPrefix,
 				dedupe,
 				candidates))
@@ -51,10 +57,35 @@ public string[] ResolveShellCompletionCandidates(string line, int cursor)
 			return [.. candidates];
 		}
 
-		if (!currentTokenIsOption)
+		// A valued option still awaiting its value (and not an enum, which the block above
+		// would have completed) makes the current token that value — offering a command or
+		// option name here would misparse.
+		if (app.Autocomplete.IsPendingOptionValue(afterExecutable, routeMatch, optionsTerminated, currentTokenPrefix))
+		{
+			return [];
+		}
+
+		AddShellCommandAndOptionCandidates(
+			resolution, activeGraph, currentTokenPrefix, currentTokenIsOption, hasTerminalRoute, dedupe, candidates);
+		candidates.Sort(StringComparer.OrdinalIgnoreCase);
+		return [.. candidates];
+	}
+
+	private void AddShellCommandAndOptionCandidates(
+		ShellResolution resolution,
+		ActiveRoutingGraph activeGraph,
+		string currentTokenPrefix,
+		bool currentTokenIsOption,
+		bool hasTerminalRoute,
+		HashSet<string> dedupe,
+		List<string> candidates)
+	{
+		// No subcommand can follow once a terminal route already carries trailing option
+		// tokens (see the interactive path).
+		if (!currentTokenIsOption && resolution.Match is not { RemainingTokens.Count: > 0 })
 		{
 			AddShellCommandCandidates(
-				commandPrefix,
+				resolution.CommandPrefix,
 				currentTokenPrefix,
 				activeGraph.Routes,
 				activeGraph.Contexts,
@@ -62,32 +93,97 @@ public string[] ResolveShellCompletionCandidates(string line, int cursor)
 				candidates);
 		}
 
-		if (currentTokenIsOption || (string.IsNullOrEmpty(currentTokenPrefix) && hasTerminalRoute))
+		if (!resolution.OptionsTerminated
+			&& (currentTokenIsOption || (string.IsNullOrEmpty(currentTokenPrefix) && hasTerminalRoute)))
 		{
 			AddShellOptionCandidates(
-				hasTerminalRoute ? routeMatch!.Route : null,
+				hasTerminalRoute ? resolution.Match!.Route : null,
 				currentTokenPrefix,
-				dedupe,
 				candidates);
 		}
+	}
 
-		candidates.Sort(StringComparer.OrdinalIgnoreCase);
-		return [.. candidates];
+	// Mirrors the interactive engine's single resolution: the first prior token is the
+	// executable name; global options are stripped with the arity-aware parser, unique
+	// command prefixes are expanded (so "i" resolves to "install" like execution does), and
+	// the route is resolved on the remaining tokens BEFORE option parsing — so dash tokens
+	// and the bare "--" bind to segments as positional values. The match's trailing tokens
+	// are the route's option region; a "--" among them terminates options, one bound to a
+	// segment does not.
+	private ShellResolution ResolveShellCommitted(string[] priorTokens, ActiveRoutingGraph activeGraph)
+	{
+		if (priorTokens.Length <= 1)
+		{
+			return new ShellResolution([], Match: null, OptionsTerminated: false);
+		}
+
+		var afterExecutable = new ArraySegment<string>(priorTokens, offset: 1, count: priorTokens.Length - 1);
+		var stripped = GlobalOptionParser
+			.Parse(afterExecutable, app.OptionsSnapshot.Output, app.OptionsSnapshot.Parsing)
+			.RemainingTokens;
+		var expanded = ExpandUniquePrefixes(stripped as string[] ?? [.. stripped], activeGraph);
+		if (app.Resolve(expanded, activeGraph.Routes) is { } match)
+		{
+			var segmentCount = Math.Min(match.Route.Template.Segments.Count, expanded.Length);
+			var optionsTerminated = false;
+			foreach (var trailing in match.RemainingTokens)
+			{
+				if (string.Equals(trailing, "--", StringComparison.Ordinal))
+				{
+					optionsTerminated = true;
+					break;
+				}
+			}
+
+			return new ShellResolution(expanded[..segmentCount], match, optionsTerminated);
+		}
+
+		return new ShellResolution(expanded, Match: null, OptionsTerminated: false);
+	}
+
+	// Bounded unique-prefix/alias expansion mirroring the interactive engine.
+	private string[] ExpandUniquePrefixes(string[] tokens, ActiveRoutingGraph activeGraph)
+	{
+		if (tokens.Length == 0)
+		{
+			return tokens;
+		}
+
+		var expansionDepth = 0;
+		foreach (var route in activeGraph.Routes)
+		{
+			expansionDepth = Math.Max(expansionDepth, route.Template.Segments.Count);
+		}
+
+		foreach (var context in activeGraph.Contexts)
+		{
+			expansionDepth = Math.Max(expansionDepth, context.Template.Segments.Count);
+		}
+
+		if (tokens.Length <= expansionDepth)
+		{
+			var resolution = app.ResolveUniquePrefixes(tokens, activeGraph);
+			return resolution.IsAmbiguous ? tokens : resolution.Tokens;
+		}
+
+		var headResolution = app.ResolveUniquePrefixes(tokens[..expansionDepth], activeGraph);
+		return headResolution.IsAmbiguous
+			? tokens
+			: [.. headResolution.Tokens, .. tokens[expansionDepth..]];
 	}
 
 	private bool TryAddRouteEnumValueCandidates(
-		RouteDefinition route,
-		string[] priorTokens,
+		RouteMatch match,
 		string currentTokenPrefix,
 		HashSet<string> dedupe,
 		List<string> candidates)
 	{
-		if (!TryResolvePendingRouteOption(route, priorTokens, out var entry))
+		if (!TryResolvePendingRouteOption(match, out var entry))
 		{
 			return false;
 		}
 
-		if (!route.OptionSchema.TryGetParameter(entry.ParameterName, out var parameter))
+		if (!match.Route.OptionSchema.TryGetParameter(entry.ParameterName, out var parameter))
 		{
 			return false;
 		}
@@ -114,24 +210,24 @@ private bool TryAddRouteEnumValueCandidates(
 	}
 
 	private bool TryResolvePendingRouteOption(
-		RouteDefinition route,
-		string[] priorTokens,
+		RouteMatch match,
 		out OptionSchemaEntry entry)
 	{
 		entry = default!;
-		if (priorTokens.Length <= 1)
-		{
-			return false;
-		}
 
-		var commandTokens = priorTokens[1..];
-		if (commandTokens.Length == 0)
+		// The pending option is the LAST token in the route's trailing option region — not a
+		// dash-prefixed token that routing already bound to a positional segment. Deriving it
+		// from match.RemainingTokens (rather than the raw prior tokens) is what keeps
+		// "deploy -m" (where -m fills {target}) from being mistaken for a pending "-m" option.
+		if (match.RemainingTokens.Count == 0)
 		{
 			return false;
 		}
 
-		var previousToken = commandTokens[^1];
-		if (!AutocompleteEngine.IsGlobalOptionToken(previousToken))
+		var previousToken = match.RemainingTokens[^1];
+		// A single dash is enough: short option aliases (e.g. "-m") take values too, and the
+		// schema resolves them like any other token below.
+		if (!AutocompleteEngine.IsOptionPrefixToken(previousToken))
 		{
 			return false;
 		}
@@ -142,7 +238,7 @@ private bool TryResolvePendingRouteOption(
 			return false;
 		}
 
-		var matches = route.OptionSchema.ResolveToken(previousToken, app.OptionsSnapshot.Parsing.OptionCaseSensitivity);
+		var matches = match.Route.OptionSchema.ResolveToken(previousToken, app.OptionsSnapshot.Parsing.OptionCaseSensitivity);
 		var distinct = matches
 			.DistinctBy(candidate => (candidate.ParameterName, candidate.TokenKind, candidate.InjectedValue), ShellOptionSchemaEntryComparer.Instance)
 			.ToArray();
@@ -202,17 +298,24 @@ private void AddShellCommandCandidates(
 	private void AddShellOptionCandidates(
 		RouteDefinition? route,
 		string currentTokenPrefix,
-		HashSet<string> dedupe,
 		List<string> candidates)
 	{
-		AddGlobalShellOptionCandidates(currentTokenPrefix, dedupe, candidates);
+		// Option candidates dedupe with the PARSER's case semantics, not the command/UI
+		// OrdinalIgnoreCase set: under case-sensitive option parsing, "-m" and "-M" can bind
+		// to different parameters and both are executable, so they must not collapse. (Option
+		// tokens start with '-' and never collide with command names, so a separate set is safe.)
+		var optionDedupe = new HashSet<string>(
+			app.OptionsSnapshot.Parsing.OptionCaseSensitivity == ReplCaseSensitivity.CaseInsensitive
+				? StringComparer.OrdinalIgnoreCase
+				: StringComparer.Ordinal);
+		AddGlobalShellOptionCandidates(currentTokenPrefix, optionDedupe, candidates);
 
 		if (route is null)
 		{
 			return;
 		}
 
-		AddRouteShellOptionCandidates(route, currentTokenPrefix, dedupe, candidates);
+		AddRouteShellOptionCandidates(route, currentTokenPrefix, optionDedupe, candidates);
 	}
 
 	private void AddGlobalShellOptionCandidates(
@@ -221,50 +324,12 @@ private void AddGlobalShellOptionCandidates(
 		List<string> candidates)
 	{
 		var options = app.OptionsSnapshot;
-		var comparison = options.Parsing.OptionCaseSensitivity == ReplCaseSensitivity.CaseInsensitive
-			? StringComparison.OrdinalIgnoreCase
-			: StringComparison.Ordinal;
-		foreach (var option in StaticShellGlobalOptions)
-		{
-			if (option.StartsWith(currentTokenPrefix, comparison))
-			{
-				TryAddShellCompletionCandidate(option, dedupe, candidates);
-			}
-		}
-
-		foreach (var alias in options.Output.Aliases.Keys)
-		{
-			var opt = $"--{alias}";
-			if (opt.StartsWith(currentTokenPrefix, comparison))
-			{
-				TryAddShellCompletionCandidate(opt, dedupe, candidates);
-			}
-		}
-
-		foreach (var format in options.Output.Transformers.Keys)
-		{
-			var opt = $"--output:{format}";
-			if (opt.StartsWith(currentTokenPrefix, comparison))
-			{
-				TryAddShellCompletionCandidate(opt, dedupe, candidates);
-			}
-		}
-
-		foreach (var custom in options.Parsing.GlobalOptions.Values)
-		{
-			if (custom.CanonicalToken.StartsWith(currentTokenPrefix, comparison))
-			{
-				TryAddShellCompletionCandidate(custom.CanonicalToken, dedupe, candidates);
-			}
-
-			foreach (var alias in custom.Aliases)
-			{
-				if (alias.StartsWith(currentTokenPrefix, comparison))
-				{
-					TryAddShellCompletionCandidate(alias, dedupe, candidates);
-				}
-			}
-		}
+		OptionTokenCompletionSource.CollectGlobalOptionTokens(
+			options,
+			currentTokenPrefix,
+			options.Parsing.OptionCaseSensitivity.ToStringComparison(),
+			dedupe,
+			candidates);
 	}
 
 	private void AddRouteShellOptionCandidates(
@@ -273,16 +338,12 @@ private void AddRouteShellOptionCandidates(
 		HashSet<string> dedupe,
 		List<string> candidates)
 	{
-		var comparison = app.OptionsSnapshot.Parsing.OptionCaseSensitivity == ReplCaseSensitivity.CaseInsensitive
-			? StringComparison.OrdinalIgnoreCase
-			: StringComparison.Ordinal;
-		foreach (var token in route.OptionSchema.KnownTokens)
-		{
-			if (token.StartsWith(currentTokenPrefix, comparison))
-			{
-				TryAddShellCompletionCandidate(token, dedupe, candidates);
-			}
-		}
+		OptionTokenCompletionSource.CollectRouteOptionTokens(
+			route,
+			currentTokenPrefix,
+			app.OptionsSnapshot.Parsing.OptionCaseSensitivity,
+			dedupe,
+			candidates);
 	}
 
 	internal static ShellCompletionInputState AnalyzeShellCompletionInput(string input, int cursor)
@@ -339,6 +400,11 @@ internal readonly record struct ShellCompletionInputState(
 		string[] PriorTokens,
 		string CurrentTokenPrefix);
 
+	private readonly record struct ShellResolution(
+		string[] CommandPrefix,
+		RouteMatch? Match,
+		bool OptionsTerminated);
+
 	internal static string ResolveShellCompletionCommandName(
 		IReadOnlyList<string>? commandLineArgs,
 		string? processPath,
diff --git a/src/Repl.IntegrationTests/Given_ModulePresence.cs b/src/Repl.IntegrationTests/Given_ModulePresence.cs
index ecc901ad..045280c1 100644
--- a/src/Repl.IntegrationTests/Given_ModulePresence.cs
+++ b/src/Repl.IntegrationTests/Given_ModulePresence.cs
@@ -138,11 +138,51 @@ public void When_ModulePresenceServiceStateChangesWithoutInvalidation_Then_Previ
 		output.Text.Should().NotContain("member");
 	}
 
+	[TestMethod]
+	[Description("Regression guard: a completion pass that evaluates a globally-gated routing graph must not poison the durable routing cache. After autocomplete resolves the graph without the line's '--env prod' applied, the committed 'gated' command (which supplies '--env prod') must still resolve, not fail as Unknown command.")]
+	public async Task When_CompletionEvaluatesGatedGraph_Then_CommittedCommandStillResolves()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("env"));
+		sut.MapModule(
+			new EnvGatedModule(),
+			context => string.Equals(
+				(context.ServiceProvider.GetService(typeof(IGlobalOptionsAccessor)) as IGlobalOptionsAccessor)?.GetValue<string>("env"),
+				"prod",
+				StringComparison.Ordinal));
+
+		// A completion pass evaluates the routing graph while "--env prod" is NOT applied,
+		// so the gated module is absent at that moment.
+		_ = await sut.Autocomplete.ResolveAutocompleteAsync(
+			new ConsoleLineReader.AutocompleteRequest("gated", 5, MenuRequested: true),
+			scopeTokens: [],
+			EmptyServiceProvider.Instance,
+			CancellationToken.None).ConfigureAwait(false);
+
+		var output = ConsoleCaptureHelper.Capture(() => sut.Run(["--env", "prod", "gated", "--no-logo"]));
+
+		output.ExitCode.Should().Be(0, output.Text);
+		output.Text.Should().Contain("gated-ok",
+			because: "completion must not cache a module-absent graph that then defeats the committed command");
+	}
+
 	private static bool IsSignedIn(IReplSessionState sessionState)
 	{
 		return sessionState.TryGet<bool>("auth.signed_in", out var signedIn) && signedIn;
 	}
 
+	private sealed class EnvGatedModule : IReplModule
+	{
+		public void Map(IReplMap map) => map.Map("gated", static string () => "gated-ok");
+	}
+
+	private sealed class EmptyServiceProvider : IServiceProvider
+	{
+		public static readonly EmptyServiceProvider Instance = new();
+
+		public object? GetService(Type serviceType) => null;
+	}
+
 	private sealed class SignedOutExperienceModule : IReplModule
 	{
 		public void Map(IReplMap map)
diff --git a/src/Repl.Tests/Given_InteractiveAutocomplete_OptionCandidates.cs b/src/Repl.Tests/Given_InteractiveAutocomplete_OptionCandidates.cs
new file mode 100644
index 00000000..37826060
--- /dev/null
+++ b/src/Repl.Tests/Given_InteractiveAutocomplete_OptionCandidates.cs
@@ -0,0 +1,819 @@
+namespace Repl.Tests;
+
+[TestClass]
+public sealed class Given_InteractiveAutocomplete_OptionCandidates
+{
+	[TestMethod]
+	[Description("Interactive autocomplete suggests route and global option names when the current token is an option prefix.")]
+	public async Task When_CurrentTokenIsOptionPrefix_Then_SuggestsRouteAndGlobalOptions()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant", aliases: ["--org"]));
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--force");
+		values.Should().Contain("--help");
+		values.Should().Contain("--json");
+		values.Should().Contain("--tenant");
+		values.Should().Contain("--org");
+		result.Suggestions.Should().OnlyContain(static suggestion => suggestion.IsSelectable);
+		result.HintLine.Should().Contain("--force");
+	}
+
+	[TestMethod]
+	[Description("Interactive autocomplete filters option suggestions by a partial option prefix.")]
+	public async Task When_CurrentTokenIsPartialOptionPrefix_Then_FiltersOptionSuggestions()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls --fo").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().ContainSingle().Which.Should().Be("--force");
+		result.ReplaceStart.Should().Be("install bib-overalls ".Length);
+		result.ReplaceLength.Should().Be("--fo".Length);
+		result.HintLine.Should().Be("--force");
+	}
+
+	[TestMethod]
+	[Description("A valued option given in its space-separated form must not break route matching: the option's VALUE is consumed like the invocation parser does, so '--channel beta' does not leave a stray positional that hides the route's own options.")]
+	public async Task When_ValuedOptionPrecedesOptionPrefix_Then_RouteOptionsAreStillSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map(
+				"install {skillName}",
+				static string (string skillName, [ReplOption] bool force, [ReplOption] string? channel) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls --channel beta --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--force", because: "consuming '--channel beta' leaves the same command prefix the router sees");
+	}
+
+	[TestMethod]
+	[Description("Command aliases resolve for option suggestions like they do for execution: a command whose TERMINAL literal is aliased ('ls' for 'list') surfaces the route's options when invoked through the alias, exercising RouteResolver's terminal-segment alias matching (not unique-prefix expansion).")]
+	public async Task When_CommandIsInvokedThroughAlias_Then_RouteOptionsAreSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("contact list", static string ([ReplOption] bool verbose) => verbose.ToString())
+			.WithAlias("ls")
+			.WithDescription("List contacts.");
+
+		var result = await ResolveAutocompleteAsync(sut, "contact ls --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--verbose", because: "the router accepts the terminal-segment alias, so autocomplete must too");
+	}
+
+	[TestMethod]
+	[Description("Unique command-prefix expansion feeds option suggestions like it feeds execution: typing a unique prefix of a command ('i' for 'install') resolves the route and surfaces its options.")]
+	public async Task When_CommandIsInvokedThroughUniquePrefix_Then_RouteOptionsAreSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "i bib-overalls --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--force", because: "'i' uniquely prefixes 'install', so its route options must appear");
+	}
+
+	[TestMethod]
+	[Description("A single dash is already an option prefix: short option aliases declared via [ReplOption(Aliases = [\"-f\"])] must be suggested when the user types '-'.")]
+	public async Task When_CurrentTokenIsSingleDash_Then_ShortOptionAliasesAreSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map(
+				"install {skillName}",
+				static string (string skillName, [ReplOption(Aliases = ["-f"])] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls -").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("-f", because: "declared short aliases are reachable only from a single-dash prefix");
+		values.Should().Contain("--force");
+	}
+
+	[TestMethod]
+	[Description("Route options wait until required positionals are filled: 'install --' (with {skillName} unfilled) must NOT offer --force, because accepting it yields 'install --force', which does not run cleanly. Global options still appear.")]
+	public async Task When_RequiredPositionalIsStillMissing_Then_RouteOptionsAreNotSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().NotContain("--force", because: "a required positional is unfilled — 'install --force' does not run cleanly, so route options wait until the arguments are satisfied");
+		values.Should().Contain("--help", because: "global options remain available regardless of positional state");
+	}
+
+	[TestMethod]
+	[Description("A bare '--' prior token is the POSIX end-of-options separator: everything after it is positional, so no option name may be suggested past it.")]
+	public async Task When_EndOfOptionsSeparatorPrecedes_Then_NoOptionIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls -- --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().NotContain("--force", because: "tokens after the end-of-options separator are positional");
+		values.Should().NotContain("--help");
+	}
+
+	[TestMethod]
+	[Description("A signed numeric literal is a positional argument, not an option prefix: typing '-4' must not open the option menu (mirrors the invocation parser's IsSignedNumericLiteral rule).")]
+	public async Task When_CurrentTokenIsNegativeNumber_Then_NoOptionIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls -4").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().NotContain("--force");
+	}
+
+	[TestMethod]
+	[Description("Dynamic completion providers complete parameter VALUES; when the current token is an option prefix the provider must not run, otherwise its values pollute the option menu.")]
+	public async Task When_CurrentTokenIsOptionPrefix_Then_DynamicCompletionProviderIsSkipped()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithCompletion("skillName", static (_, _, _) => ValueTask.FromResult<IReadOnlyList<string>>(["ga", "bu", "zo", "meu"]))
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().NotContain("ga", because: "provider values are parameter values, not option names");
+		values.Should().Contain("--force");
+	}
+
+	[TestMethod]
+	[Description("An option name shared by a global option and a route option appears exactly once in the menu — the dedup set spans both sources.")]
+	public async Task When_GlobalAndRouteOptionsCollide_Then_SuggestionAppearsOnce()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("force"));
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls --fo").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Count(static value => string.Equals(value, "--force", StringComparison.OrdinalIgnoreCase))
+			.Should().Be(1);
+	}
+
+	[TestMethod]
+	[Description("In option-prefix mode the live hint must describe the option alternatives, not a positional parameter: 'install bib-overalls --' lists --force in the menu, so a 'Param' hint would contradict what the user is looking at.")]
+	public async Task When_OptionPrefixOnTerminalRoute_Then_HintShowsOptionsNotParameter()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName} {version}", static string (string skillName, string version, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "install bib-overalls 42 --").ConfigureAwait(false);
+
+		result.HintLine.Should().NotContain("Param", because: "the user asked for options, not a positional parameter");
+		result.HintLine.Should().Contain("--force");
+	}
+
+	[TestMethod]
+	[Description("Autocomplete must NEVER touch the filesystem: a '@file' prior token stays a literal token instead of being expanded as a response file. Hosted sessions feed remote-controlled lines through this path on every keystroke — expansion would mean server-side file reads (UNC probes included) driven by keystrokes.")]
+	public async Task When_PriorTokenLooksLikeResponseFile_Then_ItIsNotExpanded()
+	{
+		var responseFile = Path.GetTempFileName();
+		try
+		{
+			// If the parser expanded this, the prefix would gain three tokens and the
+			// route would no longer match — observable as the route options vanishing.
+			await File.WriteAllTextAsync(responseFile, "ga bu zo").ConfigureAwait(false);
+			var sut = CoreReplApp.Create();
+			sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+				.WithDescription("Install a skill.");
+
+			var result = await ResolveAutocompleteAsync(sut, $"install @{responseFile} --").ConfigureAwait(false);
+
+			var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+			values.Should().Contain("--force", because: "the @token must be treated as a plain positional, not expanded from disk");
+		}
+		finally
+		{
+			File.Delete(responseFile);
+		}
+	}
+
+	[TestMethod]
+	[Description("Tokens after the end-of-options separator are positional even when they look like flags: in 'deploy -- -f ' the '-f' fills the {target} segment, so the exact-route completion provider must fire — dropping it would desync the segment count.")]
+	public async Task When_DashTokenFollowsSeparator_Then_ItCountsAsPositional()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("deploy {target}", static string (string target) => target)
+			.WithCompletion("target", static (_, _, _) => ValueTask.FromResult<IReadOnlyList<string>>(["zo-profile"]))
+			.WithDescription("Deploy a target.");
+
+		var result = await ResolveAutocompleteAsync(sut, "deploy -- -f ").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("zo-profile", because: "'-f' after '--' is a positional filling {target}, making the route exact for the provider");
+	}
+
+	[TestMethod]
+	[Description("Parity contract between the two completion surfaces: for the same app and the same '--' input, the interactive menu and the shell-completion candidates carry the same option tokens. The shared token source is only half the guarantee — the gates must agree too, and this pin catches a divergent reimplementation on either side.")]
+	public async Task When_ComparingInteractiveAndShellOnDoubleDash_Then_CandidatesMatch()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant", aliases: ["--org"]));
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string shellLine = "app install bib-overalls --";
+
+		var interactive = await ResolveAutocompleteAsync(sut, "install bib-overalls --").ConfigureAwait(false);
+		var shell = shellEngine.ResolveShellCompletionCandidates(shellLine, shellLine.Length);
+
+		var interactiveOptions = interactive.Suggestions
+			.Where(static suggestion => suggestion.IsSelectable)
+			.Select(static suggestion => suggestion.Value)
+			.ToArray();
+		interactiveOptions.Should().BeEquivalentTo(shell);
+	}
+
+	[TestMethod]
+	[Description("Parity contract for the single-dash gate: shell completion must surface short option aliases (-f) from '-' exactly like the interactive menu does — the two surfaces answering the same question differently is operator-visible confusion.")]
+	public async Task When_ComparingInteractiveAndShellOnSingleDash_Then_CandidatesMatch()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map(
+				"install {skillName}",
+				static string (string skillName, [ReplOption(Aliases = ["-f"])] bool force) => skillName)
+			.WithDescription("Install a skill.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string shellLine = "app install bib-overalls -";
+
+		var interactive = await ResolveAutocompleteAsync(sut, "install bib-overalls -").ConfigureAwait(false);
+		var shell = shellEngine.ResolveShellCompletionCandidates(shellLine, shellLine.Length);
+
+		var interactiveOptions = interactive.Suggestions
+			.Where(static suggestion => suggestion.IsSelectable)
+			.Select(static suggestion => suggestion.Value)
+			.ToArray();
+		interactiveOptions.Should().Contain("-f");
+		interactiveOptions.Should().BeEquivalentTo(shell);
+	}
+
+	[TestMethod]
+	[Description("Overloaded routes select ONE route the way execution does: 'item 42 --' resolves to the int overload, so only its options appear, never the string overload's — accepting a string-only option would fail against the int route that actually runs.")]
+	public async Task When_RouteIsOverloaded_Then_OnlyTheSelectedOverloadsOptionsAreSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("item {value}", static string (string value, [ReplOption] bool tag) => value)
+			.WithDescription("String item.");
+		sut.Map("item {value:int}", static string (int value, [ReplOption] bool verbose) => $"{value}")
+			.WithDescription("Int item.");
+
+		var result = await ResolveAutocompleteAsync(sut, "item 42 --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--verbose", because: "42 resolves to the int overload");
+		values.Should().NotContain("--tag", because: "the string overload is not the one that would run");
+	}
+
+	[TestMethod]
+	[Description("A valueless global flag before the command must not swallow the command word: '--no-logo install bib-overalls --' still resolves the install route (GlobalOptionParser knows --no-logo takes no value), so route options appear.")]
+	public async Task When_ValuelessGlobalFlagPrecedesCommand_Then_RouteOptionsAreStillSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName, [ReplOption] bool force) => skillName)
+			.WithDescription("Install a skill.");
+
+		var result = await ResolveAutocompleteAsync(sut, "--no-logo install bib-overalls --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--force", because: "'--no-logo' is a valueless global and must not consume 'install' as its value");
+	}
+
+	[TestMethod]
+	[Description("A valued short option alias must not break route resolution: 'run -c beta --' keeps the terminal 'run' route (the router leaves '-c beta' as route-option tokens), so route options keep appearing after the alias value.")]
+	public async Task When_ValuedShortAliasPrecedesOptionPrefix_Then_RouteOptionsAreStillSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption(Aliases = ["-c"])] string? channel, [ReplOption] bool force) => channel ?? "none")
+			.WithDescription("Run something.");
+
+		var result = await ResolveAutocompleteAsync(sut, "run -c beta --").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("--force", because: "'-c beta' are route-option tokens; the 'run' route stays terminal");
+	}
+
+	[TestMethod]
+	[Description("After the POSIX '--' separator a dash-prefixed current token is positional, so a value-completion provider must still run instead of being suppressed as an option prefix: 'deploy x -- -' asks the provider rather than treating '-' as an option name.")]
+	public async Task When_DashCurrentTokenFollowsSeparator_Then_ProviderStillRuns()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("deploy {target}", static string (string target) => target)
+			.WithCompletion("target", static (_, _, _) => ValueTask.FromResult<IReadOnlyList<string>>(["zo-profile"]))
+			.WithDescription("Deploy a target.");
+
+		var result = await ResolveAutocompleteAsync(sut, "deploy x -- -").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("zo-profile", because: "after '--' the '-' token is positional, so the provider still runs");
+	}
+
+	[TestMethod]
+	[Description("Shell parity for a valued short alias: after 'app install pkg -f ' shell completion still offers the install route's options, matching what execution parses ('-f' is a route option, not a stray positional).")]
+	public void When_ShellCompletesAfterValuedShortAlias_Then_RouteOptionsAreStillOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {name}", static string (string name, [ReplOption(Aliases = ["-f"])] bool force) => name)
+			.WithDescription("Install a skill.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app install pkg -f --";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().Contain("--force", because: "'-f' is a route option; the install route stays terminal in shell completion too");
+	}
+
+	[TestMethod]
+	[Description("Shell parity for the POSIX separator: after 'app install pkg -- ' everything is positional, so shell completion offers no option names past '--'.")]
+	public void When_ShellCompletesAfterSeparator_Then_NoOptionIsOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {name}", static string (string name, [ReplOption] bool force) => name)
+			.WithDescription("Install a skill.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app install pkg -- --";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().NotContain("--force", because: "tokens after '--' are positional");
+		candidates.Should().NotContain("--help");
+	}
+
+	[TestMethod]
+	[Description("Route resolution runs before option parsing, so a dash-prefixed token can satisfy a required positional: 'remote -prod st' binds '-prod' to {name}, leaving the later literal 'status' as the completion — dropping '-prod' as if it were an option would hide it.")]
+	public async Task When_DashPrefixedTokenFillsRequiredPositional_Then_LaterLiteralIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("remote {name} status", static string (string name) => name)
+			.WithDescription("Remote status.");
+
+		var result = await ResolveAutocompleteAsync(sut, "remote -prod st").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("status", because: "'-prod' binds to {name}; the next segment's literal must still be suggested");
+	}
+
+	[TestMethod]
+	[Description("The bare '--' separator is a positional value to route resolution (which runs before option parsing): 'remote -- st' binds '--' to {name}, so the later literal 'status' is still the completion.")]
+	public async Task When_SeparatorFillsRequiredPositional_Then_LaterLiteralIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("remote {name} status", static string (string name) => name)
+			.WithDescription("Remote status.");
+
+		var result = await ResolveAutocompleteAsync(sut, "remote -- st").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static suggestion => suggestion.Value).ToArray();
+		values.Should().Contain("status", because: "'--' binds to {name} as a positional; the next literal must still be suggested");
+	}
+
+	[TestMethod]
+	[Description("Per-option case sensitivity is honored when filtering option-name candidates: an entry declared case-insensitive is offered for a differently-cased prefix ('--FO' offers '--force') even when the global default is case-sensitive, matching what the parser accepts. Exercised at the shared source because a per-option override is otherwise not expressible through the attribute.")]
+	public void When_SchemaEntryIsCaseInsensitive_Then_DifferentlyCasedPrefixStillMatches()
+	{
+		var entries = new[]
+		{
+			new Repl.Internal.Options.OptionSchemaEntry(
+				"--force",
+				"force",
+				Repl.Internal.Options.OptionSchemaTokenKind.BoolFlag,
+				ReplArity.ZeroOrOne,
+				CaseSensitivity: ReplCaseSensitivity.CaseInsensitive),
+		};
+		var schema = new Repl.Internal.Options.OptionSchema(
+			entries,
+			new Dictionary<string, Repl.Internal.Options.OptionSchemaParameter>(StringComparer.OrdinalIgnoreCase));
+
+		var results = new List<string>();
+		Repl.Internal.Options.OptionTokenCompletionSource.CollectRouteOptionTokens(
+			schema,
+			"--FO",
+			ReplCaseSensitivity.CaseSensitive,
+			new HashSet<string>(StringComparer.Ordinal),
+			results);
+
+		results.Should().Contain("--force", because: "the entry is case-insensitive, so '--FO' matches it even under a case-sensitive global default");
+	}
+
+	[TestMethod]
+	[Description("Shell enum-value completion recognizes short option aliases: after 'app run -m ' (where -m is a short alias for an enum option) the enum values are offered, matching what the parser accepts for '-m Debug'.")]
+	public void When_ShellCompletesEnumValueAfterShortAlias_Then_EnumNamesAreOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption(Aliases = ["-m"])] ProbeMode mode) => mode.ToString())
+			.WithDescription("Run.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app run -m ";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().Contain("Debug", because: "'-m' is a short alias for the enum option; its values must complete like '--mode' does");
+	}
+
+	[TestMethod]
+	[Description("Route options wait until OPTIONAL positionals are disambiguated too: 'run --' on 'run {profile?}' must not offer --force, because RouteResolver would bind --force to the optional {profile} before option parsing. Once the optional is filled ('run x --'), route options appear.")]
+	public async Task When_OptionalPositionalIsUnfilled_Then_RouteOptionsAreNotSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run {profile?}", static string (string? profile, [ReplOption] bool force) => profile ?? "none")
+			.WithDescription("Run.");
+
+		var unfilled = await ResolveAutocompleteAsync(sut, "run --").ConfigureAwait(false);
+		var filled = await ResolveAutocompleteAsync(sut, "run zo --").ConfigureAwait(false);
+
+		unfilled.Suggestions.Select(static s => s.Value).Should().NotContain("--force",
+			because: "an unfilled optional positional would capture --force as its value");
+		filled.Suggestions.Select(static s => s.Value).Should().Contain("--force",
+			because: "once the optional positional is filled, route options are safe");
+	}
+
+	[TestMethod]
+	[Description("A bare '--' bound to a positional does not terminate options: 'remote -- status --' binds '--' to {name}, the route is terminal, and the trailing current token asks for option names — the positional '--' must not suppress them.")]
+	public async Task When_SeparatorIsBoundAsPositional_Then_OptionsAreStillOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("remote {name} status", static string (string name, [ReplOption] bool force) => name)
+			.WithDescription("Remote status.");
+
+		var result = await ResolveAutocompleteAsync(sut, "remote -- status --").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("--force",
+			because: "the '--' was consumed as {name}, so it is not the end-of-options separator here");
+	}
+
+	[TestMethod]
+	[Description("Token classification uses the resolved route boundary: in 'remote -prod status' the '-prod' binds to {name}, so 'status' is classified against segment 2 as a Command literal, not mis-indexed to segment 1.")]
+	public async Task When_DashPositionalPrecedes_Then_LaterLiteralClassifiesAsCommand()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("remote {name} status", static string (string name) => name)
+			.WithDescription("Remote status.");
+		const string input = "remote -prod status";
+
+		var result = await ResolveAutocompleteAsync(sut, input).ConfigureAwait(false);
+
+		var statusStart = input.IndexOf("status", StringComparison.Ordinal);
+		var statusClass = result.TokenClassifications!.Single(c => c.Start == statusStart);
+		statusClass.Kind.Should().Be(ConsoleLineReader.AutocompleteSuggestionKind.Command,
+			because: "'-prod' fills {name}, so 'status' is the literal at segment 2");
+	}
+
+	[TestMethod]
+	[Description("Interactive option completion offers output-format aliases case-insensitively, matching how GlobalOptionParser resolves them (OutputOptions.Aliases is a case-insensitive dictionary): '--J' offers '--json' even under a case-sensitive global default.")]
+	public async Task When_OutputAliasPrefixIsDifferentlyCased_Then_AliasIsStillSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.OptionCaseSensitivity = ReplCaseSensitivity.CaseSensitive);
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+
+		var result = await ResolveAutocompleteAsync(sut, "show --J").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("--json",
+			because: "output aliases resolve case-insensitively, so '--J' matches '--json'");
+	}
+
+	[TestMethod]
+	[Description("Shell completion expands unique command prefixes before resolving, like execution: 'app i pkg --' (where 'i' uniquely prefixes 'install') resolves the install route and offers its options.")]
+	public void When_ShellCompletesAfterUniquePrefix_Then_RouteOptionsAreOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {name}", static string (string name, [ReplOption] bool force) => name)
+			.WithDescription("Install a skill.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app i pkg --";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().Contain("--force", because: "'i' uniquely prefixes 'install', so its route options must be offered");
+	}
+
+	[TestMethod]
+	[Description("Shell enum completion must not fire for a dash token that routing consumed as a positional: 'app deploy -m ' binds '-m' to {target}, so no enum values are offered (accepting one would leave it as stray positional text).")]
+	public void When_DashTokenWasBoundAsPositional_Then_ShellOffersNoEnumValues()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("deploy {target}", static string (string target, [ReplOption(Aliases = ["-m"])] ProbeMode mode) => target)
+			.WithDescription("Deploy.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app deploy -m ";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().NotContain("Debug", because: "'-m' was bound to {target} by routing, so it is not a pending option here");
+	}
+
+	[TestMethod]
+	[Description("A valid leading global option keeps its option classification: '--no-logo show' classifies '--no-logo' as a Parameter (option), not Invalid — global options are executable before the command, independent of the route-option region.")]
+	public async Task When_GlobalOptionPrecedesCommand_Then_ItClassifiesAsOption()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+		const string input = "--no-logo show";
+
+		var result = await ResolveAutocompleteAsync(sut, input).ConfigureAwait(false);
+
+		var noLogo = result.TokenClassifications!.Single(c => c.Start == 0);
+		noLogo.Kind.Should().Be(ConsoleLineReader.AutocompleteSuggestionKind.Parameter,
+			because: "a global option before the command is valid, not Invalid");
+		var showStart = input.IndexOf("show", StringComparison.Ordinal);
+		result.TokenClassifications!.Single(c => c.Start == showStart).Kind
+			.Should().Be(ConsoleLineReader.AutocompleteSuggestionKind.Command);
+	}
+
+	[TestMethod]
+	[Description("Classification cost stays flat per completion rather than scaling with the token count: a long trailing token run must not blow up (it previously re-resolved the whole prefix per token). This pins that a wide input still classifies quickly and correctly.")]
+	public async Task When_ManyTrailingTokens_Then_ClassificationStaysBounded()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run {name}", static string (string name, [ReplOption] bool force) => name).WithDescription("Run.");
+		var input = "run zo " + string.Join(' ', Enumerable.Range(0, 200).Select(static i => $"a{i}"));
+
+		var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+		var result = await ResolveAutocompleteAsync(sut, input).ConfigureAwait(false);
+		stopwatch.Stop();
+
+		result.TokenClassifications!.Should().HaveCount(202);
+		stopwatch.ElapsedMilliseconds.Should().BeLessThan(500,
+			because: "classification resolves once, not once per token");
+	}
+
+	[TestMethod]
+	[Description("The '--output:<format>' transformer selector is offered for a differently-cased prefix ('--output:J' offers '--output:json'), because transformer names resolve through a case-insensitive dictionary like output aliases.")]
+	public async Task When_OutputSelectorPrefixIsDifferentlyCased_Then_TransformerIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.OptionCaseSensitivity = ReplCaseSensitivity.CaseSensitive);
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+
+		var result = await ResolveAutocompleteAsync(sut, "show --output:J").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("--output:json",
+			because: "transformer names resolve case-insensitively");
+	}
+
+	[TestMethod]
+	[Description("Shell completion preserves case-distinct executable option aliases under case-sensitive option parsing: a route with '-m' and '-M' bound to different parameters offers BOTH, rather than collapsing them with a case-insensitive dedupe.")]
+	public void When_OptionsAreCaseSensitive_Then_ShellKeepsBothCaseDistinctAliases()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.OptionCaseSensitivity = ReplCaseSensitivity.CaseSensitive);
+		sut.Map(
+				"run",
+				static string ([ReplOption(Aliases = ["-m"])] string? mode, [ReplOption(Aliases = ["-M"])] string? mask) => mode ?? mask ?? "none")
+			.WithDescription("Run.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app run -";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().Contain("-m").And.Contain("-M",
+			because: "case-sensitive parsing binds -m and -M to different parameters; both are executable");
+	}
+
+	[TestMethod]
+	[Description("A later '--' separator must not retroactively invalidate an earlier option: in 'run --force --' the '--force' before the separator stays classified as a Parameter (option), because it executes with force=true.")]
+	public async Task When_SeparatorFollowsAnOption_Then_EarlierOptionStaysClassifiedAsOption()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption] bool force) => force.ToString()).WithDescription("Run.");
+		const string input = "run --force --";
+
+		var result = await ResolveAutocompleteAsync(sut, input).ConfigureAwait(false);
+
+		var forceStart = input.IndexOf("--force", StringComparison.Ordinal);
+		result.TokenClassifications!.Single(c => c.Start == forceStart).Kind
+			.Should().Be(ConsoleLineReader.AutocompleteSuggestionKind.Parameter,
+				because: "an option before the '--' separator remains a valid option");
+	}
+
+	[TestMethod]
+	[Description("Global-option marking uses the parser's exact consumed indices, not string matching: with a valued global '--tenant' and a 'show' command, 'show' is the command and the tenant VALUE 'show' is the consumed global value — the second 'show' classifies as Command, not the first.")]
+	public async Task When_GlobalValueEqualsCommandName_Then_TheCommandTokenClassifiesAsCommand()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant"));
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+		const string input = "--tenant show show";
+
+		var result = await ResolveAutocompleteAsync(sut, input).ConfigureAwait(false);
+
+		// The command token is the SECOND "show"; the first is the tenant value.
+		var secondShowStart = input.LastIndexOf("show", StringComparison.Ordinal);
+		result.TokenClassifications!.Single(c => c.Start == secondShowStart).Kind
+			.Should().Be(ConsoleLineReader.AutocompleteSuggestionKind.Command,
+				because: "the parser consumes the first 'show' as the tenant value and the second as the command");
+	}
+
+	[TestMethod]
+	[Description("A pending valued global option suppresses command suggestions: after '--tenant ' the current token is the tenant VALUE, so completing a root command ('install') would misparse as the value — no command names are offered.")]
+	public async Task When_ValuedGlobalOptionAwaitsValue_Then_NoCommandIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant"));
+		sut.Map("install {skillName}", static string (string skillName) => skillName).WithDescription("Install.");
+
+		// Partial command token so the assertion is discriminating: a valued global suppresses
+		// it (it is the tenant value), whereas a bool global would still complete it.
+		var result = await ResolveAutocompleteAsync(sut, "--tenant ins").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().NotContain("install",
+			because: "the token after a pending valued global is its value, not a command");
+	}
+
+	[TestMethod]
+	[Description("A pending valued route option suppresses option-name suggestions: after 'run --channel -' the current token is --channel's VALUE, so offering '--force' would misparse (--channel left without a value) — no option names are offered.")]
+	public async Task When_ValuedRouteOptionAwaitsValue_Then_NoOptionNameIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption] string? channel, [ReplOption] bool force) => channel ?? "none")
+			.WithDescription("Run.");
+
+		var result = await ResolveAutocompleteAsync(sut, "run --channel -").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().NotContain("--force",
+			because: "the token after a pending valued route option is its value, not another option");
+	}
+
+	[TestMethod]
+	[Description("Built-in result-flow options are offered in completion: typing '--res' surfaces '--result:page-size' and friends, matching what GlobalOptionParser accepts and help documents.")]
+	public async Task When_ResultFlowPrefixIsTyped_Then_ResultFlowOptionsAreSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+
+		var result = await ResolveAutocompleteAsync(sut, "show --res").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static s => s.Value).ToArray();
+		values.Should().Contain("--result:page-size").And.Contain("--result:cursor");
+	}
+
+	[TestMethod]
+	[Description("A pending valued built-in result-flow option suppresses command suggestions: after '--result:page-size ' the current token is its value, so a root command must not be offered (it would be consumed as the page size).")]
+	public async Task When_ResultFlowOptionAwaitsValue_Then_NoCommandIsSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("install {skillName}", static string (string skillName) => skillName).WithDescription("Install.");
+
+		var result = await ResolveAutocompleteAsync(sut, "--result:page-size install").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().NotContain("install",
+			because: "the token after a pending valued result-flow option is its value");
+	}
+
+	[TestMethod]
+	[Description("A pending valued global does NOT suppress completion when the current token is option-like: '--tenant --' offers options, because the global parser does not consume a dash-prefixed token as the value ('--tenant' takes its fallback and '--' is a separate token).")]
+	public async Task When_ValuedGlobalIsFollowedByOptionPrefix_Then_OptionsAreStillOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant"));
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+
+		var result = await ResolveAutocompleteAsync(sut, "--tenant --").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("--help",
+			because: "'--tenant' cannot consume the option-like '--' as its value, so options remain available");
+	}
+
+	[TestMethod]
+	[Description("A pending valued route option that sits BEFORE a POSIX '--' does not suppress positional completion after the separator: 'deploy x -- -' completes the target value, because tokens after '--' are positional, not options awaiting a value.")]
+	public async Task When_SeparatorFollowsPendingLikeToken_Then_PositionalProviderStillRuns()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("deploy {target}", static string (string target, [ReplOption] string? channel) => target)
+			.WithCompletion("target", static (_, _, _) => ValueTask.FromResult<IReadOnlyList<string>>(["zo-profile"]))
+			.WithDescription("Deploy.");
+
+		var result = await ResolveAutocompleteAsync(sut, "deploy x -- -").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("zo-profile",
+			because: "after '--' the token is positional; no route option is pending a value");
+	}
+
+	[TestMethod]
+	[Description("Shell parity: a pending valued route option suppresses option-name candidates in shell completion too — 'app run --channel -' must not offer '-f' (which would leave --channel without a value).")]
+	public void When_ShellRouteOptionAwaitsValue_Then_NoOptionNameIsOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption(Aliases = ["-f"])] bool force, [ReplOption] string? channel) => channel ?? "none")
+			.WithDescription("Run.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app run --channel -";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().NotContain("-f", because: "--channel awaits a value; offering another option would misparse");
+	}
+
+	[TestMethod]
+	[Description("Shell parity: a pending valued global suppresses candidates in shell completion too — after 'app run --mode --tenant ' no enum value for --mode is offered (it would be consumed as the tenant value).")]
+	public void When_ShellGlobalAwaitsValue_Then_NoStaleEnumIsOffered()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<string>("tenant"));
+		sut.Map("run", static string ([ReplOption(Aliases = ["-m"])] ProbeMode mode) => mode.ToString())
+			.WithDescription("Run.");
+		var shellEngine = new ShellCompletionEngine(sut);
+		const string line = "app run --mode --tenant ";
+
+		var candidates = shellEngine.ResolveShellCompletionCandidates(line, line.Length);
+
+		candidates.Should().NotContain("Debug", because: "--tenant awaits its value; --mode's enum values must not leak here");
+	}
+
+	[TestMethod]
+	[Description("A pending valued option still offers its VALUE completions: with a WithCompletion provider on the option's target, 'run --channel ' returns the provider's values — only command and option-NAME candidates are suppressed, not the option's own value provider.")]
+	public async Task When_ValuedRouteOptionAwaitsValue_Then_ItsProviderStillCompletes()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("run", static string ([ReplOption] string? channel) => channel ?? "none")
+			.WithCompletion("channel", static (_, _, _) => ValueTask.FromResult<IReadOnlyList<string>>(["alpha", "beta"]))
+			.WithDescription("Run.");
+
+		var result = await ResolveAutocompleteAsync(sut, "run --channel ").ConfigureAwait(false);
+
+		var values = result.Suggestions.Select(static s => s.Value).ToArray();
+		values.Should().Contain("alpha").And.Contain("beta",
+			because: "the pending option's value provider must still run — only command/option names are suppressed");
+	}
+
+	[TestMethod]
+	[Description("A BOOL global is not a pending value, so a command still completes after it: '--verbose sh' offers 'show' (the bool consumes nothing). Resolving through the parser's token map — not an independent definition scan — keeps completion's verdict identical to the parser's even when aliases collide.")]
+	public async Task When_TokenResolvesToBoolGlobal_Then_CommandStillCompletes()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Options(options => options.Parsing.AddGlobalOption<bool>("verbose"));
+		sut.Map("show", static string () => "ok").WithDescription("Show.");
+
+		var result = await ResolveAutocompleteAsync(sut, "--verbose sh").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().Contain("show",
+			because: "a bool global consumes no value, so the following partial token still completes as a command");
+	}
+
+	[TestMethod]
+	[Description("No subcommand is offered once a route option has been typed: for routes 'parent' ([ReplOption] bool force) and 'parent child', 'parent --force c' must not suggest 'child' — the option occupies the segment position and a bool flag would swallow the word, so the child route is unreachable there.")]
+	public async Task When_OptionPrecedesSubcommandPosition_Then_SubcommandIsNotSuggested()
+	{
+		var sut = CoreReplApp.Create();
+		sut.Map("parent", static string ([ReplOption] bool force) => force.ToString()).WithDescription("Parent.");
+		sut.Map("parent child", static string () => "child").WithDescription("Child.");
+
+		var result = await ResolveAutocompleteAsync(sut, "parent --force c").ConfigureAwait(false);
+
+		result.Suggestions.Select(static s => s.Value).Should().NotContain("child",
+			because: "an option already occupies the position; 'parent --force child' would not invoke the child route");
+	}
+
+private enum ProbeMode
+	{
+		Debug,
+		Release,
+	}
+
+	private static async Task<ConsoleLineReader.AutocompleteResult> ResolveAutocompleteAsync(CoreReplApp app, string input)
+	{
+		var result = await app.Autocomplete.ResolveAutocompleteAsync(
+			new ConsoleLineReader.AutocompleteRequest(input, input.Length, MenuRequested: true),
+			scopeTokens: [],
+			EmptyServiceProvider.Instance,
+			CancellationToken.None)
+			.ConfigureAwait(false);
+		result.Should().NotBeNull();
+		return result!.Value;
+	}
+
+	private sealed class EmptyServiceProvider : IServiceProvider
+	{
+		public static readonly EmptyServiceProvider Instance = new();
+		public object? GetService(Type serviceType) => null;
+	}
+}
