onto-mcp 0.4.11 → 0.4.13

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.
Files changed (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -17,8 +17,10 @@ import fs from "node:fs";
17
17
  import path from "node:path";
18
18
  import os from "node:os";
19
19
  import { DEFAULT_GROK_BASE_URL, DEFAULT_LMSTUDIO_BASE_URL, isExternalOauthWorkerSelection, normalizeLlmModelSwitcher, } from "./model-switcher.js";
20
+ import { resolveClaudeBin } from "./claude-bin.js";
20
21
  import { appendRuntimeModelCallLogFromCurrentContext, appendRuntimeStreamChunkFromCurrentContextSync, } from "../observability/runtime-stream-observation.js";
21
22
  import { callReviewMockLlm, isReviewMockLlmRealizationEnabled, } from "./mock-llm-realization.js";
23
+ import { OPENAI_RESPONSES_MAX_OUTPUT_TOKENS_FAILURE_CODE, OpenAIResponsesIncompleteError, resolveOpenAIResponsesOutputBudget, } from "./openai-responses-incomplete-error.js";
22
24
  /**
23
25
  * Bridge: OntoConfig + CLI overrides → Partial<LlmCallConfig> that callLlm consumes.
24
26
  *
@@ -66,20 +68,75 @@ export function resolveLlmProviderConfig(args) {
66
68
  out.service_tier = service_tier;
67
69
  if (api_key_env)
68
70
  out.api_key_env = api_key_env;
71
+ // Transport timeout carried on the llm block (not a model-switch axis).
72
+ if (config.llm?.timeout_ms)
73
+ out.timeout_ms = config.llm.timeout_ms;
69
74
  if (Object.keys(models_per_provider).length > 0) {
70
75
  out.models_per_provider = models_per_provider;
71
76
  }
72
77
  return out;
73
78
  }
79
+ export function applyOpenAIResponsesOutputHeadroom(args) {
80
+ if (args.headroomTokens === undefined)
81
+ return args.config;
82
+ if (args.selection.model_provider !== "openai" ||
83
+ args.selection.auth !== "api_key" ||
84
+ args.selection.execution_route !== "direct_model_call" ||
85
+ args.selection.execution_adapter !== "openai_sdk" ||
86
+ args.selection.wire_format !== "native_sdk" ||
87
+ args.selection.base_url !== undefined) {
88
+ throw new Error("openai Responses output headroom requires the official openai + api_key + openai_sdk + native_sdk route without a custom base_url.");
89
+ }
90
+ if (args.modelMaxOutputTokens === undefined) {
91
+ throw new Error("openai Responses output headroom requires registered max_output_tokens.");
92
+ }
93
+ resolveOpenAIResponsesOutputBudget({
94
+ baseOutputTokens: args.maxBaseOutputTokens,
95
+ headroomTokens: args.headroomTokens,
96
+ modelMaxOutputTokens: args.modelMaxOutputTokens,
97
+ });
98
+ return {
99
+ ...args.config,
100
+ openai_responses_output_headroom_tokens: args.headroomTokens,
101
+ openai_responses_model_max_output_tokens: args.modelMaxOutputTokens,
102
+ };
103
+ }
74
104
  // Phase 3 production found 30s too tight for large audit batches (37 items
75
105
  // could time out then SDK-retry for 90s total). 120s is generous
76
106
  // enough for ~50-item single-batch audits while still failing fast on real
77
107
  // network problems.
78
108
  const DEFAULT_TIMEOUT_MS = Number(process.env.ONTO_LLM_TIMEOUT_MS) || 120_000;
109
+ // External OAuth CLI workers (`codex exec`, `claude -p`) spawn a full agentic
110
+ // turn, not a single SDK request — a heavy single-turn authoring call (e.g.
111
+ // reconstruct seed authoring on a frontier model) routinely runs minutes. The
112
+ // 120s SDK default is too tight for that path and caused deterministic timeouts
113
+ // on the claude-opus-4-8 reconstruct route; give the CLI workers a longer
114
+ // default so supported models complete without an unencoded env override. The
115
+ // `ONTO_LLM_TIMEOUT_MS` override still applies to both paths when set.
116
+ const DEFAULT_WORKER_TIMEOUT_MS = Number(process.env.ONTO_LLM_TIMEOUT_MS) || 600_000;
117
+ // Grace period after a timeout SIGTERM before escalating to SIGKILL. A worker
118
+ // that traps/ignores SIGTERM would otherwise leave the run hanging indefinitely
119
+ // (the close-event promise never resolves); SIGKILL cannot be trapped, so this
120
+ // bounds the worst-case hang to timeout + grace.
121
+ const WORKER_SIGKILL_GRACE_MS = 5_000;
79
122
  // SDK auto-retry hides failures behind a long stall. We surface failures
80
123
  // faster (1 retry instead of the default 2) so operators see provider errors
81
124
  // within ~2× timeout instead of ~3×.
82
125
  const DEFAULT_MAX_RETRIES = 1;
126
+ const OPENAI_RESPONSES_BASE_URL = "https://api.openai.com/v1";
127
+ function sanitizeEffectiveBaseUrl(baseUrl) {
128
+ try {
129
+ const sanitized = new URL(baseUrl);
130
+ sanitized.username = "";
131
+ sanitized.password = "";
132
+ sanitized.search = "";
133
+ sanitized.hash = "";
134
+ return sanitized.toString();
135
+ }
136
+ catch {
137
+ return "invalid-endpoint:redacted";
138
+ }
139
+ }
83
140
  /**
84
141
  * Model-call observability — emits STDERR logs for each LLM API call, covering
85
142
  * (a) pre-call model_id + provider + max_tokens, (b) post-call usage on success,
@@ -213,7 +270,7 @@ function missingModelError(provider) {
213
270
  ].join("\n"));
214
271
  }
215
272
  function unsupportedReasoningEffortError(provider) {
216
- return new Error(`provider=${provider} cannot honor reasoning_effort; remove effort from this actor or switch to provider=openai/codex.`);
273
+ return new Error(`provider=${provider} cannot honor reasoning_effort; remove effort from this actor or switch to provider=openai/codex/anthropic.`);
217
274
  }
218
275
  function assertNoUnsupportedReasoningEffort(provider, reasoningEffort) {
219
276
  if (reasoningEffort) {
@@ -223,19 +280,47 @@ function assertNoUnsupportedReasoningEffort(provider, reasoningEffort) {
223
280
  // ---------------------------------------------------------------------------
224
281
  // Anthropic call
225
282
  // ---------------------------------------------------------------------------
226
- async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens) {
283
+ async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, reasoningEffort, thinkingMode) {
227
284
  const { default: Anthropic } = await import("@anthropic-ai/sdk");
228
285
  const client = new Anthropic({
229
286
  apiKey,
230
287
  timeout: DEFAULT_TIMEOUT_MS,
231
288
  maxRetries: DEFAULT_MAX_RETRIES,
232
289
  });
233
- emitModelCallLog(`anthropic call: model="${modelId}" max_tokens=${maxTokens}`);
290
+ emitModelCallLog(`anthropic call: model="${modelId}" max_tokens=${maxTokens} effort=${reasoningEffort ?? "(unset)"} thinking=${thinkingMode ?? "(default)"}`);
234
291
  let response;
235
292
  try {
236
293
  response = await client.messages.create({
237
294
  model: modelId,
238
295
  max_tokens: maxTokens,
296
+ // Thinking configuration is a three-way choice, and every non-default
297
+ // branch is additive over the legacy no-op tail so an absent opt-in emits
298
+ // byte-identical request bytes:
299
+ // thinking_mode="disabled" → thinking:{type:"disabled"} (explicit no
300
+ // extended thinking; accepted on opus-4.7/4.8 and sonnet-5, rejected
301
+ // by Fable 5). Takes precedence over effort: effort's only job on this
302
+ // route is to modulate *thinking* depth, which is moot once thinking
303
+ // is off, so output_config is not paired here.
304
+ // reasoningEffort set → adaptive thinking paired with output_config.
305
+ // effort (GA, no beta header). On opus-4.x effort only modulates
306
+ // thinking depth when adaptive is enabled — without adaptive the model
307
+ // runs without thinking, so configured effort would not be realized on
308
+ // this route. Pairing the two makes effort actually engage reasoning
309
+ // (matching the openai Responses and claude-CLI routes); effort also
310
+ // bounds overall token spend. The free-form seat string maps onto the
311
+ // API effort enum.
312
+ // neither → no thinking block (API default: high, no thinking — legacy
313
+ // behavior preserved).
314
+ ...(thinkingMode === "disabled"
315
+ ? { thinking: { type: "disabled" } }
316
+ : reasoningEffort
317
+ ? {
318
+ thinking: { type: "adaptive" },
319
+ output_config: {
320
+ effort: reasoningEffort,
321
+ },
322
+ }
323
+ : {}),
239
324
  system: systemPrompt,
240
325
  messages: [{ role: "user", content: userPrompt }],
241
326
  });
@@ -243,9 +328,31 @@ async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxToken
243
328
  catch (err) {
244
329
  const e = err;
245
330
  emitModelCallLog(`anthropic call FAILED: model="${modelId}" status=${e.status ?? "?"} type=${e.error?.type ?? e.name ?? "?"} message="${e.error?.message ?? e.message ?? String(err)}" request_id=${e.request_id ?? "?"}`);
331
+ // Normalize SDK request timeouts (thrown after maxRetries is exhausted) to a
332
+ // message the timeout-recovery classifier recognizes — the SDK error's own
333
+ // message ("Request timed out.") does not match, so api_key-provider timeout
334
+ // recovery was previously dead. instanceof is the robust check (the class
335
+ // does not override Error.prototype.name).
336
+ if (err instanceof Anthropic.APIConnectionTimeoutError) {
337
+ throw new Error(`anthropic call timed out after ${DEFAULT_TIMEOUT_MS}ms`, { cause: err });
338
+ }
246
339
  throw err;
247
340
  }
248
341
  emitModelCallLog(`anthropic success: model_id=${response.model ?? modelId} input_tokens=${response.usage.input_tokens} output_tokens=${response.usage.output_tokens}`);
342
+ // A max_tokens stop means the response was truncated and may carry
343
+ // partial/empty text — fail loud rather than record a truncated artifact.
344
+ // Both thinking-bearing branches are guarded: with adaptive thinking on
345
+ // (effort set) thinking shares the max_tokens budget; with thinking disabled
346
+ // the whole budget is the answer, so a cap still truncates it. The legacy
347
+ // no-opt path (neither set) stays unguarded, unchanged.
348
+ if ((reasoningEffort || thinkingMode === "disabled") &&
349
+ response.stop_reason === "max_tokens") {
350
+ throw new Error(`anthropic response truncated at max_tokens=${maxTokens}` +
351
+ (reasoningEffort
352
+ ? ` (effort=${reasoningEffort} reasoning exhausted the budget)`
353
+ : " (thinking disabled; answer exceeded the budget)") +
354
+ "; raise max_tokens");
355
+ }
249
356
  const text = response.content
250
357
  .filter((block) => block.type === "text")
251
358
  .map((block) => ("text" in block ? block.text : ""))
@@ -286,6 +393,12 @@ async function callOpenAI(systemPrompt, userPrompt, apiKey, modelId, maxTokens,
286
393
  catch (err) {
287
394
  const e = err;
288
395
  emitModelCallLog(`${providerLabel} call FAILED: model="${modelId}" status=${e.status ?? "?"} type=${e.error?.type ?? e.name ?? "?"} message="${e.error?.message ?? e.message ?? String(err)}" request_id=${e.request_id ?? "?"}`);
396
+ // Normalize SDK request timeouts to a classifier-recognized message (see the
397
+ // anthropic path above) so OpenAI/grok/lmstudio api_key timeouts also trigger
398
+ // timeout recovery instead of failing hard.
399
+ if (err instanceof OpenAI.APIConnectionTimeoutError) {
400
+ throw new Error(`${providerLabel} call timed out after ${DEFAULT_TIMEOUT_MS}ms`, { cause: err });
401
+ }
289
402
  throw err;
290
403
  }
291
404
  emitModelCallLog(`${providerLabel} success: model_id=${response.model ?? modelId} input_tokens=${response.usage?.prompt_tokens ?? 0} output_tokens=${response.usage?.completion_tokens ?? 0}`);
@@ -305,6 +418,142 @@ async function callOpenAI(systemPrompt, userPrompt, apiKey, modelId, maxTokens,
305
418
  };
306
419
  }
307
420
  // ---------------------------------------------------------------------------
421
+ // OpenAI Responses API call (canonical OpenAI reasoning models, e.g. gpt-5.x)
422
+ // ---------------------------------------------------------------------------
423
+ /**
424
+ * Canonical OpenAI path. Reasoning models (gpt-5.x) are driven through the
425
+ * Responses API: reasoning depth is `reasoning.effort` and the output cap is
426
+ * `max_output_tokens` (Chat Completions' `max_tokens` is rejected by reasoning
427
+ * models). grok/lmstudio stay on Chat Completions via `callOpenAI` — they are
428
+ * OpenAI-compatible Chat Completions endpoints with no Responses API.
429
+ */
430
+ async function callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, baseMaxOutputTokens, reasoningEffort, outputHeadroomTokens, modelMaxOutputTokens) {
431
+ const budget = resolveOpenAIResponsesOutputBudget({
432
+ baseOutputTokens: baseMaxOutputTokens,
433
+ ...(outputHeadroomTokens !== undefined
434
+ ? { headroomTokens: outputHeadroomTokens }
435
+ : {}),
436
+ ...(modelMaxOutputTokens !== undefined
437
+ ? { modelMaxOutputTokens }
438
+ : {}),
439
+ });
440
+ const { default: OpenAI } = await import("openai");
441
+ const effectiveBaseUrl = sanitizeEffectiveBaseUrl(outputHeadroomTokens === undefined
442
+ ? process.env.OPENAI_BASE_URL ?? OPENAI_RESPONSES_BASE_URL
443
+ : OPENAI_RESPONSES_BASE_URL);
444
+ const noOpLogger = {
445
+ error: () => { },
446
+ warn: () => { },
447
+ info: () => { },
448
+ debug: () => { },
449
+ };
450
+ const client = new OpenAI({
451
+ apiKey,
452
+ ...(outputHeadroomTokens !== undefined
453
+ ? { baseURL: OPENAI_RESPONSES_BASE_URL }
454
+ : {}),
455
+ timeout: DEFAULT_TIMEOUT_MS,
456
+ maxRetries: DEFAULT_MAX_RETRIES,
457
+ logLevel: "off",
458
+ logger: noOpLogger,
459
+ });
460
+ emitModelCallLog(`openai call: model="${modelId}" max_output_tokens=${budget.effectiveMaxOutputTokens} effort=${reasoningEffort ?? "(unset)"}`);
461
+ let response;
462
+ try {
463
+ response = await client.responses.create({
464
+ model: modelId,
465
+ instructions: systemPrompt,
466
+ input: userPrompt,
467
+ max_output_tokens: budget.effectiveMaxOutputTokens,
468
+ // Transient single-turn authoring/review calls; the Responses API stores
469
+ // response objects provider-side by default (retrievable for ~30d). Opt
470
+ // out so these prompts/outputs are not persisted — matches the
471
+ // chat.completions path, which never stored.
472
+ store: false,
473
+ ...(reasoningEffort ? { reasoning: { effort: reasoningEffort } } : {}),
474
+ });
475
+ }
476
+ catch (err) {
477
+ const e = err;
478
+ emitModelCallLog(`openai call FAILED: model="${modelId}" status=${e.status ?? "?"} type=${e.error?.type ?? e.name ?? "?"} message="${e.error?.message ?? e.message ?? String(err)}" request_id=${e.request_id ?? "?"}`);
479
+ // Mirror the api_key timeout normalization on the chat.completions path so
480
+ // Responses-API timeouts also trigger timeout recovery.
481
+ if (err instanceof OpenAI.APIConnectionTimeoutError) {
482
+ throw new Error(`openai call timed out after ${DEFAULT_TIMEOUT_MS}ms`, {
483
+ cause: err,
484
+ });
485
+ }
486
+ throw err;
487
+ }
488
+ // Only status="completed" carries a usable result. Anything else —
489
+ // "incomplete" (e.g. max_output_tokens exhausted, partial/empty output_text),
490
+ // "failed", "cancelled", "queued", "in_progress" — would otherwise be recorded
491
+ // as a normal result via `output_text ?? ""`, silently corrupting downstream
492
+ // artifacts. Fail loud so the caller retries or surfaces it.
493
+ if (response.status !== "completed") {
494
+ const detail = response.incomplete_details?.reason ?? response.error?.message ?? "no detail";
495
+ emitModelCallLog(`openai call NOT COMPLETED: model="${modelId}" status=${response.status} detail=${detail} max_output_tokens=${budget.effectiveMaxOutputTokens}`);
496
+ if (response.status === "incomplete" &&
497
+ response.incomplete_details?.reason === "max_output_tokens") {
498
+ const usage = response.usage;
499
+ const inputTokens = typeof usage?.input_tokens === "number"
500
+ ? usage.input_tokens
501
+ : null;
502
+ const cachedInputTokens = typeof usage?.input_tokens_details?.cached_tokens === "number"
503
+ ? usage.input_tokens_details.cached_tokens
504
+ : null;
505
+ const outputTokens = typeof usage?.output_tokens === "number"
506
+ ? usage.output_tokens
507
+ : null;
508
+ const reasoningTokens = typeof usage?.output_tokens_details?.reasoning_tokens === "number"
509
+ ? usage.output_tokens_details.reasoning_tokens
510
+ : null;
511
+ const nonReasoningOutputTokens = outputTokens !== null && reasoningTokens !== null
512
+ ? Math.max(0, outputTokens - reasoningTokens)
513
+ : null;
514
+ const partialOutput = response.output_text ?? "";
515
+ throw new OpenAIResponsesIncompleteError({
516
+ failure_code: OPENAI_RESPONSES_MAX_OUTPUT_TOKENS_FAILURE_CODE,
517
+ provider_status: response.status,
518
+ incomplete_reason: "max_output_tokens",
519
+ base_output_ceiling_tokens: budget.baseOutputTokens,
520
+ configured_output_headroom_tokens: budget.headroomTokens,
521
+ effective_max_output_tokens: budget.effectiveMaxOutputTokens,
522
+ input_tokens: inputTokens,
523
+ cached_input_tokens: cachedInputTokens,
524
+ output_tokens: outputTokens,
525
+ reasoning_tokens: reasoningTokens,
526
+ non_reasoning_output_tokens: nonReasoningOutputTokens,
527
+ partial_output_chars: partialOutput.length,
528
+ partial_output_sha256: crypto
529
+ .createHash("sha256")
530
+ .update(partialOutput, "utf8")
531
+ .digest("hex"),
532
+ provider_model: response.model ?? modelId,
533
+ provider_response_id: typeof response.id === "string" ? response.id : null,
534
+ provider_request_id: typeof response._request_id === "string"
535
+ ? response._request_id
536
+ : null,
537
+ effective_base_url: effectiveBaseUrl,
538
+ sdk_max_retries: DEFAULT_MAX_RETRIES,
539
+ actual_adapter_request_count: null,
540
+ request_count_observability: "unavailable",
541
+ });
542
+ }
543
+ throw new Error(`openai response not completed (status=${response.status}: ${detail}) at max_output_tokens=${budget.effectiveMaxOutputTokens}`);
544
+ }
545
+ const usage = response.usage;
546
+ emitModelCallLog(`openai success: model_id=${response.model ?? modelId} input_tokens=${usage?.input_tokens ?? 0} output_tokens=${usage?.output_tokens ?? 0}`);
547
+ return {
548
+ text: response.output_text ?? "",
549
+ input_tokens: usage?.input_tokens ?? 0,
550
+ output_tokens: usage?.output_tokens ?? 0,
551
+ model_id: modelId,
552
+ effective_base_url: effectiveBaseUrl,
553
+ declared_billing_mode: "per_token",
554
+ };
555
+ }
556
+ // ---------------------------------------------------------------------------
308
557
  // codex CLI call (OAuth subscription path)
309
558
  // ---------------------------------------------------------------------------
310
559
  /**
@@ -317,8 +566,9 @@ async function callOpenAI(systemPrompt, userPrompt, apiKey, modelId, maxTokens,
317
566
  * --skip-git-repo-check lets the worker run from non-repo cwd. No -C/-s/-o:
318
567
  * this is single-turn, no agentic scaffold.
319
568
  */
320
- async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort, serviceTier) {
569
+ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort, serviceTier, timeoutMs) {
321
570
  const { spawn } = await import("node:child_process");
571
+ const workerTimeoutMs = timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS;
322
572
  const args = ["exec", "--skip-git-repo-check", "--ephemeral"];
323
573
  if (modelId)
324
574
  args.push("-m", modelId);
@@ -328,7 +578,7 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
328
578
  args.push("-c", `service_tier="${serviceTier}"`);
329
579
  args.push("-");
330
580
  const combinedPrompt = `${systemPrompt}\n\n---\n\n${userPrompt}`;
331
- emitModelCallLog(`codex call: model="${modelId ?? "(codex default)"}" effort="${reasoningEffort ?? "(unset)"}" service_tier="${serviceTier ?? "(unset)"}" timeout_ms=${DEFAULT_TIMEOUT_MS}`);
581
+ emitModelCallLog(`codex call: model="${modelId ?? "(codex default)"}" effort="${reasoningEffort ?? "(unset)"}" service_tier="${serviceTier ?? "(unset)"}" timeout_ms=${workerTimeoutMs}`);
332
582
  const child = spawn("codex", args, {
333
583
  stdio: ["pipe", "pipe", "pipe"],
334
584
  });
@@ -350,15 +600,32 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
350
600
  stderr += String(chunk);
351
601
  appendRuntimeStreamChunkFromCurrentContextSync("stderr", chunk, codexStreamSource);
352
602
  });
603
+ // Swallow stdin write errors (EPIPE): if the worker exits before reading the
604
+ // prompt (e.g. fast model-allowlist rejection), an unhandled 'error' on the
605
+ // stdin stream would crash the host process. The real failure surfaces via the
606
+ // exit code / stderr below.
607
+ child.stdin.on("error", () => { });
353
608
  child.stdin.write(combinedPrompt);
354
609
  child.stdin.end();
610
+ let sigkillHandle;
355
611
  const timeoutHandle = setTimeout(() => {
356
612
  timedOut = true;
357
613
  child.kill("SIGTERM");
358
- }, DEFAULT_TIMEOUT_MS);
614
+ sigkillHandle = setTimeout(() => {
615
+ try {
616
+ child.kill("SIGKILL");
617
+ }
618
+ catch { /* already exited */ }
619
+ }, WORKER_SIGKILL_GRACE_MS);
620
+ }, workerTimeoutMs);
621
+ const clearTimers = () => {
622
+ clearTimeout(timeoutHandle);
623
+ if (sigkillHandle)
624
+ clearTimeout(sigkillHandle);
625
+ };
359
626
  const exitCode = await new Promise((resolve, reject) => {
360
627
  child.on("error", (err) => {
361
- clearTimeout(timeoutHandle);
628
+ clearTimers();
362
629
  if (err.code === "ENOENT") {
363
630
  reject(new Error("codex CLI not found on PATH. Install codex to use the OAuth subscription path: https://github.com/openai/codex"));
364
631
  }
@@ -367,13 +634,13 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
367
634
  }
368
635
  });
369
636
  child.on("close", (code) => {
370
- clearTimeout(timeoutHandle);
637
+ clearTimers();
371
638
  resolve(code ?? 1);
372
639
  });
373
640
  });
374
641
  if (timedOut) {
375
- emitModelCallLog(`codex call FAILED: model="${modelId ?? "(codex default)"}" reason=timeout timeout_ms=${DEFAULT_TIMEOUT_MS}`);
376
- throw new Error(`codex CLI call timed out after ${DEFAULT_TIMEOUT_MS}ms`);
642
+ emitModelCallLog(`codex call FAILED: model="${modelId ?? "(codex default)"}" reason=timeout timeout_ms=${workerTimeoutMs}`);
643
+ throw new Error(`codex CLI call timed out after ${workerTimeoutMs}ms`);
377
644
  }
378
645
  if (exitCode !== 0) {
379
646
  const combined = [stderr.trim(), stdout.trim()]
@@ -418,7 +685,6 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
418
685
  // ---------------------------------------------------------------------------
419
686
  // claude CLI call (Claude Code OAuth subscription path)
420
687
  // ---------------------------------------------------------------------------
421
- const CLAUDE_BIN = process.env.ONTO_CLAUDE_BIN?.trim() || "claude";
422
688
  /**
423
689
  * Parse the `result` event from `claude -p --output-format json`. The CLI emits
424
690
  * a JSON array of stream events (the final one is `type:"result"`), tolerating
@@ -478,8 +744,9 @@ function parseClaudeResultEvent(stdout) {
478
744
  * is the positional arg (piped stdin is not treated as the prompt), and no
479
745
  * project/user MCP servers are loaded so the worker has no side effects.
480
746
  */
481
- async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort) {
747
+ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort, timeoutMs) {
482
748
  const { spawn } = await import("node:child_process");
749
+ const workerTimeoutMs = timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS;
483
750
  const combinedPrompt = `${systemPrompt}\n\n---\n\n${userPrompt}`;
484
751
  const args = ["-p", combinedPrompt, "--output-format", "json"];
485
752
  if (modelId)
@@ -487,8 +754,9 @@ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort)
487
754
  if (reasoningEffort)
488
755
  args.push("--effort", reasoningEffort);
489
756
  args.push("--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}');
490
- emitModelCallLog(`claude call: model="${modelId ?? "(claude default)"}" effort="${reasoningEffort ?? "(unset)"}" timeout_ms=${DEFAULT_TIMEOUT_MS}`);
491
- const child = spawn(CLAUDE_BIN, args, { stdio: ["ignore", "pipe", "pipe"] });
757
+ emitModelCallLog(`claude call: model="${modelId ?? "(claude default)"}" effort="${reasoningEffort ?? "(unset)"}" timeout_ms=${workerTimeoutMs}`);
758
+ const claudeBin = resolveClaudeBin();
759
+ const child = spawn(claudeBin, args, { stdio: ["ignore", "pipe", "pipe"] });
492
760
  const claudeStreamSourceBase = {
493
761
  kind: "process",
494
762
  label: "claude-cli",
@@ -507,28 +775,40 @@ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort)
507
775
  stderr += String(chunk);
508
776
  appendRuntimeStreamChunkFromCurrentContextSync("stderr", chunk, claudeStreamSource);
509
777
  });
778
+ let sigkillHandle;
510
779
  const timeoutHandle = setTimeout(() => {
511
780
  timedOut = true;
512
781
  child.kill("SIGTERM");
513
- }, DEFAULT_TIMEOUT_MS);
782
+ sigkillHandle = setTimeout(() => {
783
+ try {
784
+ child.kill("SIGKILL");
785
+ }
786
+ catch { /* already exited */ }
787
+ }, WORKER_SIGKILL_GRACE_MS);
788
+ }, workerTimeoutMs);
789
+ const clearTimers = () => {
790
+ clearTimeout(timeoutHandle);
791
+ if (sigkillHandle)
792
+ clearTimeout(sigkillHandle);
793
+ };
514
794
  const exitCode = await new Promise((resolve, reject) => {
515
795
  child.on("error", (err) => {
516
- clearTimeout(timeoutHandle);
796
+ clearTimers();
517
797
  if (err.code === "ENOENT") {
518
- reject(new Error(`Claude Code CLI not found (${CLAUDE_BIN}). Install and log in to claude, or set ONTO_CLAUDE_BIN, to use the Anthropic OAuth subscription path: https://docs.anthropic.com/en/docs/claude-code`));
798
+ reject(new Error(`Claude Code CLI not found (${claudeBin}). Install and log in to claude, or set ONTO_CLAUDE_BIN, to use the Anthropic OAuth subscription path: https://docs.anthropic.com/en/docs/claude-code`));
519
799
  }
520
800
  else {
521
801
  reject(err);
522
802
  }
523
803
  });
524
804
  child.on("close", (code) => {
525
- clearTimeout(timeoutHandle);
805
+ clearTimers();
526
806
  resolve(code ?? 1);
527
807
  });
528
808
  });
529
809
  if (timedOut) {
530
- emitModelCallLog(`claude call FAILED: model="${modelId ?? "(claude default)"}" reason=timeout timeout_ms=${DEFAULT_TIMEOUT_MS}`);
531
- throw new Error(`claude CLI call timed out after ${DEFAULT_TIMEOUT_MS}ms`);
810
+ emitModelCallLog(`claude call FAILED: model="${modelId ?? "(claude default)"}" reason=timeout timeout_ms=${workerTimeoutMs}`);
811
+ throw new Error(`claude CLI call timed out after ${workerTimeoutMs}ms`);
532
812
  }
533
813
  if (exitCode !== 0) {
534
814
  const combined = [stderr.trim(), stdout.trim()]
@@ -585,10 +865,9 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
585
865
  const maxTokens = config.max_tokens ?? 1024;
586
866
  if (plan.provider_identity === "codex") {
587
867
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.codex;
588
- return callCodexCli(systemPrompt, userPrompt, modelId, config.reasoning_effort, config.service_tier);
868
+ return callCodexCli(systemPrompt, userPrompt, modelId, config.reasoning_effort, config.service_tier, config.timeout_ms);
589
869
  }
590
870
  if (plan.provider_identity === "anthropic") {
591
- assertNoUnsupportedReasoningEffort("anthropic", config.reasoning_effort);
592
871
  const apiKey = readEnvApiKey(config.api_key_env ? [config.api_key_env] : ["ANTHROPIC_API_KEY"]);
593
872
  if (!apiKey) {
594
873
  throw new Error(explicitProviderMissingCredentialError("anthropic", config.api_key_env));
@@ -596,7 +875,7 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
596
875
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.anthropic;
597
876
  if (!modelId)
598
877
  throw missingModelError("anthropic");
599
- return callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens);
878
+ return callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, config.reasoning_effort, config.thinking_mode);
600
879
  }
601
880
  if (plan.provider_identity === "openai") {
602
881
  const apiKey = readOpenAiApiKey(config.api_key_env);
@@ -606,7 +885,7 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
606
885
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.openai;
607
886
  if (!modelId)
608
887
  throw missingModelError("openai");
609
- return callOpenAI(systemPrompt, userPrompt, apiKey, modelId, maxTokens, undefined, "openai", config.reasoning_effort);
888
+ return callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, maxTokens, config.reasoning_effort, config.openai_responses_output_headroom_tokens, config.openai_responses_model_max_output_tokens);
610
889
  }
611
890
  if (plan.provider_identity === "grok") {
612
891
  assertNoUnsupportedReasoningEffort("grok", config.reasoning_effort);
@@ -646,23 +925,26 @@ export async function callLlm(systemPrompt, userPrompt, config) {
646
925
  config?.artifact_generation_realization === "semantic_mock") {
647
926
  return callReviewMockLlm(systemPrompt, userPrompt, config);
648
927
  }
928
+ if (config?.openai_responses_output_headroom_tokens !== undefined &&
929
+ (config.provider !== "openai" ||
930
+ config.execution_adapter !== "openai_sdk" ||
931
+ config.base_url !== undefined)) {
932
+ throw new Error("openai Responses output headroom reached an unsupported dispatch route.");
933
+ }
649
934
  if (config?.plan) {
650
935
  return dispatchByPlan(systemPrompt, userPrompt, config);
651
936
  }
652
937
  if (isLegacyCodexCliProvider(config)) {
653
- return callCodexCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.codex, config.reasoning_effort, config.service_tier);
938
+ return callCodexCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.codex, config.reasoning_effort, config.service_tier, config.timeout_ms);
654
939
  }
655
940
  // Anthropic OAuth → Claude Code CLI worker. The model-switcher keeps
656
941
  // provider="anthropic" for OAuth (the brand lives in execution_adapter), so
657
942
  // dispatch on the adapter here, before the SDK/api-key path. This is the
658
- // direct-call analog of openai OAuth → codex. claude DOES honor effort, so it
659
- // is passed through rather than rejected by the SDK-path effort guard.
943
+ // direct-call analog of openai OAuth → codex. Both this CLI route (--effort)
944
+ // and the api_key SDK route below (output_config.effort) honor effort.
660
945
  if (config?.provider === "anthropic" &&
661
946
  config?.execution_adapter === "claude_code") {
662
- return callClaudeCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.anthropic, config.reasoning_effort);
663
- }
664
- if (config?.provider === "anthropic") {
665
- assertNoUnsupportedReasoningEffort("anthropic", config.reasoning_effort);
947
+ return callClaudeCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.anthropic, config.reasoning_effort, config.timeout_ms);
666
948
  }
667
949
  if (config?.provider === "grok") {
668
950
  assertNoUnsupportedReasoningEffort("grok", config.reasoning_effort);
@@ -690,20 +972,19 @@ export async function callLlm(systemPrompt, userPrompt, config) {
690
972
  switch (resolved.provider) {
691
973
  case "codex": {
692
974
  const modelId = config?.model_id ?? perProviderModel;
693
- return callCodexCli(systemPrompt, userPrompt, modelId, config?.reasoning_effort, config?.service_tier);
975
+ return callCodexCli(systemPrompt, userPrompt, modelId, config?.reasoning_effort, config?.service_tier, config?.timeout_ms);
694
976
  }
695
977
  case "anthropic": {
696
- assertNoUnsupportedReasoningEffort("anthropic", config?.reasoning_effort);
697
978
  const modelId = config?.model_id ?? perProviderModel;
698
979
  if (!modelId)
699
980
  throw missingModelError("anthropic");
700
- return callAnthropic(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens);
981
+ return callAnthropic(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, config?.reasoning_effort, config?.thinking_mode);
701
982
  }
702
983
  case "openai": {
703
984
  const modelId = config?.model_id ?? perProviderModel;
704
985
  if (!modelId)
705
986
  throw missingModelError("openai");
706
- return callOpenAI(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, undefined, "openai", config?.reasoning_effort);
987
+ return callOpenAIResponses(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, config?.reasoning_effort, config?.openai_responses_output_headroom_tokens, config?.openai_responses_model_max_output_tokens);
707
988
  }
708
989
  case "grok": {
709
990
  assertNoUnsupportedReasoningEffort("grok", config?.reasoning_effort);
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { parseRuntimeIssueDeliberationSchemaContext, parseRuntimeIssueStanceSchemaContext, parseRuntimeIssueSynthesisSchemaContext, parseRuntimeProblemFramingContext, } from "../cli/runtime-submit-context.js";
5
+ import { packetHasResubmitErrorSpec } from "../cli/unit-resubmit.js";
5
6
  import { parseParticipatingLensPaths } from "../review/participating-lens-paths.js";
6
7
  export const REVIEW_MOCK_REALIZATION_ENV = "ONTO_LLM_MOCK";
7
8
  export function isReviewMockLlmRealizationEnabled(env = process.env) {
@@ -432,16 +433,46 @@ function issueArtifactArgs(unitId, packetText, toolCtx) {
432
433
  throw new Error(`Unsupported mock issue artifact unit: ${unitId}`);
433
434
  }
434
435
  }
435
- function issueStanceArgs(packetText) {
436
+ /**
437
+ * 설계 A 픽스처 스위치 (F-A1/F-A2): 지정 렌즈의 stance 유닛이 허용 집합 밖
438
+ * evidence_ref를 제출해 실제 `issue_evidence_refs` 화이트리스트 검증을
439
+ * 거부당하게 한다.
440
+ * - `ONTO_LLM_MOCK_STANCE_UNSUPPORTED_REF_LENSES`: csv 렌즈 id 또는 `*`.
441
+ * - `ONTO_LLM_MOCK_STANCE_UNSUPPORTED_REF_MODE`: `persist`(기본, cap 소진
442
+ * 경로) | `correct_on_resubmit`(packet에 resubmit 오류 명세가 주입되면
443
+ * 유효 ref로 치유 — 오류 명세 전달의 E2E 증거).
444
+ */
445
+ function stanceUnsupportedRefTargets(env = process.env) {
446
+ const raw = env.ONTO_LLM_MOCK_STANCE_UNSUPPORTED_REF_LENSES;
447
+ if (!raw)
448
+ return null;
449
+ if (raw.trim() === "*")
450
+ return "*";
451
+ const lenses = raw
452
+ .split(",")
453
+ .map((value) => value.trim())
454
+ .filter((value) => value.length > 0);
455
+ return lenses.length > 0 ? new Set(lenses) : null;
456
+ }
457
+ function issueStanceArgs(unitId, packetText) {
436
458
  const context = parseRuntimeIssueStanceSchemaContext(packetText);
459
+ const lensId = unitId.startsWith("issue-stance:")
460
+ ? unitId.slice("issue-stance:".length)
461
+ : unitId;
462
+ const targets = stanceUnsupportedRefTargets();
463
+ const healed = process.env.ONTO_LLM_MOCK_STANCE_UNSUPPORTED_REF_MODE ===
464
+ "correct_on_resubmit" && packetHasResubmitErrorSpec(packetText);
465
+ const emitUnsupportedRef = targets !== null && (targets === "*" || targets.has(lensId)) && !healed;
437
466
  return {
438
- stances: Object.entries(context.issue_evidence_refs).map(([issueId, refs]) => ({
467
+ stances: Object.entries(context.issue_evidence_refs).map(([issueId, refs], index) => ({
439
468
  issue_id: issueId,
440
469
  stance: "support",
441
470
  rationale: "Mock realization supports the low-severity issue framing.",
442
471
  root_hypothesis_position: "accepts",
443
472
  severity_position: "keeps",
444
- evidence_refs: refs.slice(0, 1),
473
+ evidence_refs: emitUnsupportedRef && index === 0
474
+ ? [`mock-unsupported-ref:${lensId}`]
475
+ : refs.slice(0, 1),
445
476
  })),
446
477
  };
447
478
  }
@@ -502,7 +533,7 @@ async function executeSubmitTool(args) {
502
533
  await args.tool.execute(issueArtifactArgs(unitId, args.userPrompt, args.toolCtx), args.toolCtx);
503
534
  return;
504
535
  case "submit_issue_stance_response":
505
- await args.tool.execute(issueStanceArgs(args.userPrompt), args.toolCtx);
536
+ await args.tool.execute(issueStanceArgs(unitId, args.userPrompt), args.toolCtx);
506
537
  return;
507
538
  case "submit_issue_deliberation_response":
508
539
  await args.tool.execute(issueDeliberationArgs(args.userPrompt), args.toolCtx);
@@ -31,7 +31,7 @@ export function normalizeLlmModelSwitcher(config) {
31
31
  throw new Error("auth=local currently requires provider=lmstudio.");
32
32
  }
33
33
  if (config.service_tier && !(provider === "openai" && auth === "oauth")) {
34
- throw new Error("service_tier requires the external OAuth worker route with auth=oauth and provider=openai.");
34
+ throw new Error(`service_tier is only valid on the openai + auth=oauth (Codex) route; remove service_tier for provider=${provider} + auth=${auth}.`);
35
35
  }
36
36
  const model_id = config.model;
37
37
  const common = {