onto-mcp 0.4.12 → 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 (128) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +137 -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 +169 -8
  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/productized-live-path.md +9 -5
  11. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  12. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  13. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  14. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  15. package/README.md +64 -1
  16. package/dist/cli.js +11 -1
  17. package/dist/core-api/reconstruct-api.js +575 -24
  18. package/dist/core-api/review-api.js +70 -5
  19. package/dist/core-api/review-progress.js +7 -0
  20. package/dist/core-api/runtime-observation.js +7 -0
  21. package/dist/core-runtime/artifact-io.js +75 -3
  22. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
  23. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  24. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  25. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  26. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  27. package/dist/core-runtime/cli/run-seats.js +18 -0
  28. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  29. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  30. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  31. package/dist/core-runtime/discovery/host-detection.js +71 -22
  32. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  33. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  34. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  35. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  36. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  37. package/dist/core-runtime/discovery/supported-models.js +282 -29
  38. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  39. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  40. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  46. package/dist/core-runtime/effort-calibration-ingest.js +135 -0
  47. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  48. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  49. package/dist/core-runtime/llm/llm-caller.js +188 -47
  50. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  51. package/dist/core-runtime/llm/model-switcher.js +1 -1
  52. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  53. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  54. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  55. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  56. package/dist/core-runtime/onboard/cli-host.js +5 -2
  57. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  58. package/dist/core-runtime/onboard/host-target.js +28 -3
  59. package/dist/core-runtime/onboard/register.js +54 -12
  60. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  61. package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
  62. package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
  63. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  64. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  65. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  66. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  67. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  68. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  69. package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
  70. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  71. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  72. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  73. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  74. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  75. package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
  76. package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
  77. package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
  78. package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
  79. package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
  80. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  81. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
  82. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  83. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
  84. package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
  85. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
  86. package/dist/core-runtime/reconstruct/record.js +42 -0
  87. package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
  88. package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
  89. package/dist/core-runtime/reconstruct/run.js +5198 -536
  90. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
  91. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
  92. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
  93. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  94. package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
  95. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
  96. package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
  97. package/dist/core-runtime/review/continuation-plan.js +12 -5
  98. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  99. package/dist/core-runtime/review/materializers.js +100 -11
  100. package/dist/core-runtime/review/nesting-batch.js +6 -2
  101. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  102. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  103. package/dist/core-runtime/review/review-artifact-utils.js +217 -5
  104. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  105. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  106. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  107. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  108. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  109. package/dist/core-runtime/route-identity.js +182 -0
  110. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  111. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  112. package/dist/core-runtime/target-material-kind.js +58 -0
  113. package/dist/mcp/server.js +12 -5
  114. package/dist/tui/app.js +167 -0
  115. package/dist/tui/data/event-follower.js +116 -0
  116. package/dist/tui/data/node-detail.js +42 -0
  117. package/dist/tui/data/projection-poll.js +26 -0
  118. package/dist/tui/data/session-discovery.js +65 -0
  119. package/dist/tui/index.js +108 -0
  120. package/dist/tui/screens/log.js +25 -0
  121. package/dist/tui/screens/node-detail.js +17 -0
  122. package/dist/tui/screens/session-selector.js +26 -0
  123. package/dist/tui/screens/workflow-tree.js +90 -0
  124. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  125. package/dist/tui/view-model/review-adapter.js +270 -0
  126. package/dist/tui/view-model/tree-view-model.js +19 -0
  127. package/package.json +18 -3
  128. package/settings.example.json +77 -16
@@ -20,6 +20,7 @@ import { DEFAULT_GROK_BASE_URL, DEFAULT_LMSTUDIO_BASE_URL, isExternalOauthWorker
20
20
  import { resolveClaudeBin } from "./claude-bin.js";
21
21
  import { appendRuntimeModelCallLogFromCurrentContext, appendRuntimeStreamChunkFromCurrentContextSync, } from "../observability/runtime-stream-observation.js";
22
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";
23
24
  /**
24
25
  * Bridge: OntoConfig + CLI overrides → Partial<LlmCallConfig> that callLlm consumes.
25
26
  *
@@ -67,11 +68,39 @@ export function resolveLlmProviderConfig(args) {
67
68
  out.service_tier = service_tier;
68
69
  if (api_key_env)
69
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;
70
74
  if (Object.keys(models_per_provider).length > 0) {
71
75
  out.models_per_provider = models_per_provider;
72
76
  }
73
77
  return out;
74
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
+ }
75
104
  // Phase 3 production found 30s too tight for large audit batches (37 items
76
105
  // could time out then SDK-retry for 90s total). 120s is generous
77
106
  // enough for ~50-item single-batch audits while still failing fast on real
@@ -94,6 +123,20 @@ const WORKER_SIGKILL_GRACE_MS = 5_000;
94
123
  // faster (1 retry instead of the default 2) so operators see provider errors
95
124
  // within ~2× timeout instead of ~3×.
96
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
+ }
97
140
  /**
98
141
  * Model-call observability — emits STDERR logs for each LLM API call, covering
99
142
  * (a) pre-call model_id + provider + max_tokens, (b) post-call usage on success,
@@ -237,35 +280,47 @@ function assertNoUnsupportedReasoningEffort(provider, reasoningEffort) {
237
280
  // ---------------------------------------------------------------------------
238
281
  // Anthropic call
239
282
  // ---------------------------------------------------------------------------
240
- async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, reasoningEffort) {
283
+ async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, reasoningEffort, thinkingMode) {
241
284
  const { default: Anthropic } = await import("@anthropic-ai/sdk");
242
285
  const client = new Anthropic({
243
286
  apiKey,
244
287
  timeout: DEFAULT_TIMEOUT_MS,
245
288
  maxRetries: DEFAULT_MAX_RETRIES,
246
289
  });
247
- emitModelCallLog(`anthropic call: model="${modelId}" max_tokens=${maxTokens} effort=${reasoningEffort ?? "(unset)"}`);
290
+ emitModelCallLog(`anthropic call: model="${modelId}" max_tokens=${maxTokens} effort=${reasoningEffort ?? "(unset)"} thinking=${thinkingMode ?? "(default)"}`);
248
291
  let response;
249
292
  try {
250
293
  response = await client.messages.create({
251
294
  model: modelId,
252
295
  max_tokens: maxTokens,
253
- // Reasoning depth is controlled by output_config.effort (GA, no beta
254
- // header), but on opus-4.x effort only modulates *thinking* depth when
255
- // adaptive thinking is enabled — without it the model runs without
256
- // thinking, so configured effort would not be realized on this route.
257
- // Pair the two so effort actually engages reasoning (matching the openai
258
- // Responses and claude-CLI routes); effort also bounds overall token
259
- // spend. The free-form seat string maps onto the API effort enum; absent
260
- // the API default (high) with no thinking (legacy behavior preserved).
261
- ...(reasoningEffort
262
- ? {
263
- thinking: { type: "adaptive" },
264
- output_config: {
265
- effort: reasoningEffort,
266
- },
267
- }
268
- : {}),
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
+ : {}),
269
324
  system: systemPrompt,
270
325
  messages: [{ role: "user", content: userPrompt }],
271
326
  });
@@ -284,11 +339,19 @@ async function callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxToken
284
339
  throw err;
285
340
  }
286
341
  emitModelCallLog(`anthropic success: model_id=${response.model ?? modelId} input_tokens=${response.usage.input_tokens} output_tokens=${response.usage.output_tokens}`);
287
- // With adaptive thinking on (effort set), thinking shares the max_tokens
288
- // budget; a max_tokens stop means the response was truncated and may carry
289
- // partial/empty text. Fail loud rather than record a truncated artifact.
290
- if (reasoningEffort && response.stop_reason === "max_tokens") {
291
- throw new Error(`anthropic response truncated at max_tokens=${maxTokens} (effort=${reasoningEffort} reasoning exhausted the budget); raise max_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");
292
355
  }
293
356
  const text = response.content
294
357
  .filter((block) => block.type === "text")
@@ -364,21 +427,44 @@ async function callOpenAI(systemPrompt, userPrompt, apiKey, modelId, maxTokens,
364
427
  * models). grok/lmstudio stay on Chat Completions via `callOpenAI` — they are
365
428
  * OpenAI-compatible Chat Completions endpoints with no Responses API.
366
429
  */
367
- async function callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, maxOutputTokens, reasoningEffort) {
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
+ });
368
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
+ };
369
450
  const client = new OpenAI({
370
451
  apiKey,
452
+ ...(outputHeadroomTokens !== undefined
453
+ ? { baseURL: OPENAI_RESPONSES_BASE_URL }
454
+ : {}),
371
455
  timeout: DEFAULT_TIMEOUT_MS,
372
456
  maxRetries: DEFAULT_MAX_RETRIES,
457
+ logLevel: "off",
458
+ logger: noOpLogger,
373
459
  });
374
- emitModelCallLog(`openai call: model="${modelId}" max_output_tokens=${maxOutputTokens} effort=${reasoningEffort ?? "(unset)"}`);
460
+ emitModelCallLog(`openai call: model="${modelId}" max_output_tokens=${budget.effectiveMaxOutputTokens} effort=${reasoningEffort ?? "(unset)"}`);
375
461
  let response;
376
462
  try {
377
463
  response = await client.responses.create({
378
464
  model: modelId,
379
465
  instructions: systemPrompt,
380
466
  input: userPrompt,
381
- max_output_tokens: maxOutputTokens,
467
+ max_output_tokens: budget.effectiveMaxOutputTokens,
382
468
  // Transient single-turn authoring/review calls; the Responses API stores
383
469
  // response objects provider-side by default (retrievable for ~30d). Opt
384
470
  // out so these prompts/outputs are not persisted — matches the
@@ -406,8 +492,55 @@ async function callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, ma
406
492
  // artifacts. Fail loud so the caller retries or surfaces it.
407
493
  if (response.status !== "completed") {
408
494
  const detail = response.incomplete_details?.reason ?? response.error?.message ?? "no detail";
409
- emitModelCallLog(`openai call NOT COMPLETED: model="${modelId}" status=${response.status} detail=${detail} max_output_tokens=${maxOutputTokens}`);
410
- throw new Error(`openai response not completed (status=${response.status}: ${detail}) at max_output_tokens=${maxOutputTokens} — raise max_output_tokens if truncated`);
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}`);
411
544
  }
412
545
  const usage = response.usage;
413
546
  emitModelCallLog(`openai success: model_id=${response.model ?? modelId} input_tokens=${usage?.input_tokens ?? 0} output_tokens=${usage?.output_tokens ?? 0}`);
@@ -416,7 +549,7 @@ async function callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, ma
416
549
  input_tokens: usage?.input_tokens ?? 0,
417
550
  output_tokens: usage?.output_tokens ?? 0,
418
551
  model_id: modelId,
419
- effective_base_url: "https://api.openai.com/v1",
552
+ effective_base_url: effectiveBaseUrl,
420
553
  declared_billing_mode: "per_token",
421
554
  };
422
555
  }
@@ -433,8 +566,9 @@ async function callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, ma
433
566
  * --skip-git-repo-check lets the worker run from non-repo cwd. No -C/-s/-o:
434
567
  * this is single-turn, no agentic scaffold.
435
568
  */
436
- async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort, serviceTier) {
569
+ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort, serviceTier, timeoutMs) {
437
570
  const { spawn } = await import("node:child_process");
571
+ const workerTimeoutMs = timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS;
438
572
  const args = ["exec", "--skip-git-repo-check", "--ephemeral"];
439
573
  if (modelId)
440
574
  args.push("-m", modelId);
@@ -444,7 +578,7 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
444
578
  args.push("-c", `service_tier="${serviceTier}"`);
445
579
  args.push("-");
446
580
  const combinedPrompt = `${systemPrompt}\n\n---\n\n${userPrompt}`;
447
- emitModelCallLog(`codex call: model="${modelId ?? "(codex default)"}" effort="${reasoningEffort ?? "(unset)"}" service_tier="${serviceTier ?? "(unset)"}" timeout_ms=${DEFAULT_WORKER_TIMEOUT_MS}`);
581
+ emitModelCallLog(`codex call: model="${modelId ?? "(codex default)"}" effort="${reasoningEffort ?? "(unset)"}" service_tier="${serviceTier ?? "(unset)"}" timeout_ms=${workerTimeoutMs}`);
448
582
  const child = spawn("codex", args, {
449
583
  stdio: ["pipe", "pipe", "pipe"],
450
584
  });
@@ -483,7 +617,7 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
483
617
  }
484
618
  catch { /* already exited */ }
485
619
  }, WORKER_SIGKILL_GRACE_MS);
486
- }, DEFAULT_WORKER_TIMEOUT_MS);
620
+ }, workerTimeoutMs);
487
621
  const clearTimers = () => {
488
622
  clearTimeout(timeoutHandle);
489
623
  if (sigkillHandle)
@@ -505,8 +639,8 @@ async function callCodexCli(systemPrompt, userPrompt, modelId, reasoningEffort,
505
639
  });
506
640
  });
507
641
  if (timedOut) {
508
- emitModelCallLog(`codex call FAILED: model="${modelId ?? "(codex default)"}" reason=timeout timeout_ms=${DEFAULT_WORKER_TIMEOUT_MS}`);
509
- throw new Error(`codex CLI call timed out after ${DEFAULT_WORKER_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`);
510
644
  }
511
645
  if (exitCode !== 0) {
512
646
  const combined = [stderr.trim(), stdout.trim()]
@@ -610,8 +744,9 @@ function parseClaudeResultEvent(stdout) {
610
744
  * is the positional arg (piped stdin is not treated as the prompt), and no
611
745
  * project/user MCP servers are loaded so the worker has no side effects.
612
746
  */
613
- async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort) {
747
+ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort, timeoutMs) {
614
748
  const { spawn } = await import("node:child_process");
749
+ const workerTimeoutMs = timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS;
615
750
  const combinedPrompt = `${systemPrompt}\n\n---\n\n${userPrompt}`;
616
751
  const args = ["-p", combinedPrompt, "--output-format", "json"];
617
752
  if (modelId)
@@ -619,7 +754,7 @@ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort)
619
754
  if (reasoningEffort)
620
755
  args.push("--effort", reasoningEffort);
621
756
  args.push("--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}');
622
- emitModelCallLog(`claude call: model="${modelId ?? "(claude default)"}" effort="${reasoningEffort ?? "(unset)"}" timeout_ms=${DEFAULT_WORKER_TIMEOUT_MS}`);
757
+ emitModelCallLog(`claude call: model="${modelId ?? "(claude default)"}" effort="${reasoningEffort ?? "(unset)"}" timeout_ms=${workerTimeoutMs}`);
623
758
  const claudeBin = resolveClaudeBin();
624
759
  const child = spawn(claudeBin, args, { stdio: ["ignore", "pipe", "pipe"] });
625
760
  const claudeStreamSourceBase = {
@@ -650,7 +785,7 @@ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort)
650
785
  }
651
786
  catch { /* already exited */ }
652
787
  }, WORKER_SIGKILL_GRACE_MS);
653
- }, DEFAULT_WORKER_TIMEOUT_MS);
788
+ }, workerTimeoutMs);
654
789
  const clearTimers = () => {
655
790
  clearTimeout(timeoutHandle);
656
791
  if (sigkillHandle)
@@ -672,8 +807,8 @@ async function callClaudeCli(systemPrompt, userPrompt, modelId, reasoningEffort)
672
807
  });
673
808
  });
674
809
  if (timedOut) {
675
- emitModelCallLog(`claude call FAILED: model="${modelId ?? "(claude default)"}" reason=timeout timeout_ms=${DEFAULT_WORKER_TIMEOUT_MS}`);
676
- throw new Error(`claude CLI call timed out after ${DEFAULT_WORKER_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`);
677
812
  }
678
813
  if (exitCode !== 0) {
679
814
  const combined = [stderr.trim(), stdout.trim()]
@@ -730,7 +865,7 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
730
865
  const maxTokens = config.max_tokens ?? 1024;
731
866
  if (plan.provider_identity === "codex") {
732
867
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.codex;
733
- 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);
734
869
  }
735
870
  if (plan.provider_identity === "anthropic") {
736
871
  const apiKey = readEnvApiKey(config.api_key_env ? [config.api_key_env] : ["ANTHROPIC_API_KEY"]);
@@ -740,7 +875,7 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
740
875
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.anthropic;
741
876
  if (!modelId)
742
877
  throw missingModelError("anthropic");
743
- return callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, config.reasoning_effort);
878
+ return callAnthropic(systemPrompt, userPrompt, apiKey, modelId, maxTokens, config.reasoning_effort, config.thinking_mode);
744
879
  }
745
880
  if (plan.provider_identity === "openai") {
746
881
  const apiKey = readOpenAiApiKey(config.api_key_env);
@@ -750,7 +885,7 @@ async function dispatchByPlan(systemPrompt, userPrompt, config) {
750
885
  const modelId = config.model_id ?? plan.model_id ?? config.models_per_provider?.openai;
751
886
  if (!modelId)
752
887
  throw missingModelError("openai");
753
- return callOpenAIResponses(systemPrompt, userPrompt, apiKey, modelId, maxTokens, 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);
754
889
  }
755
890
  if (plan.provider_identity === "grok") {
756
891
  assertNoUnsupportedReasoningEffort("grok", config.reasoning_effort);
@@ -790,11 +925,17 @@ export async function callLlm(systemPrompt, userPrompt, config) {
790
925
  config?.artifact_generation_realization === "semantic_mock") {
791
926
  return callReviewMockLlm(systemPrompt, userPrompt, config);
792
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
+ }
793
934
  if (config?.plan) {
794
935
  return dispatchByPlan(systemPrompt, userPrompt, config);
795
936
  }
796
937
  if (isLegacyCodexCliProvider(config)) {
797
- 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);
798
939
  }
799
940
  // Anthropic OAuth → Claude Code CLI worker. The model-switcher keeps
800
941
  // provider="anthropic" for OAuth (the brand lives in execution_adapter), so
@@ -803,7 +944,7 @@ export async function callLlm(systemPrompt, userPrompt, config) {
803
944
  // and the api_key SDK route below (output_config.effort) honor effort.
804
945
  if (config?.provider === "anthropic" &&
805
946
  config?.execution_adapter === "claude_code") {
806
- return callClaudeCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.anthropic, config.reasoning_effort);
947
+ return callClaudeCli(systemPrompt, userPrompt, config.model_id ?? config.models_per_provider?.anthropic, config.reasoning_effort, config.timeout_ms);
807
948
  }
808
949
  if (config?.provider === "grok") {
809
950
  assertNoUnsupportedReasoningEffort("grok", config.reasoning_effort);
@@ -831,19 +972,19 @@ export async function callLlm(systemPrompt, userPrompt, config) {
831
972
  switch (resolved.provider) {
832
973
  case "codex": {
833
974
  const modelId = config?.model_id ?? perProviderModel;
834
- 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);
835
976
  }
836
977
  case "anthropic": {
837
978
  const modelId = config?.model_id ?? perProviderModel;
838
979
  if (!modelId)
839
980
  throw missingModelError("anthropic");
840
- return callAnthropic(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, config?.reasoning_effort);
981
+ return callAnthropic(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, config?.reasoning_effort, config?.thinking_mode);
841
982
  }
842
983
  case "openai": {
843
984
  const modelId = config?.model_id ?? perProviderModel;
844
985
  if (!modelId)
845
986
  throw missingModelError("openai");
846
- return callOpenAIResponses(systemPrompt, userPrompt, resolved.apiKey, modelId, maxTokens, 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);
847
988
  }
848
989
  case "grok": {
849
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 = {
@@ -0,0 +1,46 @@
1
+ export const OPENAI_RESPONSES_MAX_OUTPUT_TOKENS_FAILURE_CODE = "openai_responses_max_output_tokens";
2
+ export class OpenAIResponsesIncompleteError extends Error {
3
+ evidence;
4
+ constructor(evidence) {
5
+ super(`openai response not completed (status=${evidence.provider_status}: ` +
6
+ `${evidence.incomplete_reason}) at max_output_tokens=` +
7
+ `${evidence.effective_max_output_tokens}`);
8
+ this.name = "OpenAIResponsesIncompleteError";
9
+ this.evidence = evidence;
10
+ }
11
+ }
12
+ export function readOpenAIResponsesIncompleteEvidence(error) {
13
+ return error instanceof OpenAIResponsesIncompleteError
14
+ ? error.evidence
15
+ : null;
16
+ }
17
+ export function resolveOpenAIResponsesOutputBudget(args) {
18
+ if (args.headroomTokens === undefined) {
19
+ return {
20
+ baseOutputTokens: args.baseOutputTokens,
21
+ headroomTokens: 0,
22
+ effectiveMaxOutputTokens: args.baseOutputTokens,
23
+ };
24
+ }
25
+ if (!Number.isSafeInteger(args.headroomTokens) || args.headroomTokens <= 0) {
26
+ throw new Error("openai Responses output headroom must be a positive safe integer.");
27
+ }
28
+ if (args.modelMaxOutputTokens === undefined ||
29
+ !Number.isSafeInteger(args.modelMaxOutputTokens) ||
30
+ args.modelMaxOutputTokens <= 0) {
31
+ throw new Error("openai Responses output headroom requires a positive model max_output_tokens capability.");
32
+ }
33
+ const effective = args.baseOutputTokens + args.headroomTokens;
34
+ if (!Number.isSafeInteger(effective)) {
35
+ throw new Error("openai Responses effective max_output_tokens is not a safe integer.");
36
+ }
37
+ if (effective > args.modelMaxOutputTokens) {
38
+ throw new Error(`openai Responses effective max_output_tokens=${effective} exceeds ` +
39
+ `model max_output_tokens=${args.modelMaxOutputTokens}.`);
40
+ }
41
+ return {
42
+ baseOutputTokens: args.baseOutputTokens,
43
+ headroomTokens: args.headroomTokens,
44
+ effectiveMaxOutputTokens: effective,
45
+ };
46
+ }