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
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { resolveOntoHome } from "../core-runtime/discovery/onto-home.js";
8
- import { defaultReviewRetrySettings, resolveSettingsChain, } from "../core-runtime/discovery/settings-chain.js";
8
+ import { completeDispatchBreakerSettings, defaultReviewRetrySettings, resolveSettingsChain, } from "../core-runtime/discovery/settings-chain.js";
9
9
  import { loadCoreLensRegistry } from "../core-runtime/discovery/lens-registry.js";
10
10
  import { fileExists, isDeprecatedDomainAlias, isoFromTimestamp, isoNow, normalizeDomainValue, readYamlDocument, writeYamlDocument, } from "../core-runtime/review/review-artifact-utils.js";
11
11
  import { assertPathInsideRoot, realpathIfExists, } from "../core-runtime/path-boundary.js";
@@ -29,6 +29,7 @@ import { appendRuntimeStatusEventSync, } from "../core-runtime/observability/run
29
29
  import { buildReviewPipelineExecutionLedger, } from "../core-runtime/review/pipeline-execution-ledger.js";
30
30
  import { buildReviewContinuationPlan, } from "../core-runtime/review/continuation-plan.js";
31
31
  import { reviewAdvance as coreReviewAdvance, reviewRound as coreReviewRound, } from "../core-runtime/review/review-execution-steps.js";
32
+ import { dispatchIncompleteArtifactPath } from "../core-runtime/llm/dispatch-breaker.js";
32
33
  export class ReviewContinuationError extends Error {
33
34
  failureContent;
34
35
  originalError;
@@ -1228,6 +1229,7 @@ function buildHaltPresentation(input) {
1228
1229
  prompt: [
1229
1230
  "Render a concise halted-partial onto review update from bounded runtime facts.",
1230
1231
  "Use only the provided input facts. Lead with halt identity, produced artifacts, absent artifacts, and available action candidates.",
1232
+ "When run_control.continuationAvailable is true, present the default recovery as calling onto_review_continue with the same sessionRoot: it resumes the session, reuses already-completed units, and reruns only the failed/missing frontier — prefer it over restarting a new review. This operational resume is distinct from the continue_review action-candidate, which means expanding the review's evidence boundary.",
1231
1233
  "Do not present partial findings as a completed review. Use the user's conversation language.",
1232
1234
  ].join("\n"),
1233
1235
  input,
@@ -1511,12 +1513,28 @@ async function deriveRuntimeUnitProgress(args) {
1511
1513
  unitKind: "lens",
1512
1514
  packetPath,
1513
1515
  outputPath: seat.output_path,
1516
+ sidecarOutputPath: seat.sidecar_output_path ?? null,
1514
1517
  runningLogRef: path.join(path.dirname(seat.output_path), `.${seat.lens_id}.running.log`),
1515
1518
  };
1516
1519
  });
1517
1520
  const projections = [];
1518
1521
  for (const unit of lensUnits) {
1519
- const output = await observeFile(unit.outputPath);
1522
+ // In sidecar mode (review.artifacts.write_lens_markdown=false) the authored
1523
+ // `.md` is never written and the lens's real artifact is the findings
1524
+ // sidecar (`.findings.yaml`). Resolve the unit's output to whichever artifact
1525
+ // actually exists, preferring the authored `.md`, so drill-down and the
1526
+ // output-presence completion signal see the real file instead of an absent
1527
+ // planned path.
1528
+ const authoredOutput = await observeFile(unit.outputPath);
1529
+ let output = authoredOutput;
1530
+ let outputPath = unit.outputPath;
1531
+ if (!authoredOutput.exists && unit.sidecarOutputPath) {
1532
+ const sidecar = await observeFile(unit.sidecarOutputPath);
1533
+ if (sidecar.exists) {
1534
+ output = sidecar;
1535
+ outputPath = unit.sidecarOutputPath;
1536
+ }
1537
+ }
1520
1538
  const runningLog = await observeFile(unit.runningLogRef);
1521
1539
  const signals = signalsByUnit.get(unit.unitId) ?? [];
1522
1540
  const latestSignal = latestRuntimeSignal(signals);
@@ -1579,7 +1597,7 @@ async function deriveRuntimeUnitProgress(args) {
1579
1597
  progressStepId: runtimeUnitStepId(unit.unitKind),
1580
1598
  status,
1581
1599
  packetPath: unit.packetPath,
1582
- outputPath: unit.outputPath,
1600
+ outputPath,
1583
1601
  runningLogRef: runningLog.exists ? unit.runningLogRef : null,
1584
1602
  latestSignal: latestSignalName,
1585
1603
  latestSignalAt,
@@ -2156,7 +2174,10 @@ function reviewExecutionHostFromRoute(args) {
2156
2174
  return args.modelProvider;
2157
2175
  return reviewExecutionHostFromRuntime(args.hostRuntime, args.workerExecutor);
2158
2176
  }
2159
- function reviewRetrySettingsFromUnknown(value) {
2177
+ /** Exported for tests: pins the continue-path round-trip contract — every
2178
+ * opt-in stamped in the manifest retry policy (salvage, resubmit) must
2179
+ * survive profile reconstruction, or a resumed run silently loses it. */
2180
+ export function reviewRetrySettingsFromUnknown(value) {
2160
2181
  if (!value || typeof value !== "object" || Array.isArray(value))
2161
2182
  return null;
2162
2183
  const record = value;
@@ -2201,7 +2222,47 @@ function reviewRetrySettingsFromUnknown(value) {
2201
2222
  ...(transcription !== undefined ? { transcription_llm: transcription } : {}),
2202
2223
  delta_completion: "unit_llm",
2203
2224
  };
2204
- return { ...retry, salvage };
2225
+ // Round-trip the resubmit opt-in for the same reason as salvage: a
2226
+ // resubmit-enabled run resumed via continue must not silently revert to
2227
+ // the whole-run-halt promotion rule (artifacts stamped before the field
2228
+ // existed resume with resubmit disabled — conservative).
2229
+ const resubmitRecord = record.resubmit &&
2230
+ typeof record.resubmit === "object" &&
2231
+ !Array.isArray(record.resubmit)
2232
+ ? record.resubmit
2233
+ : undefined;
2234
+ const resubmit = {
2235
+ enabled: resubmitRecord?.enabled === true,
2236
+ };
2237
+ // Round-trip the dispatch breaker for the same reason: the canonical
2238
+ // recovery flow for a TRIPPED batch is onto_review_continue during the
2239
+ // same outage — dropping the key here would resume the incomplete set
2240
+ // with the breaker silently OFF (the exact retry-storm class 설계 B
2241
+ // prevents). Artifacts stamped before the field existed resume with the
2242
+ // breaker disabled — conservative (completeDispatchBreakerSettings
2243
+ // defaults enabled:false).
2244
+ const breakerRecord = record.dispatch_breaker &&
2245
+ typeof record.dispatch_breaker === "object" &&
2246
+ !Array.isArray(record.dispatch_breaker)
2247
+ ? record.dispatch_breaker
2248
+ : undefined;
2249
+ const breakerNumber = (item, min) => typeof item === "number" && Number.isInteger(item) && item >= min
2250
+ ? item
2251
+ : undefined;
2252
+ const breakerInput = {
2253
+ enabled: breakerRecord?.enabled === true,
2254
+ systemic_threshold: breakerNumber(breakerRecord?.systemic_threshold, 1),
2255
+ per_call_max_attempts: breakerNumber(breakerRecord?.per_call_max_attempts, 1),
2256
+ backoff_initial_ms: breakerNumber(breakerRecord?.backoff_initial_ms, 0),
2257
+ backoff_cap_ms: breakerNumber(breakerRecord?.backoff_cap_ms, 0),
2258
+ };
2259
+ const dispatchBreaker = completeDispatchBreakerSettings(breakerInput);
2260
+ return {
2261
+ ...retry,
2262
+ salvage,
2263
+ resubmit,
2264
+ dispatch_breaker: dispatchBreaker,
2265
+ };
2205
2266
  }
2206
2267
  function reviewExecutionProfileFromManifest(manifest) {
2207
2268
  const profile = manifest?.review_execution_profile;
@@ -2480,6 +2541,10 @@ function continuationSessionArtifactRefs(args) {
2480
2541
  args.executionPlan.deliberation_output_path,
2481
2542
  args.executionPlan.final_output_path,
2482
2543
  args.executionPlan.review_record_path,
2544
+ // 설계 B: 트립 배치의 회복 집합 기록 — continuation이 breaker-ON 배치를
2545
+ // 재실행하면 덮어써지므로, 직전 상태를 continuation-attempts/에 백업해
2546
+ // 어느 트립 상태를 회복했는지 감사 가능하게 남긴다.
2547
+ dispatchIncompleteArtifactPath(args.sessionRoot),
2483
2548
  ];
2484
2549
  }
2485
2550
  async function resolveContinuationRequestText(args) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * core-api re-export of the review progress-step contract (the canonical step
3
+ * ids, ordered specs, and label lookup that review-api itself uses), so
4
+ * consumers like the `onto watch` TUI reuse the same label authority through
5
+ * core-api instead of reaching into core-runtime/review or duplicating labels.
6
+ */
7
+ export { REVIEW_PROGRESS_STEPS, reviewProgressStepById, } from "../core-runtime/review/review-progress-contract.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * core-api re-export of the runtime event-stream surface, so consumers (the
3
+ * `onto watch` TUI) depend on core-api only — never reaching into
4
+ * core-runtime/observability directly. The event log is the append-only
5
+ * `runtime-events.ndjson` produced by every review/reconstruct session.
6
+ */
7
+ export { runtimeStreamEventLogPath, } from "../core-runtime/observability/runtime-stream-observation.js";
@@ -1,3 +1,4 @@
1
+ import { constants as fsConstants } from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { stringify as stringifyYaml } from "yaml";
@@ -35,13 +36,84 @@ export async function atomicWriteFile(filePath, contents) {
35
36
  throw error;
36
37
  }
37
38
  }
39
+ async function fsyncDirectory(directoryPath) {
40
+ const handle = await fs.open(directoryPath, fsConstants.O_RDONLY);
41
+ try {
42
+ await handle.sync();
43
+ }
44
+ finally {
45
+ await handle.close();
46
+ }
47
+ }
38
48
  /**
39
- * Serialize `value` to YAML and write it atomically. Byte-for-byte identical
40
- * output to a direct `stringifyYaml(value)` write only the write mechanism
49
+ * Atomic write with crash-durable file and directory metadata. Use for small
50
+ * control-plane authorities whose rename must survive a process or host crash.
51
+ */
52
+ export async function durableAtomicWriteFile(filePath, contents) {
53
+ const directoryPath = path.dirname(filePath);
54
+ await fs.mkdir(directoryPath, { recursive: true });
55
+ tempWriteCounter += 1;
56
+ const tempPath = `${filePath}.${process.pid}.${tempWriteCounter}.durable.tmp`;
57
+ let handle = null;
58
+ try {
59
+ handle = await fs.open(tempPath, fsConstants.O_CREAT |
60
+ fsConstants.O_EXCL |
61
+ fsConstants.O_WRONLY |
62
+ fsConstants.O_NOFOLLOW, 0o600);
63
+ await handle.writeFile(contents, "utf8");
64
+ await handle.sync();
65
+ await handle.close();
66
+ handle = null;
67
+ await fs.rename(tempPath, filePath);
68
+ await fsyncDirectory(directoryPath);
69
+ }
70
+ catch (error) {
71
+ await handle?.close().catch(() => undefined);
72
+ await fs.rm(tempPath, { force: true });
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * Fields stamped on in-memory artifacts for same-process consumers but NEVER persisted. Persisting one
78
+ * changes the on-disk bytes, which feeds reuse-provenance: the scout-pack snapshot hashes the raw
79
+ * validation-file bytes via `sha256File`, so a persisted field rotates that snapshot (the
80
+ * `reuseMatchArtifactHash` in-memory digest is neutralized separately, in run.ts
81
+ * `stripVolatileArtifactFields`). The only such field today is the G(a) obligation-coverage telemetry
82
+ * (`asserted_obligation_ids`): validators stamp it and the coverage harvest reads it off the in-memory
83
+ * return value — no consumer reads it from disk. Keeping it out of persistence lets the reuse-hashed /
84
+ * scout-captured validators be instrumented with zero hash rotation and no resume migration.
85
+ * See development-records/design/20260623-deferred7-obligation-telemetry-design.md.
86
+ */
87
+ const IN_MEMORY_ONLY_ARTIFACT_FIELDS = ["asserted_obligation_ids"];
88
+ /**
89
+ * Return `value` with any in-memory-only telemetry fields dropped from the top level. Top-level only by
90
+ * design: validation artifacts carry these fields at top level and the reuse channels only hash
91
+ * standalone validation files, so a recursive walk would add an O(size) cost on every large artifact
92
+ * write (e.g. a 190K-row workbook inventory) for no benefit. Returns the input unchanged (no copy) when
93
+ * no such field is present — the common case for every non-validation artifact.
94
+ */
95
+ export function stripInMemoryOnlyArtifactFields(value) {
96
+ if (!value || typeof value !== "object" || Array.isArray(value))
97
+ return value;
98
+ const record = value;
99
+ if (!IN_MEMORY_ONLY_ARTIFACT_FIELDS.some((key) => key in record))
100
+ return value;
101
+ const persisted = { ...record };
102
+ for (const key of IN_MEMORY_ONLY_ARTIFACT_FIELDS)
103
+ delete persisted[key];
104
+ return persisted;
105
+ }
106
+ /**
107
+ * Serialize `value` to YAML and write it atomically, dropping in-memory-only telemetry fields
108
+ * (see `stripInMemoryOnlyArtifactFields`). Byte-for-byte identical to a direct `stringifyYaml(value)`
109
+ * write for any value without those fields — only the write mechanism (and the telemetry omission)
41
110
  * changes.
42
111
  */
43
112
  export async function atomicWriteYamlDocument(filePath, value) {
44
- await atomicWriteFile(filePath, stringifyYaml(value));
113
+ await atomicWriteFile(filePath, stringifyYaml(stripInMemoryOnlyArtifactFields(value)));
114
+ }
115
+ export async function durableAtomicWriteYamlDocument(filePath, value) {
116
+ await durableAtomicWriteFile(filePath, stringifyYaml(stripInMemoryOnlyArtifactFields(value)));
45
117
  }
46
118
  /**
47
119
  * Fail-closed shape guard for trusted artifact reads. The pipeline reads its
@@ -30,9 +30,10 @@ import { SALVAGE_INCOMPLETE_SENTINEL, buildDeltaRowsSalvagePrompt, buildTranscri
30
30
  const CLAUDE_READONLY_ALLOWED_TOOLS = ["Read", "Grep", "Glob"];
31
31
  const CLAUDE_BIN = resolveClaudeBin();
32
32
  /**
33
- * Embed the submit-tool JSON Schema into the bounded prompt. Claude Code's
34
- * `--json-schema` flag silently rejects the runtime's complex submit schemas,
35
- * so the schema travels in-prompt and the submit tool validates the result.
33
+ * Embed the submit-tool JSON Schema into the bounded prompt as a redundant
34
+ * semantic hint. The same schema is also passed through Claude Code's
35
+ * `--json-schema` flag when available; the submit tool remains the artifact
36
+ * validator.
36
37
  */
37
38
  function appendSchemaToPrompt(boundedPrompt, schema) {
38
39
  if (schema === undefined)
@@ -192,6 +193,7 @@ async function runSubmitSalvageMode(args) {
192
193
  process.stderr.write(`[plan:executor] kind=claude_code unit_id=${args.unitId} salvage=${mode.mode}\n`);
193
194
  const workerBase = {
194
195
  projectRoot: args.projectRoot,
196
+ jsonSchema: args.submitSchema.schema,
195
197
  sandboxMode: args.sandboxMode,
196
198
  unitId: args.unitId,
197
199
  unitKind: args.unitKind,
@@ -269,11 +271,19 @@ async function runClaudeWorker(args) {
269
271
  if (args.reasoningEffort && args.reasoningEffort.length > 0) {
270
272
  claudeArgs.push("--effort", args.reasoningEffort);
271
273
  }
272
- // The schema is embedded in the prompt rather than passed via --json-schema:
273
- // claude's --json-schema validator silently rejects the runtime's complex
274
- // submit schemas (additionalProperties:false + deep required) and exits with
275
- // no output. The submit tool remains the authoritative validator.
276
- //
274
+ // --json-schema constrains output at the API level (grammar-based structured
275
+ // outputs) so the model CANNOT emit syntactically malformed JSON; the CLI then
276
+ // returns a parsed `structured_output` object (extractClaudeStructuredPayload
277
+ // reads that first). Empirically (opus-4.8 + haiku, 2026-07) the flag accepts
278
+ // the runtime's submit schemas (additionalProperties:false + deep required,
279
+ // ~20 params, enums) and returns native objects — the earlier "silently
280
+ // rejects complex schemas" note was inaccurate. Without this, opus free-forms
281
+ // the JSON in-prompt and can drop a comma on large findings → output_contract
282
+ // failure with no recovery. The in-prompt schema stays as a redundant semantic
283
+ // hint; the submit tool remains the authoritative VALIDATOR of field content.
284
+ if (args.jsonSchema !== undefined) {
285
+ claudeArgs.push("--json-schema", JSON.stringify(args.jsonSchema));
286
+ }
277
287
  // Never load project/user MCP servers in the bounded worker (no side effects,
278
288
  // no mcp__* tools to auto-approve).
279
289
  claudeArgs.push("--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}');
@@ -557,6 +567,7 @@ export async function runClaudeCodeReviewUnitExecutorCli(argv) {
557
567
  const stdout = await runClaudeWorker({
558
568
  projectRoot,
559
569
  boundedPrompt,
570
+ jsonSchema: submitSchema?.schema,
560
571
  model,
561
572
  reasoningEffort,
562
573
  sandboxMode,
@@ -6,7 +6,9 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { reviewMaterialGoals } from "../target-material-kind.js";
9
10
  import { fileExists, isoNow, readYamlDocument, writeYamlDocument, toRelativePath, truncateForEmbedding, } from "../review/review-artifact-utils.js";
11
+ import { DEFAULT_MAX_EMBED_LINES } from "../review/review-prompt-budget.js";
10
12
  import { printOntoReleaseChannelNotice } from "../release-channel/release-channel.js";
11
13
  import { writeAndThrowStructuredFailureRecord } from "../review/failure-records.js";
12
14
  import { resolveInstallationPath } from "../discovery/installation-paths.js";
@@ -52,19 +54,52 @@ export function renderEmbeddedMaterializedInputSection(materializedInput) {
52
54
  ${materializedInput}
53
55
  <!-- onto:embedded-materialized-input:end -->`;
54
56
  }
55
- function materialKindReviewObligations(targetMaterialKind) {
56
- switch (targetMaterialKind) {
57
+ function materialKindReviewObligations(profile) {
58
+ switch (profile.target_material_kind) {
57
59
  case "code":
58
60
  return [
59
61
  "Treat declared types, exported API signatures, documented contracts, and observable runtime behavior as review evidence.",
60
62
  "Check visible type/runtime contract mismatches, edge-case input behavior, error/null/undefined paths, and caller-facing failure modes.",
61
63
  "Classify a visible correctness or runtime-contract failure as material when it can violate the declared review goal inside the bounded target.",
62
64
  ];
63
- case "spreadsheet":
65
+ case "spreadsheet": {
66
+ // The obligation prose stays in lockstep with the per-ref disposition projected into
67
+ // review_goal (the SSOT): a goal is named only when its specific structure was
68
+ // rendered. Three honest cases, so the lens is never told to audit absent formulas nor
69
+ // told the data is unavailable when it was actually rendered:
70
+ const spreadsheetGoals = reviewMaterialGoals("spreadsheet");
71
+ const backed = profile.review_goal.filter((goal) => spreadsheetGoals.includes(goal));
72
+ if (backed.length > 0) {
73
+ // Project the prose from the BACKED goal SUBSET (not a single any-backed bit), so the
74
+ // lens is told to audit only the structure the inventory actually rendered for this
75
+ // target — a macro-only or validation-only workbook is never told to audit formulas
76
+ // that review_goal and the render both show are absent. Emitted in stable catalog order.
77
+ const GOAL_PROSE = {
78
+ formula_integrity: "Treat formulas and their recalculation behavior as review evidence; check visible formula mismatches, stale derived values, and decision-impacting calculation errors.",
79
+ cross_sheet_reference_integrity: "Treat cross-sheet references as review evidence; check broken, stale, or misaimed references across sheets.",
80
+ named_range_hygiene: "Treat named ranges as review evidence; check missing, overlapping, or wrongly-scoped named ranges.",
81
+ data_validation_coverage: "Treat data-validation rules (type, operator, bounds) as review evidence; check missing or inconsistent input guards.",
82
+ access_and_protection_hygiene: "Treat sheet protection, hidden sheets, and macro presence as review evidence; check the access/protection posture and any macro-carried risk.",
83
+ structural_risk_signals: "Treat the recorded structural risk signals, external links, and error cells as review evidence; check decision-impacting structural risks.",
84
+ };
85
+ return spreadsheetGoals
86
+ .filter((goal) => backed.includes(goal))
87
+ .map((goal) => GOAL_PROSE[goal]);
88
+ }
89
+ // No structural obligation is backed. Distinguish an inspected plain-data workbook
90
+ // (read — e.g. a flat CSV — but with no formula/named-range/validation/protection
91
+ // structure to audit) from one that could not be inspected at all.
92
+ if (profile.material_profile.support_status === "supported") {
93
+ return [
94
+ "The target workbook(s) were structurally inspected but carry no formula, cross-sheet reference, named range, data validation, or protection structure to audit beyond the rendered columns/data.",
95
+ "Treat the rendered structural inventory as the evidence; do not infer calculation logic the workbook does not contain.",
96
+ ];
97
+ }
64
98
  return [
65
- "Treat formulas, cross-sheet references, named ranges, input assumptions, and recalculation behavior as review evidence.",
66
- "Check visible formula/reference mismatches, stale derived values, missing input guards, and decision-impacting calculation errors.",
99
+ "The target workbook(s) could not be structurally inspected (unsupported format, unreadable, or empty); the materialized input carries only an unsupported note, not formula/reference structure.",
100
+ "Treat only what the materialized input actually renders as evidence; do not assume formulas, cross-sheet references, or recalculated values are available, and preserve material uncertainty about the uninspected structure.",
67
101
  ];
102
+ }
68
103
  case "document":
69
104
  return [
70
105
  "Treat declared purpose, audience, claims, evidence, structure, and unresolved ambiguity as review evidence.",
@@ -87,13 +122,13 @@ function materialKindReviewObligations(targetMaterialKind) {
87
122
  ];
88
123
  }
89
124
  }
90
- function renderReviewTargetProfileSummary(profile) {
125
+ export function renderReviewTargetProfileSummary(profile) {
91
126
  if (!profile) {
92
127
  return `## Review Target Profile Summary
93
128
  - profile: unavailable
94
129
  - consequence: use only the materialized input and explicit request summary as target authority.`;
95
130
  }
96
- const obligations = materialKindReviewObligations(profile.target_material_kind);
131
+ const obligations = materialKindReviewObligations(profile);
97
132
  return `## Review Target Profile Summary
98
133
  - target_material_kind: ${profile.target_material_kind}
99
134
  - target_input_kind: ${profile.target_input_kind}
@@ -190,7 +225,9 @@ Submit \`domain_context_assumptions\` as a list of strings.
190
225
  If there are no findings, submit \`findings: []\` and a concise \`no_findings_rationale\`.
191
226
  ${args.humanOutputPath ? `The runtime may render a human-readable projection at ${toRelativePath(args.humanOutputPath, args.projectRoot)}.` : "No human-readable lens markdown projection is requested for this session."}`;
192
227
  }
193
- const DEFAULT_MAX_EMBED_LINES = 300;
228
+ // DEFAULT_MAX_EMBED_LINES is owned by review-prompt-budget.ts (the single
229
+ // conversion point), imported above and shared with the prepare-time budget
230
+ // resolution so the no-regression floor lives in one place.
194
231
  // Core role IDs derived from .onto/authority/core-lens-registry.yaml (single source of truth)
195
232
  import { loadCoreLensRegistry } from "../discovery/lens-registry.js";
196
233
  const CORE_ROLE_IDS = new Set(loadCoreLensRegistry().core_role_ids);
@@ -731,12 +768,20 @@ export async function runMaterializeReviewPromptPacketsCli(argv) {
731
768
  allowPositionals: false,
732
769
  args: argv,
733
770
  });
734
- let maxEmbedLines = typeof values["max-embed-lines"] === "string" && values["max-embed-lines"].length > 0
735
- ? Number.parseInt(values["max-embed-lines"], 10)
736
- : DEFAULT_MAX_EMBED_LINES;
737
- if (!Number.isFinite(maxEmbedLines) || maxEmbedLines < 1) {
738
- console.warn(`[onto] Invalid max-embed-lines value (${values["max-embed-lines"]}), using default ${DEFAULT_MAX_EMBED_LINES}.`);
739
- maxEmbedLines = DEFAULT_MAX_EMBED_LINES;
771
+ // Explicit CLI --max-embed-lines wins (an operator override). An invalid value
772
+ // falls back (warned) so it does NOT mask the plan's window-proportional budget.
773
+ // The plan's persisted max_embed_lines (Stage 1) is applied below, after the
774
+ // plan is read; absent both → DEFAULT (no regression).
775
+ let cliMaxEmbedLines;
776
+ if (typeof values["max-embed-lines"] === "string" &&
777
+ values["max-embed-lines"].length > 0) {
778
+ const parsed = Number.parseInt(values["max-embed-lines"], 10);
779
+ if (Number.isFinite(parsed) && parsed >= 1) {
780
+ cliMaxEmbedLines = parsed;
781
+ }
782
+ else {
783
+ console.warn(`[onto] Invalid max-embed-lines value (${values["max-embed-lines"]}), using default ${DEFAULT_MAX_EMBED_LINES}.`);
784
+ }
740
785
  }
741
786
  const projectRoot = path.resolve(requireString(values["project-root"], "project-root"));
742
787
  const sessionRoot = path.resolve(requireString(values["session-root"], "session-root"));
@@ -749,6 +794,15 @@ export async function runMaterializeReviewPromptPacketsCli(argv) {
749
794
  const binding = await readYamlDocument(bindingPath);
750
795
  const sessionMetadata = await readYamlDocument(sessionMetadataPath);
751
796
  const executionPlan = await readYamlDocument(executionPlanPath);
797
+ // Stage 1 embed-budget precedence: explicit CLI override wins; else the plan's
798
+ // window-proportional max_embed_lines (persisted at prepare time); else DEFAULT
799
+ // (model-unaware run — no regression).
800
+ const planMaxEmbedLines = typeof executionPlan.max_embed_lines === "number" &&
801
+ Number.isFinite(executionPlan.max_embed_lines) &&
802
+ executionPlan.max_embed_lines >= 1
803
+ ? executionPlan.max_embed_lines
804
+ : undefined;
805
+ const maxEmbedLines = cliMaxEmbedLines ?? planMaxEmbedLines ?? DEFAULT_MAX_EMBED_LINES;
752
806
  const promptPacketsRoot = executionPlan.prompt_packets_root ?? path.join(sessionRoot, "prompt-packets");
753
807
  const materializedInputText = await readOptionalText(binding.materialized_input_path);
754
808
  const reviewTargetProfile = await readYamlDocument(binding.review_target_profile_path);
@@ -3,11 +3,32 @@ import path from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { DEFAULT_DIRECTORY_LISTING_OPTIONS, hasOptionFlag, parseBooleanFlag, } from "../review/review-artifact-utils.js";
6
- import { bootstrapInvocationBindingArtifacts, materializeReviewExecutionPreparationArtifacts, writeInvocationInterpretationArtifact, } from "../review/materializers.js";
6
+ import { bootstrapInvocationBindingArtifacts, materializeReviewExecutionPreparationArtifacts, resolveReviewExecutionSettingsForArtifacts, writeInvocationInterpretationArtifact, } from "../review/materializers.js";
7
+ import { DEFAULT_MAX_EMBED_LINES, deriveReviewMaxEmbedLines, resolveReviewLensContextWindowTokens, reviewWindowMultiplier, } from "../review/review-prompt-budget.js";
8
+ import { deriveWorkbookInventoryPromptCaps } from "../spreadsheet-structure-observer.js";
9
+ import { loadSupportedModelRegistry } from "../discovery/supported-models.js";
7
10
  import { printOntoReleaseChannelNotice } from "../release-channel/release-channel.js";
8
11
  import { detectCodexEnvSignal } from "../discovery/host-detection.js";
9
12
  import { resolveOntoHome } from "../discovery/onto-home.js";
10
13
  import { resolveSettingsChain } from "../discovery/settings-chain.js";
14
+ /**
15
+ * Stage 1 — resolve the window-proportional review prompt budget multiplier from
16
+ * the lens model's registered context window. The registry load and model
17
+ * resolution are best-effort: any failure (missing/malformed authority,
18
+ * unresolved model, no registered window) returns multiplier 1, so the budget
19
+ * floors to DEFAULT caps + DEFAULT embed lines (no regression). Never throws.
20
+ */
21
+ function resolveReviewPromptBudget(ontoConfig) {
22
+ try {
23
+ const registry = loadSupportedModelRegistry();
24
+ const execution = resolveReviewExecutionSettingsForArtifacts(ontoConfig);
25
+ const window = resolveReviewLensContextWindowTokens(execution, registry);
26
+ return { multiplier: reviewWindowMultiplier(window) };
27
+ }
28
+ catch {
29
+ return { multiplier: 1 };
30
+ }
31
+ }
11
32
  function requireString(value, optionName) {
12
33
  if (typeof value !== "string" || value.length === 0) {
13
34
  throw new Error(`Missing required option --${optionName}`);
@@ -175,6 +196,16 @@ export async function runPrepareReviewSessionCli(argv) {
175
196
  const projectRoot = path.resolve(requireString(values["project-root"], "project-root"));
176
197
  const ontoHome = resolveOntoHome(optionalString(values["onto-home"]) || undefined);
177
198
  const ontoConfig = await resolveSettingsChain(ontoHome, projectRoot);
199
+ // Stage 1 — resolve the window-proportional review prompt budget ONCE, here,
200
+ // and thread it to both consumers: the spreadsheet inventory prompt caps
201
+ // (into the materialize call) and the embed line budget (persisted into the
202
+ // execution plan for the packet stage). The lens MODEL's registered context
203
+ // window drives a single multiplier. Any failure (registry load, unresolved
204
+ // model, no registered window) falls through to multiplier 1 → DEFAULT caps +
205
+ // DEFAULT embed lines (no regression). Never throws.
206
+ const reviewPromptBudget = resolveReviewPromptBudget(ontoConfig);
207
+ const inventoryPromptCaps = deriveWorkbookInventoryPromptCaps(reviewPromptBudget.multiplier);
208
+ const maxEmbedLines = deriveReviewMaxEmbedLines(reviewPromptBudget.multiplier, DEFAULT_MAX_EMBED_LINES);
178
209
  const bindingParams = {
179
210
  projectRoot,
180
211
  ontoConfig,
@@ -195,6 +226,7 @@ export async function runPrepareReviewSessionCli(argv) {
195
226
  filesystemAllowedRoots: values["filesystem-allowed-root"],
196
227
  bindingNotes: values["binding-note"],
197
228
  ontoHome,
229
+ maxEmbedLines,
198
230
  ...(typeof values["session-id"] === "string" && values["session-id"].length > 0
199
231
  ? { sessionId: values["session-id"] }
200
232
  : {}),
@@ -253,6 +285,7 @@ export async function runPrepareReviewSessionCli(argv) {
253
285
  roleDefinitionRefs: values["role-definition-ref"],
254
286
  executionRuleRefs: values["execution-rule-ref"],
255
287
  directoryListingOptions,
288
+ inventoryPromptCaps,
256
289
  });
257
290
  console.log(JSON.stringify({
258
291
  session_root: sessionRoot,
@@ -642,6 +642,7 @@ export async function runRenderReviewFinalOutputCli(argv) {
642
642
  const defaultPurposeAlignment = executionStatus === "completed"
643
643
  ? "- bounded review execution completed"
644
644
  : `- execution status: ${executionStatus}`;
645
+ const fallbackImmediateActions = renderActionCandidates(classificationSummary);
645
646
  const finalOutputText = `---
646
647
  session_id: ${bindingArtifact.session_id}
647
648
  process: review
@@ -737,7 +738,7 @@ ${renderedBoundaryNotes}
737
738
  ### Immediate Actions Required
738
739
  ${synthesisLedger
739
740
  ? renderLedgerActions(synthesisLedger)
740
- : degradationSummary}
741
+ : fallbackImmediateActions}
741
742
 
742
743
  ### Recommendations
743
744
  ${synthesisLedger