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
@@ -3,25 +3,206 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  import { RECONSTRUCT_STAGE_IDS, } from "../core-runtime/reconstruct/artifact-types.js";
6
- import { materializeReconstructPreparationArtifacts, } from "../core-runtime/reconstruct/materialize-preparation.js";
6
+ import { deriveDocumentExcerptProjectionBudget, DOCUMENT_EXCERPT_PROJECTION_FLOOR, materializeReconstructPreparationArtifacts, } from "../core-runtime/reconstruct/materialize-preparation.js";
7
7
  import { writeTargetMaterialProfileValidationArtifact, } from "../core-runtime/reconstruct/material-profile-validation.js";
8
- import { assembleReconstructRecord, } from "../core-runtime/reconstruct/record.js";
8
+ import { assembleReconstructRecord, reconstructTerminalStatus, } from "../core-runtime/reconstruct/record.js";
9
+ export { reconstructTerminalStatus, } from "../core-runtime/reconstruct/record.js";
9
10
  import { createDirectCallReconstructConfirmationProvider, createDirectCallReconstructDirectiveAuthor, runReconstruct, } from "../core-runtime/reconstruct/run.js";
11
+ import { RECONSTRUCT_SEMANTIC_AUTHOR_MAX_BASE_OUTPUT_TOKENS, } from "../core-runtime/reconstruct/output-budget.js";
10
12
  // Explicit reconstruct mock realization switch point (INV-MOCK-1 boundary;
11
13
  // allowlisted in scripts/check-import-boundary.ts). Active only when
12
14
  // ONTO_LLM_MOCK=1; mock runs record mock actor ids in the run manifest.
13
15
  import { RECONSTRUCT_MOCK_AUTHOR_ID, RECONSTRUCT_MOCK_CONFIRMATION_PROVIDER_ID, callReconstructMockLlm, isReconstructMockLlmRealizationEnabled, } from "../core-runtime/reconstruct/mock-llm-realization.js";
14
- import { assertSettingsModelsSupported, resolveSettingsChain, resolveReconstructActorLlmSettings, } from "../core-runtime/discovery/settings-chain.js";
16
+ import { assertSettingsModelsSupported, completeDispatchBreakerSettings, isReconstructSemanticMapAuthoringEnabled, resolveOptionalReconstructActorLlmSettings, resolveSettingsChain, resolveReconstructActorLlmSettings, resolveReconstructSemanticAuthorLlmRuntimeSettings, } from "../core-runtime/discovery/settings-chain.js";
17
+ import { assertDispatchFallbackSessionAdmission } from "../core-runtime/reconstruct/dispatch-fallback-artifacts.js";
18
+ import { normalizeLlmModelSwitcher, } from "../core-runtime/llm/model-switcher.js";
15
19
  import { resolveOntoHome, } from "../core-runtime/discovery/onto-home.js";
16
- import { isSupportedModelRoute, loadSupportedModelRegistry, } from "../core-runtime/discovery/supported-models.js";
17
- import { resolveLlmProviderConfig, } from "../core-runtime/llm/llm-caller.js";
20
+ import { isSupportedModelRoute, loadSupportedModelRegistry, supportedModelMaxOutputTokens, } from "../core-runtime/discovery/supported-models.js";
21
+ import { applyOpenAIResponsesOutputHeadroom, resolveLlmProviderConfig, } from "../core-runtime/llm/llm-caller.js";
22
+ import { createSealedDispatchCapability, SemanticMapDispatchAccounting, } from "../core-runtime/llm/sealed-dispatch-capability.js";
23
+ import { createReconstructExecutionTelemetryCollector } from "../core-runtime/reconstruct/execution-telemetry.js";
18
24
  import { writeOntologySeedValidationArtifact, writeCandidateDispositionValidationArtifact, } from "../core-runtime/reconstruct/ontology-seed-validation.js";
19
25
  import { writeSourceObservationDirectiveValidationArtifact, } from "../core-runtime/reconstruct/directive-validation.js";
20
26
  import { loadReconstructSourceProfiles, } from "../core-runtime/reconstruct/source-profiles.js";
21
27
  import { assertReconstructDomainId, } from "../core-runtime/reconstruct/domain-id.js";
22
- import { buildReconstructPipelineExecutionLedger, } from "../core-runtime/reconstruct/pipeline-execution-ledger.js";
28
+ import { buildReconstructPipelineExecutionLedger, reconstructStageIdForArtifactRef, reconstructStageOwner, } from "../core-runtime/reconstruct/pipeline-execution-ledger.js";
29
+ import { reconcileReconstructLlmDispatchFailures, } from "../core-runtime/reconstruct/run-control-validation.js";
30
+ import { isReconstructLlmDispatchFailureRef, projectReconstructLlmDispatchFailureSummary, readReconstructLlmDispatchFailureArtifactWithHash, readReconstructLlmDispatchFailureError, } from "../core-runtime/reconstruct/llm-dispatch-failure.js";
23
31
  import { spawnRuntimeWatcherPane, } from "../core-runtime/cli/spawn-watcher.js";
32
+ import { assertPathInsideRoot } from "../core-runtime/path-boundary.js";
24
33
  import { appendRuntimeStatusEventSync, runWithRuntimeObservationContext, } from "../core-runtime/observability/runtime-stream-observation.js";
34
+ function failedReconstructProgress(failure, completedStageRefs) {
35
+ return {
36
+ executionProfile: null,
37
+ currentStageId: failure.unit_id,
38
+ stageCount: RECONSTRUCT_STAGE_IDS.length,
39
+ liveness: {
40
+ state: "halted_or_partial",
41
+ recommendedPollIntervalMs: null,
42
+ },
43
+ countSummary: {
44
+ sourceObservationCount: null,
45
+ selectedObservationCount: null,
46
+ semanticClaimCount: null,
47
+ confirmedClaimCount: null,
48
+ partialClaimCount: null,
49
+ deferredClaimCount: null,
50
+ competencyQuestionCount: null,
51
+ assessmentCount: null,
52
+ failureCount: null,
53
+ revisionProposalCount: null,
54
+ unresolvedCount: null,
55
+ passRate: null,
56
+ },
57
+ answerabilitySummary: null,
58
+ stages: RECONSTRUCT_STAGE_IDS.map((stageId) => ({
59
+ stageId,
60
+ state: stageId === failure.unit_id
61
+ ? "halted"
62
+ : completedStageRefs.has(stageId)
63
+ ? "completed"
64
+ : "pending",
65
+ owner: stageId === failure.unit_id
66
+ ? reconstructStageOwner(stageId)
67
+ : null,
68
+ artifactRefs: stageId === failure.unit_id
69
+ ? [failure.failure_artifact_ref]
70
+ : [...(completedStageRefs.get(stageId) ?? [])],
71
+ reason: stageId === failure.unit_id ? failure.failure_code : null,
72
+ authorityImpact: stageId === failure.unit_id
73
+ ? "The provider output ceiling stopped the owning actor before a canonical stage artifact was accepted."
74
+ : null,
75
+ })),
76
+ };
77
+ }
78
+ async function isRegularRefInsideSession(sessionRoot, artifactRef) {
79
+ const relative = path.relative(sessionRoot, path.resolve(artifactRef));
80
+ if (relative.startsWith("..") || path.isAbsolute(relative))
81
+ return false;
82
+ try {
83
+ const stat = await fs.lstat(artifactRef);
84
+ if (!stat.isFile() || stat.isSymbolicLink())
85
+ return false;
86
+ const [realSessionRoot, realArtifactRef] = await Promise.all([
87
+ fs.realpath(sessionRoot),
88
+ fs.realpath(artifactRef),
89
+ ]);
90
+ const realRelative = path.relative(realSessionRoot, realArtifactRef);
91
+ return !realRelative.startsWith("..") && !path.isAbsolute(realRelative);
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ async function collectFailedStatusArtifactEvidence(args) {
98
+ const reusable = new Set();
99
+ const completedStageRefs = new Map();
100
+ const addStageRef = (stageId, ref) => {
101
+ const refs = completedStageRefs.get(stageId) ?? [];
102
+ if (!refs.includes(ref))
103
+ refs.push(ref);
104
+ completedStageRefs.set(stageId, refs);
105
+ };
106
+ for (const transaction of args.runControl.write_transactions) {
107
+ if (transaction.owner_attempt_id !== args.ownerAttemptId ||
108
+ transaction.transaction_status !== "committed" ||
109
+ !transaction.committed_hash ||
110
+ isReconstructLlmDispatchFailureRef(args.sessionRoot, transaction.artifact_ref) ||
111
+ !(await isRegularRefInsideSession(args.sessionRoot, transaction.artifact_ref))) {
112
+ continue;
113
+ }
114
+ const bytes = await fs.readFile(transaction.artifact_ref);
115
+ const observedHash = crypto.createHash("sha256").update(bytes).digest("hex");
116
+ if (observedHash !== transaction.committed_hash)
117
+ continue;
118
+ reusable.add(transaction.artifact_ref);
119
+ const stageId = reconstructStageIdForArtifactRef(transaction.artifact_ref);
120
+ if (stageId)
121
+ addStageRef(stageId, transaction.artifact_ref);
122
+ }
123
+ return {
124
+ reusableArtifactRefs: [...reusable].sort(),
125
+ completedStageRefs,
126
+ };
127
+ }
128
+ async function trustedFailedSessionStatus(args) {
129
+ if (!args.reconciliation)
130
+ return null;
131
+ const runControl = args.reconciliation.runControl;
132
+ const validation = args.reconciliation.validation ??
133
+ await readYamlArtifactIfPresent(args.runControlValidationRef);
134
+ const latestAttempt = runControl.attempt_rows.at(-1) ?? null;
135
+ if (latestAttempt?.attempt_status !== "failed")
136
+ return null;
137
+ const failureTransactions = runControl.write_transactions.filter((row) => row.owner_attempt_id === latestAttempt.attempt_id &&
138
+ row.transaction_status === "committed" &&
139
+ isReconstructLlmDispatchFailureRef(args.sessionRoot, row.artifact_ref));
140
+ const failureTransaction = failureTransactions.length === 1
141
+ ? failureTransactions[0]
142
+ : null;
143
+ if (!validation ||
144
+ validation.validation_status !== "valid" ||
145
+ validation.current_attempt_id !== latestAttempt.attempt_id ||
146
+ !failureTransaction) {
147
+ throw new Error(`reconstruct session has no trusted failed terminal: ${args.sessionRoot}`);
148
+ }
149
+ const failureRead = await readReconstructLlmDispatchFailureArtifactWithHash({
150
+ sessionRoot: args.sessionRoot,
151
+ artifactRef: failureTransaction.artifact_ref,
152
+ });
153
+ if (failureTransaction.prepared_content_hash !==
154
+ failureTransaction.committed_hash ||
155
+ failureRead.sha256 !== failureTransaction.committed_hash ||
156
+ failureRead.artifact.session_id !== runControl.session_id ||
157
+ failureRead.artifact.owner_attempt_id !== latestAttempt.attempt_id) {
158
+ throw new Error(`trusted failed terminal changed after validation: ${failureTransaction.artifact_ref}`);
159
+ }
160
+ const failure = projectReconstructLlmDispatchFailureSummary(failureRead.artifact, failureTransaction.artifact_ref);
161
+ const artifactEvidence = await collectFailedStatusArtifactEvidence({
162
+ sessionRoot: args.sessionRoot,
163
+ runControl,
164
+ ownerAttemptId: latestAttempt.attempt_id,
165
+ });
166
+ artifactEvidence.completedStageRefs.set("run_control", [args.runControlRef]);
167
+ artifactEvidence.completedStageRefs.set("run_control_validation", [
168
+ args.runControlValidationRef,
169
+ ]);
170
+ return {
171
+ sessionId: runControl.session_id,
172
+ sessionRoot: args.sessionRoot,
173
+ status: "failed",
174
+ artifactRefs: {
175
+ reconstruct_run_control: args.runControlRef,
176
+ reconstruct_run_control_validation: args.runControlValidationRef,
177
+ },
178
+ claimProjection: null,
179
+ claimProjectionValidation: null,
180
+ progress: failedReconstructProgress(failure, artifactEvidence.completedStageRefs),
181
+ reconstructRecord: null,
182
+ runControlRef: args.runControlRef,
183
+ runControlValidationRef: args.runControlValidationRef,
184
+ failure,
185
+ reusableArtifactRefs: artifactEvidence.reusableArtifactRefs,
186
+ };
187
+ }
188
+ export async function recoverReconstructFailedRunStatus(args) {
189
+ const typedFailure = readReconstructLlmDispatchFailureError(args.error);
190
+ if (!typedFailure)
191
+ return null;
192
+ const runControlRef = path.join(args.sessionRoot, "reconstruct-run-control.yaml");
193
+ const runControlValidationRef = path.join(args.sessionRoot, "reconstruct-run-control-validation.yaml");
194
+ const reconciliation = await reconcileReconstructLlmDispatchFailures({
195
+ sessionRoot: args.sessionRoot,
196
+ runControlPath: runControlRef,
197
+ validationOutputPath: runControlValidationRef,
198
+ });
199
+ return trustedFailedSessionStatus({
200
+ sessionRoot: args.sessionRoot,
201
+ reconciliation,
202
+ runControlRef,
203
+ runControlValidationRef,
204
+ });
205
+ }
25
206
  async function directoryExists(directoryPath) {
26
207
  try {
27
208
  const stat = await fs.stat(directoryPath);
@@ -61,7 +242,11 @@ export function resolveJudgeLlmConfig(args) {
61
242
  // INV-MODEL-1 is keyed by MODEL provider (e.g. openai/gpt-5.5), not the
62
243
  // runtime adapter provider (OpenAI OAuth normalizes to codex). Check the
63
244
  // model provider so a supported judge model is not spuriously degraded.
64
- const supported = isSupportedModelRoute(args.judgeModelProvider, candidate.model_id, args.registry);
245
+ const supported = isSupportedModelRoute(args.judgeModelProvider, candidate.model_id, args.registry,
246
+ // Judge adoption is a NAMED non-settings dispatch (design §2.3): a
247
+ // role-restricted entry (e.g. synthesize-only) must not be adoptable
248
+ // as the answer-support judge.
249
+ { kind: "request_judge" });
65
250
  // Credential safety: the candidate resolves on the author's provider, so its
66
251
  // runtime provider must match the author's (guarantees api_key_env/adapter
67
252
  // never cross providers). Uses the runtime provider, not the model provider.
@@ -85,7 +270,74 @@ export function resolveJudgeLlmConfig(args) {
85
270
  judge.reasoning_effort = authorEffort;
86
271
  else
87
272
  delete judge.reasoning_effort;
88
- return { judgeLlmConfig: judge, note };
273
+ const judgeLlmConfig = args.outputHeadroom
274
+ ? applyOpenAIResponsesOutputHeadroom({
275
+ config: judge,
276
+ selection: args.outputHeadroom.selection,
277
+ headroomTokens: args.outputHeadroom.headroomTokens,
278
+ maxBaseOutputTokens: RECONSTRUCT_SEMANTIC_AUTHOR_MAX_BASE_OUTPUT_TOKENS,
279
+ modelMaxOutputTokens: supportedModelMaxOutputTokens(args.registry, args.judgeModelProvider ?? "", judge.model_id ?? ""),
280
+ })
281
+ : judge;
282
+ return { judgeLlmConfig, note };
283
+ }
284
+ /**
285
+ * Mock-safe IDENTITY projection of the optional synthesize seat (design §5.4):
286
+ * carries only the dispatch-identity axes the reuse fold and census read
287
+ * (provider/model/effort/base_url) — deliberately NOT resolveLlmProviderConfig,
288
+ * which validates provider/auth combinations and resolves credentials that a
289
+ * mock run must not require. The identity is deterministic per seat edit, so
290
+ * P3's fold-rotation assertions hold under mock without any live material.
291
+ * (No execution_adapter under mock — the canonical fold serializes it as
292
+ * "default"; live runs fold the real adapter.)
293
+ */
294
+ export function synthesizeSeatIdentityProjection(selection) {
295
+ return {
296
+ ...(selection.provider !== undefined
297
+ ? { provider: selection.provider }
298
+ : {}),
299
+ ...(selection.model !== undefined ? { model_id: selection.model } : {}),
300
+ ...(selection.effort !== undefined
301
+ ? { reasoning_effort: selection.effort }
302
+ : {}),
303
+ ...(selection.base_url !== undefined
304
+ ? { base_url: selection.base_url }
305
+ : {}),
306
+ };
307
+ }
308
+ /**
309
+ * The ONE deterministic seam from post-chain settings to the factory's
310
+ * semantic-map wiring (design §5.4/§5.5): opt-in read, single seat read
311
+ * (resolveOptionalReconstructActorLlmSettings), live provider completion vs
312
+ * mock identity projection, and the dormant-seat honesty note. Coverage is
313
+ * two-layered: unit tests exercise this function over resolved settings, and
314
+ * the seam→factory CONSUMPTION edge is bound by the mock E2Es — the dormant
315
+ * branch via the N11 note, the active branch via the P3 census
316
+ * synthesize_model_identity assertion (dropping either factory spread at the
317
+ * runReconstruct call site fails P3).
318
+ */
319
+ export function resolveSemanticMapSynthesizeWiring(args) {
320
+ const enabled = isReconstructSemanticMapAuthoringEnabled(args.settings);
321
+ const seat = resolveOptionalReconstructActorLlmSettings(args.settings, "semantic_map_synthesize");
322
+ if (seat === undefined)
323
+ return { enableSemanticMapAuthoring: enabled };
324
+ if (!enabled) {
325
+ return {
326
+ enableSemanticMapAuthoring: false,
327
+ dormantSeatNote: "semantic_map_synthesize seat is configured but reconstruct.execution.semantic_map_authoring is off — the seat is dormant (no synthesize dispatch, not gate-validated) until the opt-in is enabled",
328
+ };
329
+ }
330
+ return {
331
+ enableSemanticMapAuthoring: true,
332
+ semanticMapSynthesizeLlmConfig: args.mockRealizationEnabled
333
+ ? synthesizeSeatIdentityProjection(seat)
334
+ : resolveLlmProviderConfig({
335
+ config: { llm: seat },
336
+ ...(args.llmEffortOverride
337
+ ? { cliOverrides: args.llmEffortOverride }
338
+ : {}),
339
+ }),
340
+ };
89
341
  }
90
342
  function dateStamp() {
91
343
  return new Date().toISOString().slice(0, 10).replace(/-/g, "");
@@ -170,15 +422,20 @@ function deriveReconstructProgress(args) {
170
422
  });
171
423
  const lastReachedStage = [...stages].reverse().find((stage) => stage.state !== "pending") ??
172
424
  stages[0];
425
+ // Terminality (completed / graceful-blocked / graceful-limited) is judged once, via the single
426
+ // projection (design §16.7). A graceful terminal is terminal for polling (stop) but not
427
+ // "completed" — its liveness state stays "halted_or_partial" while the poll interval goes null.
428
+ const terminalStatus = reconstructTerminalStatus(args.record);
429
+ const isCompleted = terminalStatus === "completed";
430
+ const isGracefulTerminal = terminalStatus === "blocked" || terminalStatus === "limited";
431
+ const isTerminal = isCompleted || isGracefulTerminal;
173
432
  return {
174
433
  executionProfile: args.runManifest?.execution_profile ?? null,
175
434
  currentStageId: lastReachedStage.stageId,
176
435
  stageCount: RECONSTRUCT_STAGE_IDS.length,
177
436
  liveness: {
178
- state: args.record.record_stage === "completed"
179
- ? "completed"
180
- : "halted_or_partial",
181
- recommendedPollIntervalMs: args.record.record_stage === "completed" ? null : 1000,
437
+ state: isCompleted ? "completed" : "halted_or_partial",
438
+ recommendedPollIntervalMs: isTerminal ? null : 1000,
182
439
  },
183
440
  countSummary: {
184
441
  sourceObservationCount: args.metrics?.source_observation_count ?? null,
@@ -223,6 +480,23 @@ function recordArtifactRefsFromPreparation(refs) {
223
480
  source_observations: refs.source_observations,
224
481
  };
225
482
  }
483
+ export async function tryCreateEligiblePrimarySealedDispatchCapability(args) {
484
+ const selection = normalizeLlmModelSwitcher(args.llm);
485
+ if (!selection ||
486
+ selection.auth !== "api_key" ||
487
+ selection.execution_route !== "direct_model_call" ||
488
+ (selection.execution_adapter !== "openai_sdk" &&
489
+ selection.execution_adapter !== "anthropic_sdk") ||
490
+ selection.base_url !== undefined) {
491
+ return undefined;
492
+ }
493
+ try {
494
+ return await createSealedDispatchCapability(args);
495
+ }
496
+ catch {
497
+ return undefined;
498
+ }
499
+ }
226
500
  export function createOntoReconstructCoreApi(options = {}) {
227
501
  const ontoHome = resolveOntoHome(options.ontoHome);
228
502
  return {
@@ -239,6 +513,11 @@ export function createOntoReconstructCoreApi(options = {}) {
239
513
  const sessionRoot = request.sessionRoot
240
514
  ? resolveFromBase(projectRoot, request.sessionRoot)
241
515
  : createDefaultSessionRoot(projectRoot);
516
+ await assertPathInsideRoot({
517
+ root: projectRoot,
518
+ candidate: sessionRoot,
519
+ label: "reconstruct sessionRoot",
520
+ });
242
521
  const profilesRoot = await resolveProfilesRoot({
243
522
  projectRoot,
244
523
  ...(request.profilesRoot
@@ -285,6 +564,11 @@ export function createOntoReconstructCoreApi(options = {}) {
285
564
  const sessionRoot = request.sessionRoot
286
565
  ? resolveFromBase(projectRoot, request.sessionRoot)
287
566
  : createDefaultSessionRoot(projectRoot);
567
+ await assertPathInsideRoot({
568
+ root: projectRoot,
569
+ candidate: sessionRoot,
570
+ label: "reconstruct sessionRoot",
571
+ });
288
572
  const profilesRoot = await resolveProfilesRoot({
289
573
  projectRoot,
290
574
  ...(request.profilesRoot
@@ -294,6 +578,10 @@ export function createOntoReconstructCoreApi(options = {}) {
294
578
  });
295
579
  const targetRefs = request.targetRefs.map((targetRef) => resolveFromBase(projectRoot, targetRef));
296
580
  const settings = await resolveSettingsChain(ontoHome ?? projectRoot, projectRoot);
581
+ await assertDispatchFallbackSessionAdmission({
582
+ sessionRoot,
583
+ enabled: settings.reconstruct?.execution?.dispatch_fallback?.enabled === true,
584
+ });
297
585
  const semanticAuthorRealization = request.semanticAuthorRealization ?? "direct_call";
298
586
  const confirmationProviderRealization = request.confirmationProviderRealization ?? "direct_call";
299
587
  if (request.domain) {
@@ -313,14 +601,20 @@ export function createOntoReconstructCoreApi(options = {}) {
313
601
  const llmEffortOverride = request.llmEffort
314
602
  ? { reasoning_effort: request.llmEffort }
315
603
  : undefined;
316
- const semanticAuthorLlmConfig = mockRealizationEnabled
317
- ? {}
318
- : resolveLlmProviderConfig({
319
- config: {
320
- llm: resolveReconstructActorLlmSettings(settings, "semantic_author"),
321
- },
604
+ // The semantic_author actor settings (non-mock). The MODEL provider here
605
+ // (e.g. "openai") is the supported-models registry key, DISTINCT from the
606
+ // resolved RUNTIME provider (openai OAuth resolves to "codex"). The document
607
+ // projection-budget lookup keys on the model provider so the default
608
+ // gpt-5.5 OAuth seat resolves (see deriveDocumentExcerptProjectionBudget).
609
+ const semanticAuthorActorLlm = mockRealizationEnabled
610
+ ? null
611
+ : resolveReconstructActorLlmSettings(settings, "semantic_author");
612
+ let semanticAuthorLlmConfig = semanticAuthorActorLlm
613
+ ? resolveLlmProviderConfig({
614
+ config: { llm: semanticAuthorActorLlm },
322
615
  ...(llmEffortOverride ? { cliOverrides: llmEffortOverride } : {}),
323
- });
616
+ })
617
+ : {};
324
618
  const confirmationProviderLlmConfig = mockRealizationEnabled
325
619
  ? {}
326
620
  : resolveLlmProviderConfig({
@@ -329,6 +623,150 @@ export function createOntoReconstructCoreApi(options = {}) {
329
623
  },
330
624
  ...(llmEffortOverride ? { cliOverrides: llmEffortOverride } : {}),
331
625
  });
626
+ // The supported-models registry (non-mock only): loaded once and shared by
627
+ // the judge-override support check and the document projection-budget
628
+ // lookup. Mock realization makes no real provider calls, so it stays
629
+ // decoupled from the install authority file (C6).
630
+ const supportedModelRegistry = mockRealizationEnabled
631
+ ? null
632
+ : loadSupportedModelRegistry();
633
+ const semanticAuthorLlmRuntime = mockRealizationEnabled
634
+ ? undefined
635
+ : resolveReconstructSemanticAuthorLlmRuntimeSettings(settings);
636
+ if (semanticAuthorLlmRuntime && semanticAuthorActorLlm) {
637
+ const selection = normalizeLlmModelSwitcher(semanticAuthorActorLlm);
638
+ if (!selection) {
639
+ throw new Error("semantic_author output headroom requires a resolved LLM selection.");
640
+ }
641
+ semanticAuthorLlmConfig = applyOpenAIResponsesOutputHeadroom({
642
+ config: semanticAuthorLlmConfig,
643
+ selection,
644
+ headroomTokens: semanticAuthorLlmRuntime.openai_responses_output_headroom_tokens,
645
+ maxBaseOutputTokens: RECONSTRUCT_SEMANTIC_AUTHOR_MAX_BASE_OUTPUT_TOKENS,
646
+ modelMaxOutputTokens: supportedModelMaxOutputTokens(supportedModelRegistry, selection.model_provider, semanticAuthorLlmConfig.model_id ?? selection.model_id ?? ""),
647
+ });
648
+ }
649
+ // Semantic-map authoring opt-in + optional synthesize seat (INV-MODEL-1
650
+ // role-aware design §5.4/§5.5): ONE deterministic seam owns the wiring
651
+ // (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).
655
+ const semanticMapWiring = resolveSemanticMapSynthesizeWiring({
656
+ settings,
657
+ mockRealizationEnabled,
658
+ llmEffortOverride,
659
+ });
660
+ // Single seed-stage document projection budget (chars), derived once from
661
+ // the semantic author's MODEL window.
662
+ const documentExcerptProjectionBudget = supportedModelRegistry
663
+ ? deriveDocumentExcerptProjectionBudget({
664
+ ...(semanticAuthorActorLlm?.provider
665
+ ? { provider: semanticAuthorActorLlm.provider }
666
+ : {}),
667
+ ...(semanticAuthorLlmConfig.model_id
668
+ ? { modelId: semanticAuthorLlmConfig.model_id }
669
+ : {}),
670
+ }, supportedModelRegistry)
671
+ : DOCUMENT_EXCERPT_PROJECTION_FLOOR;
672
+ const dispatchFallbackSettings = settings.reconstruct?.execution?.dispatch_fallback;
673
+ const dispatchBreakerSettings = completeDispatchBreakerSettings(settings.reconstruct?.execution?.dispatch_breaker);
674
+ if (semanticAuthorLlmRuntime &&
675
+ dispatchFallbackSettings?.enabled === true) {
676
+ throw new Error("reconstruct output headroom cannot be combined with dispatch_fallback until the sealed semantic-map route preserves the same output-budget and typed-incomplete contract.");
677
+ }
678
+ let dispatchFallbackRuntime;
679
+ let dispatchFallbackTelemetry;
680
+ if (dispatchFallbackSettings?.enabled === true) {
681
+ if (!dispatchBreakerSettings.enabled) {
682
+ throw new Error("reconstruct dispatch_fallback requires reconstruct.execution.dispatch_breaker.enabled=true.");
683
+ }
684
+ if (mockRealizationEnabled) {
685
+ throw new Error("reconstruct dispatch_fallback requires the live sealed SDK path; mock realization is not product evidence.");
686
+ }
687
+ if (!semanticMapWiring.enableSemanticMapAuthoring) {
688
+ throw new Error("reconstruct dispatch_fallback requires reconstruct.execution.semantic_map_authoring=true.");
689
+ }
690
+ const effectiveEffort = request.llmEffort;
691
+ const primarySynthesizeLlm = {
692
+ ...(resolveOptionalReconstructActorLlmSettings(settings, "semantic_map_synthesize") ?? resolveReconstructActorLlmSettings(settings, "semantic_author")),
693
+ ...(effectiveEffort ? { effort: effectiveEffort } : {}),
694
+ };
695
+ const primaryVerifyLlm = {
696
+ ...resolveReconstructActorLlmSettings(settings, "semantic_author"),
697
+ ...(effectiveEffort ? { effort: effectiveEffort } : {}),
698
+ };
699
+ const fallbackLlm = {
700
+ ...dispatchFallbackSettings.llm,
701
+ ...(effectiveEffort ? { effort: effectiveEffort } : {}),
702
+ };
703
+ const [primarySynthesize, primaryVerify, fallbackSynthesize, fallbackVerify] = await Promise.all([
704
+ tryCreateEligiblePrimarySealedDispatchCapability({
705
+ llm: primarySynthesizeLlm,
706
+ operation: "semantic_map_synthesize",
707
+ }),
708
+ tryCreateEligiblePrimarySealedDispatchCapability({
709
+ llm: primaryVerifyLlm,
710
+ operation: "semantic_map_verify",
711
+ }),
712
+ createSealedDispatchCapability({
713
+ llm: fallbackLlm,
714
+ operation: "semantic_map_synthesize",
715
+ }),
716
+ createSealedDispatchCapability({
717
+ llm: fallbackLlm,
718
+ operation: "semantic_map_verify",
719
+ }),
720
+ ]);
721
+ const eligiblePrimaryCapabilities = [
722
+ primarySynthesize,
723
+ primaryVerify,
724
+ ].filter((capability) => capability !== undefined);
725
+ if (eligiblePrimaryCapabilities.length === 0) {
726
+ throw new Error("reconstruct dispatch_fallback requires at least one eligible sealed primary semantic-map operation.");
727
+ }
728
+ const eligiblePrimaryProviders = new Set(eligiblePrimaryCapabilities.map((capability) => capability.public_descriptor.model_provider));
729
+ if ([...eligiblePrimaryProviders].every((provider) => provider === fallbackSynthesize.public_descriptor.model_provider) ||
730
+ fallbackSynthesize.public_descriptor.model_provider !==
731
+ fallbackVerify.public_descriptor.model_provider) {
732
+ throw new Error("reconstruct dispatch_fallback requires one alternate provider for the complete fallback pair.");
733
+ }
734
+ const accounting = new SemanticMapDispatchAccounting();
735
+ dispatchFallbackTelemetry =
736
+ createReconstructExecutionTelemetryCollector({
737
+ nullMixedRouteProjection: true,
738
+ });
739
+ const fallbackLlmConfig = resolveLlmProviderConfig({
740
+ config: { llm: fallbackLlm },
741
+ });
742
+ const fallbackDirectiveAuthor = createDirectCallReconstructDirectiveAuthor({
743
+ llmConfig: fallbackLlmConfig,
744
+ semanticMapSynthesizeLlmConfig: fallbackLlmConfig,
745
+ enableSemanticMapAuthoring: true,
746
+ semanticMapDispatchCapabilities: {
747
+ synthesize: fallbackSynthesize,
748
+ verify: fallbackVerify,
749
+ accounting,
750
+ executionSource: "fallback",
751
+ allowParseRepair: false,
752
+ maxTransportAttempts: 1,
753
+ },
754
+ documentExcerptProjectionBudget,
755
+ executionTelemetry: dispatchFallbackTelemetry,
756
+ });
757
+ dispatchFallbackRuntime = {
758
+ accounting,
759
+ primary: {
760
+ ...(primarySynthesize ? { synthesize: primarySynthesize } : {}),
761
+ ...(primaryVerify ? { verify: primaryVerify } : {}),
762
+ },
763
+ fallback: {
764
+ synthesize: fallbackSynthesize,
765
+ verify: fallbackVerify,
766
+ directiveAuthor: fallbackDirectiveAuthor,
767
+ },
768
+ };
769
+ }
332
770
  // Opt-in per-stage JUDGE config (semantic-independence lever). Default =
333
771
  // inherit the semantic-author config (judgeLlmConfig undefined → no change,
334
772
  // zero regression). A judgeModel override resolves ON THE AUTHOR'S PROVIDER
@@ -347,7 +785,7 @@ export function createOntoReconstructCoreApi(options = {}) {
347
785
  const judgeAuthorActorLlm = !mockRealizationEnabled && request.judgeModel
348
786
  ? resolveReconstructActorLlmSettings(settings, "semantic_author")
349
787
  : null;
350
- const judgeModelCandidate = judgeAuthorActorLlm
788
+ const judgeModelCandidateBase = judgeAuthorActorLlm
351
789
  ? resolveLlmProviderConfig({
352
790
  config: { llm: judgeAuthorActorLlm },
353
791
  cliOverrides: { model: request.judgeModel },
@@ -360,13 +798,24 @@ export function createOntoReconstructCoreApi(options = {}) {
360
798
  ...(request.judgeLlmEffort
361
799
  ? { judgeLlmEffort: request.judgeLlmEffort }
362
800
  : {}),
363
- judgeModelCandidate,
801
+ judgeModelCandidate: judgeModelCandidateBase,
364
802
  // Registry key is the MODEL provider (e.g. openai), not the runtime
365
803
  // adapter (openai OAuth → codex). The judge uses the author's provider.
366
804
  ...(judgeAuthorActorLlm?.provider
367
805
  ? { judgeModelProvider: judgeAuthorActorLlm.provider }
368
806
  : {}),
369
- registry: loadSupportedModelRegistry(),
807
+ ...(judgeAuthorActorLlm && semanticAuthorLlmRuntime
808
+ ? {
809
+ outputHeadroom: {
810
+ selection: normalizeLlmModelSwitcher(judgeAuthorActorLlm),
811
+ headroomTokens: semanticAuthorLlmRuntime
812
+ .openai_responses_output_headroom_tokens,
813
+ },
814
+ }
815
+ : {}),
816
+ // Non-null in this branch: both this and supportedModelRegistry gate on
817
+ // the same mockRealizationEnabled check.
818
+ registry: supportedModelRegistry,
370
819
  });
371
820
  const judgeLlmConfig = judgeResolution.judgeLlmConfig;
372
821
  if (!mockRealizationEnabled)
@@ -374,6 +823,40 @@ export function createOntoReconstructCoreApi(options = {}) {
374
823
  const directiveAuthor = createDirectCallReconstructDirectiveAuthor({
375
824
  llmConfig: semanticAuthorLlmConfig,
376
825
  ...(judgeLlmConfig ? { judgeLlmConfig } : {}),
826
+ // Production opt-in + per-role synthesize override (design §5.5/§5.2)
827
+ // from the single wiring seam. Opt-in absent/false = pair not
828
+ // attached, stage skips — byte-parity with today. A dormant seat
829
+ // (configured, opt-in off) is surfaced via the honest note below.
830
+ ...(semanticMapWiring.enableSemanticMapAuthoring
831
+ ? { enableSemanticMapAuthoring: true }
832
+ : {}),
833
+ ...(semanticMapWiring.semanticMapSynthesizeLlmConfig
834
+ ? {
835
+ semanticMapSynthesizeLlmConfig: semanticMapWiring.semanticMapSynthesizeLlmConfig,
836
+ }
837
+ : {}),
838
+ documentExcerptProjectionBudget,
839
+ ...(dispatchFallbackRuntime
840
+ ? {
841
+ semanticMapDispatchCapabilities: {
842
+ ...(dispatchFallbackRuntime.primary.synthesize
843
+ ? {
844
+ synthesize: dispatchFallbackRuntime.primary.synthesize,
845
+ }
846
+ : {}),
847
+ ...(dispatchFallbackRuntime.primary.verify
848
+ ? { verify: dispatchFallbackRuntime.primary.verify }
849
+ : {}),
850
+ accounting: dispatchFallbackRuntime.accounting,
851
+ executionSource: "primary",
852
+ allowParseRepair: true,
853
+ maxTransportAttempts: 3,
854
+ },
855
+ }
856
+ : {}),
857
+ ...(dispatchFallbackTelemetry
858
+ ? { executionTelemetry: dispatchFallbackTelemetry }
859
+ : {}),
377
860
  ...(mockRealizationEnabled
378
861
  ? {
379
862
  llmCall: callReconstructMockLlm,
@@ -414,6 +897,22 @@ export function createOntoReconstructCoreApi(options = {}) {
414
897
  stageId: "answer_support_judgment",
415
898
  });
416
899
  }
900
+ if (semanticMapWiring.dormantSeatNote) {
901
+ // Honest accounting (design §5.4, N11 / judge-note precedent): the
902
+ // operator configured a synthesize seat but the semantic-map authoring
903
+ // opt-in is off, so the seat is DORMANT — no synthesize dispatch will
904
+ // use it and the gate deliberately excludes it (U6). Emitted BEFORE the
905
+ // run so the inert config is never a silent no-op. This emission binds
906
+ // the seam's DORMANT branch to the live path (N11); the ACTIVE branch
907
+ // is bound by the P3 census-identity assertion on the factory spreads.
908
+ appendRuntimeStatusEventSync({
909
+ pipeline: "reconstruct",
910
+ sessionRoot,
911
+ sourceLabel: "onto_reconstruct",
912
+ message: semanticMapWiring.dormantSeatNote,
913
+ stageId: "start",
914
+ });
915
+ }
417
916
  const watcherResult = spawnRuntimeWatcherPane(projectRoot, sessionRoot, ontoHome);
418
917
  appendRuntimeStatusEventSync({
419
918
  pipeline: "reconstruct",
@@ -444,6 +943,17 @@ export function createOntoReconstructCoreApi(options = {}) {
444
943
  confirmationProviderRealization,
445
944
  directiveAuthor,
446
945
  confirmationProvider,
946
+ // 설계 B: settings가 유일 권위(INV-CFG-1) — 기본 OFF, 완성값은
947
+ // settings chain이 채운다.
948
+ dispatchBreaker: dispatchBreakerSettings,
949
+ ...(settings.reconstruct?.execution?.dispatch_fallback
950
+ ? {
951
+ dispatchFallback: settings.reconstruct.execution.dispatch_fallback,
952
+ }
953
+ : {}),
954
+ ...(dispatchFallbackRuntime
955
+ ? { dispatchFallbackRuntime }
956
+ : {}),
447
957
  filesystemAllowedRoots: request.filesystemAllowedRoots?.map((root) => resolveFromBase(projectRoot, root)) ??
448
958
  [projectRoot],
449
959
  }));
@@ -464,6 +974,12 @@ export function createOntoReconstructCoreApi(options = {}) {
464
974
  message: `reconstruct session failed: ${error instanceof Error ? error.message : String(error)}`,
465
975
  stageId: "complete",
466
976
  });
977
+ const failedStatus = await recoverReconstructFailedRunStatus({
978
+ sessionRoot,
979
+ error,
980
+ });
981
+ if (failedStatus)
982
+ return failedStatus;
467
983
  throw error;
468
984
  }
469
985
  },
@@ -510,7 +1026,33 @@ export function createOntoReconstructCoreApi(options = {}) {
510
1026
  },
511
1027
  async getRunStatus(sessionRoot) {
512
1028
  const resolvedSessionRoot = path.resolve(sessionRoot);
513
- const reconstructRecord = await readYamlArtifact(path.join(resolvedSessionRoot, "reconstruct-record.yaml"));
1029
+ const runControlRef = path.join(resolvedSessionRoot, "reconstruct-run-control.yaml");
1030
+ const runControlValidationRef = path.join(resolvedSessionRoot, "reconstruct-run-control-validation.yaml");
1031
+ const reconciliation = await reconcileReconstructLlmDispatchFailures({
1032
+ sessionRoot: resolvedSessionRoot,
1033
+ runControlPath: runControlRef,
1034
+ validationOutputPath: runControlValidationRef,
1035
+ });
1036
+ const blockedPartialWrite = reconciliation
1037
+ ? [...reconciliation.runControl.resume_rows].reverse().find((row) => row.resume_decision === "blocked_partial_write")
1038
+ : undefined;
1039
+ if (blockedPartialWrite) {
1040
+ throw new Error(`reconstruct session is blocked by a partial failure write: ${blockedPartialWrite.stale_artifact_refs.join(",") || "unknown ref"}`);
1041
+ }
1042
+ const failedStatus = await trustedFailedSessionStatus({
1043
+ sessionRoot: resolvedSessionRoot,
1044
+ reconciliation,
1045
+ runControlRef,
1046
+ runControlValidationRef,
1047
+ });
1048
+ if (failedStatus)
1049
+ return failedStatus;
1050
+ const reconstructRecord = await readYamlArtifactIfPresent(path.join(resolvedSessionRoot, "reconstruct-record.yaml"));
1051
+ if (!reconstructRecord) {
1052
+ throw new Error(reconciliation
1053
+ ? `reconstruct session has no readable terminal record or trusted failed terminal: ${resolvedSessionRoot}`
1054
+ : `reconstruct session has neither a record nor run-control: ${resolvedSessionRoot}`);
1055
+ }
514
1056
  const reconstructRunManifest = await readYamlArtifactIfPresent(reconstructRecord.artifact_refs.reconstruct_run_manifest);
515
1057
  const reconstructMetrics = await readYamlArtifactIfPresent(reconstructRecord.artifact_refs.reconstruct_metrics);
516
1058
  const claimProjection = await readYamlArtifactIfPresent(reconstructRecord.artifact_refs.claim_projection);
@@ -526,7 +1068,7 @@ export function createOntoReconstructCoreApi(options = {}) {
526
1068
  return {
527
1069
  sessionId: path.basename(resolvedSessionRoot),
528
1070
  sessionRoot: resolvedSessionRoot,
529
- status: reconstructRecord.record_stage,
1071
+ status: reconstructTerminalStatus(reconstructRecord),
530
1072
  artifactRefs: reconstructRecord.artifact_refs,
531
1073
  claimProjection,
532
1074
  claimProjectionValidation,
@@ -541,6 +1083,15 @@ export function createOntoReconstructCoreApi(options = {}) {
541
1083
  },
542
1084
  async getRunResult(sessionRoot) {
543
1085
  const status = await this.getRunStatus(sessionRoot);
1086
+ if (status.status === "failed") {
1087
+ return {
1088
+ ...status,
1089
+ finalOutputPath: null,
1090
+ finalOutputText: null,
1091
+ reconstructRunManifestPath: null,
1092
+ reconstructRunManifest: null,
1093
+ };
1094
+ }
544
1095
  const finalOutputPath = status.reconstructRecord.artifact_refs.final_output;
545
1096
  const reconstructRunManifestPath = status.reconstructRecord.artifact_refs.reconstruct_run_manifest;
546
1097
  return {