onto-mcp 0.4.13 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,11 +65,18 @@ Output formats are shared: `lens-sidecar`, `issue-artifact`,
65
65
  ### 3.1 Adapter-specific schema delivery
66
66
 
67
67
  - `codex_cli`: the schema is written to a file and passed via `codex exec --output-schema`.
68
- - `claude_code`: the schema is embedded in the prompt; `claude --json-schema` is
69
- **not** used because its validator silently rejects the runtime's complex
70
- submit schemas (`additionalProperties:false` + deep `required`) and exits with
71
- no output. The in-process submit tool remains the authoritative validator, so
72
- schema enforcement is preserved regardless of delivery mechanism.
68
+ - `claude_code`: `claude --json-schema <schema>` **is** used — it constrains
69
+ output at the API level (grammar-based structured outputs) so the model cannot
70
+ emit syntactically malformed JSON, and the CLI returns a parsed
71
+ `structured_output` object. The earlier claim here that its validator "silently
72
+ rejects the runtime's complex submit schemas" was **inaccurate**: empirically
73
+ (opus-4.8 + haiku, 2026-07) the flag accepts the runtime's submit schemas
74
+ (`additionalProperties:false` + deep `required`, ~20 params, enums) and returns
75
+ native objects; without it, opus free-forms the JSON in-prompt and can drop a
76
+ comma on large findings → an unrecoverable `output_contract` failure. The
77
+ in-prompt schema remains as a redundant semantic hint, and the in-process submit
78
+ tool remains the authoritative validator of field CONTENT — so schema
79
+ enforcement holds regardless of delivery mechanism.
73
80
 
74
81
  ### 3.2 Result extraction
75
82
 
package/README.md CHANGED
@@ -83,6 +83,19 @@ npm exec -- onto mcp
83
83
  Before running reviews, configure an LLM provider in `.onto/settings.json` or
84
84
  `~/.onto/settings.json` (see [Configuration](#configuration)).
85
85
 
86
+ ## onto CLI
87
+
88
+ The `onto` binary exposes a small set of commands; the actual product work is
89
+ driven through the MCP tools by your host.
90
+
91
+ | Command | What it does |
92
+ |---|---|
93
+ | `onto mcp` | Start the MCP stdio tool server — each MCP host launches this (see [Quickstart](#quickstart)) |
94
+ | `onto register` | Register the server into supported MCP hosts (see [Quickstart](#quickstart)) |
95
+ | `onto configure-provider` | Write LLM provider/model settings into the settings.json chain (see [Configuration](#configuration)) |
96
+ | `onto seats` | Print a read-only inventory of every LLM model seat the runtime can dispatch, resolved against the settings.json chain (`--json` for machine output); writes nothing |
97
+ | `onto watch [session]` | Open a live, read-only TUI over a review/reconstruct session — pass a session id substring or path, or omit it for the most recent (see [Observing a run](#observing-a-run)) |
98
+
86
99
  ## What it does
87
100
 
88
101
  ### Review
@@ -159,16 +172,12 @@ point-in-time map (v0.4.7 snapshot, not maintained) is kept at
159
172
  | `onto_review_round` | Host-orchestration: return the units ready to execute now, with prompt packets materialized |
160
173
  | `onto_review_advance` | Host-orchestration: report host-executed units; onto validates seats, records results, and returns the next round |
161
174
  | `onto_review_cancel` | Request cancellation for a running review session |
162
- | `onto_review_status` | Read structured status and artifact refs |
163
- | `onto_review_result` | Read bounded result projections; `compact`/`standard` use count-and-signal summaries, `full` includes `review-record.yaml` and final output |
164
- | `onto_list_lenses` | List canonical lens sets |
165
- | `onto_list_domains` | List available domain ids |
166
- | `onto_list_source_profiles` | List reconstruct source profiles |
175
+ | `onto_review_read` | Read a review session — one entry point for liveness while running and the bounded result once complete; `projectionLevel` `compact`/`standard`/`full` (`full` adds `review-record.yaml` and final output) |
176
+ | `onto_list` | List a registry by `kind`: `lenses` (canonical lens sets), `domains` (available domain ids), or `source_profiles` (reconstruct source profiles) |
167
177
  | `onto_observe_source` | Materialize reconstruct material profile, inventory, source observations, and initial record |
168
178
  | `onto_validate_reconstruct_directive` | Validate LLM-authored reconstruct artifacts |
169
179
  | `onto_reconstruct` | Run the material-aware direct-call reconstruct path with runtime validation gates |
170
- | `onto_reconstruct_status` | Read reconstruct session status, progress, counts, and artifact refs |
171
- | `onto_reconstruct_result` | Read `reconstruct-record.yaml`, run manifest, progress projection, and final output |
180
+ | `onto_reconstruct_read` | Read a reconstruct session stage progress, liveness, and counts, or the full record, run manifest, and final output at `projectionLevel=full` |
172
181
 
173
182
  MCP results include `llmPresentation` prompts: the runtime supplies bounded
174
183
  facts, and the host LLM uses those prompts to explain the opening brief and
@@ -185,17 +194,22 @@ onto without external docs:
185
194
  `intent`, `reviewMode`) and `reconstruct_seed` (args: `targetRefs`,
186
195
  `intent`).
187
196
 
188
- `onto_review_status` accepts `projectionLevel` (`compact` | `standard` |
197
+ `onto_review_read` accepts `projectionLevel` (`compact` | `standard` |
189
198
  `full`); use `compact` in token-limited hosts.
190
199
 
191
- ### Runtime watcher
200
+ ### Observing a run
201
+
202
+ `onto watch` opens a live, read-only TUI over a review or reconstruct session:
203
+ browse the workflow tree, node details, and log. Pass a session id substring or
204
+ a session path, or omit it to attach to the most recent session
205
+ (`--project-root` selects where to look for `.onto/{review,reconstruct}`). It
206
+ observes only — it writes nothing and drives nothing.
192
207
 
193
- When a review or reconstruct run starts, the runtime writes a session-local
194
- `runtime-events.ndjson` stream and tries to open a live watcher in a supported
195
- terminal (tmux, Warp, Cursor, iTerm2, Apple Terminal, Codex Desktop with a
196
- configured launcher). Set `ONTO_RUNTIME_WATCHER=0` to disable, or
197
- `ONTO_RUNTIME_WATCHER_COMMAND` with a `{watcherCommand}` template for
198
- unsupported hosts.
208
+ The runtime also writes a session-local `runtime-events.ndjson` stream and tries
209
+ to open a watcher automatically in a supported terminal (tmux, Warp, Cursor,
210
+ iTerm2, Apple Terminal, Codex Desktop with a configured launcher). Set
211
+ `ONTO_RUNTIME_WATCHER=0` to disable, or `ONTO_RUNTIME_WATCHER_COMMAND` with a
212
+ `{watcherCommand}` template for unsupported hosts.
199
213
 
200
214
  ## Configuration
201
215
 
@@ -322,8 +336,10 @@ onto configure-provider --provider anthropic --model claude-opus-4-8 --auth oaut
322
336
  - `--project`는 프로젝트 seat(`.onto/settings.json`)에, 생략하면 사용자 seat
323
337
  (`~/.onto/settings.json`)에 기록합니다.
324
338
  - 지원 model id는 [`.onto/authority/supported-models.yaml`](.onto/authority/supported-models.yaml)가
325
- authority입니다(현재 anthropic: `claude-opus-4-8`; `claude-sonnet-5`는
326
- `semantic_map_synthesize` 역할 전용).
339
+ authority입니다. 범용 seat은 `gpt-5.5`(openai)·`claude-opus-4-8`(anthropic)이고,
340
+ 역할 전용으로 `review`에 `gpt-5.6-sol`·`claude-fable-5`, `semantic_map_synthesize`에
341
+ `gpt-5.6-luna`·`claude-sonnet-5`가 인증돼 있습니다. 현재 프로젝트에서 실제로
342
+ 해석되는 seat은 `onto seats`로 확인하세요.
327
343
  - 이 명령은 actor 블록만 기록합니다. `units[].llm.model`로 unit별 model을 고정해
328
344
  두었다면(위 정적 프로필 예시가 그렇습니다) 같은 model로 바꾸거나 그 `model` 키를
329
345
  지워 actor 값을 상속하게 하세요.
@@ -13,7 +13,8 @@ import { RECONSTRUCT_SEMANTIC_AUTHOR_MAX_BASE_OUTPUT_TOKENS, } from "../core-run
13
13
  // allowlisted in scripts/check-import-boundary.ts). Active only when
14
14
  // ONTO_LLM_MOCK=1; mock runs record mock actor ids in the run manifest.
15
15
  import { RECONSTRUCT_MOCK_AUTHOR_ID, RECONSTRUCT_MOCK_CONFIRMATION_PROVIDER_ID, callReconstructMockLlm, isReconstructMockLlmRealizationEnabled, } from "../core-runtime/reconstruct/mock-llm-realization.js";
16
- import { assertSettingsModelsSupported, completeDispatchBreakerSettings, isReconstructSemanticMapAuthoringEnabled, resolveOptionalReconstructActorLlmSettings, resolveSettingsChain, resolveReconstructActorLlmSettings, resolveReconstructSemanticAuthorLlmRuntimeSettings, } from "../core-runtime/discovery/settings-chain.js";
16
+ import { assertSettingsModelsSupported, completeDispatchBreakerSettings, isReconstructSemanticMapAuthoringEnabled, resolveOptionalReconstructActorLlmSettings, resolveSettingsChain, resolveReconstructActorLlmSettings, resolveReconstructSemanticAuthorLlmRuntimeSettings, PerCallLlmOverrideSchema, } from "../core-runtime/discovery/settings-chain.js";
17
+ import { applyReconstructLlmOverride } from "../core-runtime/discovery/llm-override.js";
17
18
  import { assertDispatchFallbackSessionAdmission } from "../core-runtime/reconstruct/dispatch-fallback-artifacts.js";
18
19
  import { normalizeLlmModelSwitcher, } from "../core-runtime/llm/model-switcher.js";
19
20
  import { resolveOntoHome, } from "../core-runtime/discovery/onto-home.js";
@@ -329,13 +330,12 @@ export function resolveSemanticMapSynthesizeWiring(args) {
329
330
  }
330
331
  return {
331
332
  enableSemanticMapAuthoring: true,
333
+ // Effort comes from the seat's own (already overlaid) llm block —
334
+ // resolveLlmProviderConfig reads config.llm.effort (design v4 §6(a)).
332
335
  semanticMapSynthesizeLlmConfig: args.mockRealizationEnabled
333
336
  ? synthesizeSeatIdentityProjection(seat)
334
337
  : resolveLlmProviderConfig({
335
338
  config: { llm: seat },
336
- ...(args.llmEffortOverride
337
- ? { cliOverrides: args.llmEffortOverride }
338
- : {}),
339
339
  }),
340
340
  };
341
341
  }
@@ -577,7 +577,32 @@ export function createOntoReconstructCoreApi(options = {}) {
577
577
  ...(ontoHome ? { ontoHome } : {}),
578
578
  });
579
579
  const targetRefs = request.targetRefs.map((targetRef) => resolveFromBase(projectRoot, targetRef));
580
- const settings = await resolveSettingsChain(ontoHome ?? projectRoot, projectRoot);
580
+ // Per-call LLM override (design v4): overlay the resolved settings ONCE,
581
+ // here, so every downstream consumer (dispatch-fallback admission, the
582
+ // supported-model gate, actor/judge provider resolution, census,
583
+ // provenance) reads the SAME effective settings. Identity when absent →
584
+ // byte-identical default-off.
585
+ // The strict override schema IS the security boundary (it excludes
586
+ // base_url/api_key_env/timeout_ms so a per-call input cannot pick an
587
+ // endpoint or credential env). The MCP handler validates, but this Core
588
+ // API is also called programmatically, where a static type enforces
589
+ // nothing at runtime — so re-parse here, at admission, and overlay ONLY
590
+ // the parsed value.
591
+ const llmOverride = request.llmOverride === undefined
592
+ ? undefined
593
+ : PerCallLlmOverrideSchema.parse(request.llmOverride);
594
+ const baseSettings = await resolveSettingsChain(ontoHome ?? projectRoot, projectRoot);
595
+ // design v4 §2.5: `judgeModel` names a model ON THE AUTHOR'S PROVIDER, so
596
+ // combining it with an override that switches that provider is ambiguous —
597
+ // the judge would silently resolve (or degrade) on the switched-in
598
+ // provider, reintroducing the "believe X, run Y" failure the override
599
+ // contract forbids. Reject at the handler, before any dispatch.
600
+ const settings = applyReconstructLlmOverride(baseSettings, llmOverride);
601
+ const authorProviderBefore = baseSettings.reconstruct?.execution?.actors?.semantic_author?.llm?.provider;
602
+ const authorProviderAfter = settings.reconstruct?.execution?.actors?.semantic_author?.llm?.provider;
603
+ if (request.judgeModel && authorProviderAfter !== authorProviderBefore) {
604
+ throw new Error(`llmOverride switches the semantic author's provider (${String(authorProviderBefore)} → ${String(authorProviderAfter)}) while judgeModel="${request.judgeModel}" names a model on the author's provider. This combination is ambiguous: drop judgeModel, or keep the author's provider.`);
605
+ }
581
606
  await assertDispatchFallbackSessionAdmission({
582
607
  sessionRoot,
583
608
  enabled: settings.reconstruct?.execution?.dispatch_fallback?.enabled === true,
@@ -598,9 +623,9 @@ export function createOntoReconstructCoreApi(options = {}) {
598
623
  // Mock realization needs no provider config: actor llm settings stay
599
624
  // required only for live direct_call execution, and the recorded route
600
625
  // comes from the mock result marker, not from a configured provider.
601
- const llmEffortOverride = request.llmEffort
602
- ? { reasoning_effort: request.llmEffort }
603
- : undefined;
626
+ // The author-side effort is carried on each actor's (already overlaid) llm
627
+ // block resolveLlmProviderConfig reads config.llm.effort — so no separate
628
+ // effort-override plumbing is needed (design v4 §6(a)).
604
629
  // The semantic_author actor settings (non-mock). The MODEL provider here
605
630
  // (e.g. "openai") is the supported-models registry key, DISTINCT from the
606
631
  // resolved RUNTIME provider (openai OAuth resolves to "codex"). The document
@@ -612,7 +637,6 @@ export function createOntoReconstructCoreApi(options = {}) {
612
637
  let semanticAuthorLlmConfig = semanticAuthorActorLlm
613
638
  ? resolveLlmProviderConfig({
614
639
  config: { llm: semanticAuthorActorLlm },
615
- ...(llmEffortOverride ? { cliOverrides: llmEffortOverride } : {}),
616
640
  })
617
641
  : {};
618
642
  const confirmationProviderLlmConfig = mockRealizationEnabled
@@ -621,7 +645,6 @@ export function createOntoReconstructCoreApi(options = {}) {
621
645
  config: {
622
646
  llm: resolveReconstructActorLlmSettings(settings, "confirmation_provider"),
623
647
  },
624
- ...(llmEffortOverride ? { cliOverrides: llmEffortOverride } : {}),
625
648
  });
626
649
  // The supported-models registry (non-mock only): loaded once and shared by
627
650
  // the judge-override support check and the document projection-budget
@@ -649,13 +672,12 @@ export function createOntoReconstructCoreApi(options = {}) {
649
672
  // Semantic-map authoring opt-in + optional synthesize seat (INV-MODEL-1
650
673
  // role-aware design §5.4/§5.5): ONE deterministic seam owns the wiring
651
674
  // (resolveSemanticMapSynthesizeWiring) — live completes the seat into a
652
- // provider config (own auth/adapter, cross-provider; the request
653
- // llmEffort pin applies as cliOverrides, so pin > seat effort), mock
654
- // takes an identity-only projection (no auth material required).
675
+ // provider config (own auth/adapter, cross-provider; effort comes from the
676
+ // seat's own already overlaid llm block), mock takes an identity-only
677
+ // projection (no auth material required).
655
678
  const semanticMapWiring = resolveSemanticMapSynthesizeWiring({
656
679
  settings,
657
680
  mockRealizationEnabled,
658
- llmEffortOverride,
659
681
  });
660
682
  // Single seed-stage document projection budget (chars), derived once from
661
683
  // the semantic author's MODEL window.
@@ -687,18 +709,19 @@ export function createOntoReconstructCoreApi(options = {}) {
687
709
  if (!semanticMapWiring.enableSemanticMapAuthoring) {
688
710
  throw new Error("reconstruct dispatch_fallback requires reconstruct.execution.semantic_map_authoring=true.");
689
711
  }
690
- const effectiveEffort = request.llmEffort;
712
+ // Effort is carried on each (already overlaid) llm block: the primary
713
+ // seats via the semantic_map_synthesize/semantic_author actors, the
714
+ // fallback via dispatch_fallback.llm (applyReconstructLlmOverride threads
715
+ // override.effort into it). No separate request-effort pin (design v4 §6(a)).
716
+ // Fresh copies keep these dispatch inputs independent of the shared settings.
691
717
  const primarySynthesizeLlm = {
692
718
  ...(resolveOptionalReconstructActorLlmSettings(settings, "semantic_map_synthesize") ?? resolveReconstructActorLlmSettings(settings, "semantic_author")),
693
- ...(effectiveEffort ? { effort: effectiveEffort } : {}),
694
719
  };
695
720
  const primaryVerifyLlm = {
696
721
  ...resolveReconstructActorLlmSettings(settings, "semantic_author"),
697
- ...(effectiveEffort ? { effort: effectiveEffort } : {}),
698
722
  };
699
723
  const fallbackLlm = {
700
724
  ...dispatchFallbackSettings.llm,
701
- ...(effectiveEffort ? { effort: effectiveEffort } : {}),
702
725
  };
703
726
  const [primarySynthesize, primaryVerify, fallbackSynthesize, fallbackVerify] = await Promise.all([
704
727
  tryCreateEligiblePrimarySealedDispatchCapability({
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { resolveOntoHome } from "../core-runtime/discovery/onto-home.js";
8
8
  import { completeDispatchBreakerSettings, defaultReviewRetrySettings, resolveSettingsChain, } from "../core-runtime/discovery/settings-chain.js";
9
+ import { applyReviewLlmOverride } from "../core-runtime/discovery/llm-override.js";
9
10
  import { loadCoreLensRegistry } from "../core-runtime/discovery/lens-registry.js";
10
11
  import { fileExists, isDeprecatedDomainAlias, isoFromTimestamp, isoNow, normalizeDomainValue, readYamlDocument, writeYamlDocument, } from "../core-runtime/review/review-artifact-utils.js";
11
12
  import { assertPathInsideRoot, realpathIfExists, } from "../core-runtime/path-boundary.js";
@@ -2448,7 +2449,11 @@ function reviewExecutionProfileFromActorProfiles(args) {
2448
2449
  return reconstructed;
2449
2450
  }
2450
2451
  async function projectReviewExecutionProfileForContinuation(args) {
2451
- const settings = await resolveSettingsChain(args.ontoHome, args.projectRoot);
2452
+ const projectSettings = await resolveSettingsChain(args.ontoHome, args.projectRoot);
2453
+ // Re-apply the session's per-call override (design v4 §7) so the unit
2454
+ // execution policy this profile carries stays on the overridden route rather
2455
+ // than reverting to the non-overlaid project models. Identity when absent.
2456
+ const settings = applyReviewLlmOverride(projectSettings, args.llmOverride);
2452
2457
  const resolved = resolveReviewExecutionProfile({
2453
2458
  explicitCodex: false,
2454
2459
  settings,
@@ -3116,6 +3121,9 @@ export function createOntoReviewCoreApi(options = {}) {
3116
3121
  : await projectReviewExecutionProfileForContinuation({
3117
3122
  projectRoot,
3118
3123
  ontoHome,
3124
+ ...(executionPlan.llm_override
3125
+ ? { llmOverride: executionPlan.llm_override }
3126
+ : {}),
3119
3127
  });
3120
3128
  const actorProfileReviewExecutionProfile = manifestReviewExecutionProfile
3121
3129
  ? undefined
@@ -11,6 +11,7 @@ import { printOntoReleaseChannelNotice } from "../release-channel/release-channe
11
11
  import { detectCodexEnvSignal } from "../discovery/host-detection.js";
12
12
  import { resolveOntoHome } from "../discovery/onto-home.js";
13
13
  import { resolveSettingsChain } from "../discovery/settings-chain.js";
14
+ import { applyReviewLlmOverride, parsePerCallLlmOverrideArg, } from "../discovery/llm-override.js";
14
15
  /**
15
16
  * Stage 1 — resolve the window-proportional review prompt budget multiplier from
16
17
  * the lens model's registered context window. The registry load and model
@@ -178,6 +179,7 @@ export async function runPrepareReviewSessionCli(argv) {
178
179
  "max-listing-depth": { type: "string" },
179
180
  "max-listing-entries": { type: "string" },
180
181
  "max-embed-lines": { type: "string" },
182
+ "llm-override": { type: "string" },
181
183
  },
182
184
  strict: true,
183
185
  allowPositionals: false,
@@ -195,7 +197,16 @@ export async function runPrepareReviewSessionCli(argv) {
195
197
  const executionRealization = resolveExecutionRealization(argv, values["execution-realization"] ?? values["execution-mode"], hostRuntime);
196
198
  const projectRoot = path.resolve(requireString(values["project-root"], "project-root"));
197
199
  const ontoHome = resolveOntoHome(optionalString(values["onto-home"]) || undefined);
198
- const ontoConfig = await resolveSettingsChain(ontoHome, projectRoot);
200
+ // Per-call LLM override (design v4): overlay the resolved settings so the
201
+ // baked artifacts (session-metadata resolved_llm_plan, actor-invocation
202
+ // profiles) carry the effective actor LLM — this is what onto_review_continue
203
+ // replays. Same overlay applied at the orchestrator profile seam
204
+ // (resolveReviewInvokeSetup); identity when absent → byte-identical default-off.
205
+ const overlay = parsePerCallLlmOverrideArg(values["llm-override"]);
206
+ const resolvedConfig = await resolveSettingsChain(ontoHome, projectRoot);
207
+ const ontoConfig = overlay
208
+ ? applyReviewLlmOverride(resolvedConfig, overlay)
209
+ : resolvedConfig;
199
210
  // Stage 1 — resolve the window-proportional review prompt budget ONCE, here,
200
211
  // and thread it to both consumers: the spreadsheet inventory prompt caps
201
212
  // (into the materialize call) and the embed line budget (persisted into the
@@ -209,6 +220,10 @@ export async function runPrepareReviewSessionCli(argv) {
209
220
  const bindingParams = {
210
221
  projectRoot,
211
222
  ontoConfig,
223
+ // Stamp the RAW override into the execution plan too: ontoConfig carries its
224
+ // effect on the actors, but continuation re-resolves the project profile for
225
+ // unit execution policy and must re-overlay it (design v4 §7).
226
+ ...(overlay ? { llmOverride: overlay } : {}),
212
227
  requestedTarget: requireString(values["requested-target"], "requested-target"),
213
228
  requestedDomainToken: optionalString(values["requested-domain-token"]),
214
229
  targetScopeKind,
@@ -57,6 +57,12 @@ export function appendReviewInvocationRequestArgs(args, request, options) {
57
57
  if (request.confirmValueAlignment) {
58
58
  result.push("--confirm-value-alignment");
59
59
  }
60
+ if (request.llmOverride) {
61
+ // Serialize the per-call override as a single JSON passthrough arg — it must
62
+ // survive into the start argv so both the orchestrator profile resolve and
63
+ // the prepare-session bake apply the same overlay (design v4 §2.3).
64
+ result.push("--llm-override", JSON.stringify(request.llmOverride));
65
+ }
60
66
  return result;
61
67
  }
62
68
  function executorRealizationFromRequest(request) {
@@ -8,7 +8,8 @@ import { generateReviewSessionId } from "../review/materializers.js";
8
8
  import { fileExists, hasOptionFlag, isDeprecatedDomainAlias, normalizeDomainValue, readMultiOptionValuesFromArgv, readYamlDocument, readSingleOptionValueFromArgv, } from "../review/review-artifact-utils.js";
9
9
  import { isOntoRoot, resolveOntoHome } from "../discovery/onto-home.js";
10
10
  import { resolveInstallationPath } from "../discovery/installation-paths.js";
11
- import { REVIEW_EXECUTION_UNIT_IDS, resolveSettingsChain, } from "../discovery/settings-chain.js";
11
+ import { assertSettingsModelsSupported, REVIEW_EXECUTION_UNIT_IDS, resolveSettingsChain, } from "../discovery/settings-chain.js";
12
+ import { applyReviewLlmOverride, parsePerCallLlmOverrideArg, } from "../discovery/llm-override.js";
12
13
  import { loadCoreLensRegistry } from "../discovery/lens-registry.js";
13
14
  import { isPathInsideRoot, isPathInsideRootRealpathAwareSync, } from "../path-boundary.js";
14
15
  import { isDirectModelCallSelection, isExternalOauthWorkerSelection, normalizeLlmModelSwitcher, } from "../llm/model-switcher.js";
@@ -61,6 +62,9 @@ const KNOWN_PASSTHROUGH_OPTION_NAMES = [
61
62
  "max-listing-depth",
62
63
  "max-listing-entries",
63
64
  "max-embed-lines",
65
+ // Per-call LLM override JSON — passthrough so it reaches the prepare-session
66
+ // bake (prepare-review-session.ts) as well as the orchestrator profile resolve.
67
+ "llm-override",
64
68
  ];
65
69
  const KNOWN_PASSTHROUGH_FLAG_NAMES = [
66
70
  "codex",
@@ -2053,9 +2057,29 @@ export async function resolveReviewInvokeSetup(argv) {
2053
2057
  const ontoHomeFlag = readSingleOptionValueFromArgv(argv, "onto-home");
2054
2058
  const ontoHome = resolveOntoHome(ontoHomeFlag);
2055
2059
  const projectRoot = path.resolve(readSingleOptionValueFromArgv(argv, "project-root") ?? ".");
2056
- const ontoConfig = ontoHome
2060
+ const resolvedOntoConfig = ontoHome
2057
2061
  ? await resolveSettingsChain(ontoHome, projectRoot)
2058
2062
  : await readOntoConfig(projectRoot);
2063
+ // Per-call LLM override (design v4): overlay the resolved settings BEFORE the
2064
+ // execution profile is resolved, so the profile route/adapter/model reflects
2065
+ // the effective settings and flows into the spawn `--provider/--model/...`
2066
+ // flags. Identity when absent → default-off is byte-identical. When an override
2067
+ // IS present, fail loud (cause = override) if it selects an unsupported model
2068
+ // (INV-MODEL-1) — the one new review-side model-support gate; it stays scoped
2069
+ // to overrides so an existing (ungated) config's behavior is unchanged.
2070
+ const llmOverride = parsePerCallLlmOverrideArg(readSingleOptionValueFromArgv(argv, "llm-override"));
2071
+ const ontoConfig = llmOverride
2072
+ ? applyReviewLlmOverride(resolvedOntoConfig, llmOverride)
2073
+ : resolvedOntoConfig;
2074
+ if (llmOverride) {
2075
+ // Gate ONLY the review dispatch seats. The shared gate walks every seat in
2076
+ // the object it is handed, so passing the whole config would fail a review
2077
+ // because of an unrelated reconstruct model the review never dispatches.
2078
+ // Review resolves its route from `review.execution` actors + units alone
2079
+ // (llmRouteEntries), and salvage lives under review.execution.retry — so the
2080
+ // review block is exactly the set this run can dispatch.
2081
+ assertSettingsModelsSupported(ontoConfig.review ? { review: ontoConfig.review } : {});
2082
+ }
2059
2083
  const resolvedInvokeInputs = await resolveReviewInvokeInputs(argvWithSessionId, ontoConfig, projectRoot, sessionId, ontoHome);
2060
2084
  const { optionTokens: argvWithoutPositionals } = splitArgvIntoOptionsAndPositionals(argvWithSessionId, [...KNOWN_INVOKE_ONLY_OPTION_NAMES, ...KNOWN_PASSTHROUGH_OPTION_NAMES], [...KNOWN_INVOKE_ONLY_FLAG_NAMES, ...KNOWN_PASSTHROUGH_FLAG_NAMES]);
2061
2085
  const normalizedStartArgv = appendReviewInvokeDerivedArgs(stripOptionsFromArgv(argvWithoutPositionals, [...KNOWN_INVOKE_ONLY_OPTION_NAMES], [...KNOWN_INVOKE_ONLY_FLAG_NAMES]), resolvedInvokeInputs);