onto-mcp 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Reconstruct effort-calibration adapter (P3). Distills a reconstruct
3
+ * golden-fixture quality-gate result into the pipeline-agnostic gate signal the
4
+ * sweep aggregator consumes.
5
+ *
6
+ * Reconstruct quality is golden-fixture-anchored and pipeline-WIDE: a sweep run
7
+ * pins one effort knob — the global semantic-author effort (the "author" stage)
8
+ * or the answer-support judge effort (the "judge" stage) — and measures the
9
+ * whole-run golden gate, attributing the result to that stage. The "gate" is the
10
+ * golden gate's overall pass; "quality" is a continuous 0..1 "finding 양·질"
11
+ * proxy synthesized from the gate's three optimization axes:
12
+ * - Q1 expected-concept recall (양·질 of the authored seed),
13
+ * - Q2 competency-question support rate, and
14
+ * - Q3 dropped-question health (fraction of authored questions still assessed;
15
+ * the runtime batches instead of dropping, so this is normally 1.0).
16
+ * Each axis is a 0..1 health fraction; equal-weight mean keeps the score on the
17
+ * same scale as the review adapter's check-pass fraction. The binary pass/fail
18
+ * stays owned by `passed`; this score only feeds the frontier's plateau search.
19
+ *
20
+ * A run that produced no quality verdict — `rejected` (missing telemetry
21
+ * provenance) or `not_applicable` (mock realization incompatible with the
22
+ * fixture) — yields passed=null and qualityScore=null, mirroring review's
23
+ * unjudged treatment: non-passing in the quorum and contributing no quality.
24
+ *
25
+ * Pure — no live LLM and no IO; aggregation and report assembly live in
26
+ * `effort-calibration-sweep.ts`.
27
+ */
28
+ /** Q3 as a 0..1 health fraction: share of authored questions still assessed. */
29
+ function q3Health(q3) {
30
+ const authored = q3.authored_question_count;
31
+ if (authored <= 0)
32
+ return 1; // no questions to drop → vacuously healthy
33
+ const kept = authored - q3.dropped_question_count;
34
+ return Math.max(0, Math.min(1, kept / authored));
35
+ }
36
+ /** Distill the calibration gate signal from a reconstruct golden gate result. */
37
+ export function reconstructRunGateSignal(gate) {
38
+ // passed/failed are the only quality verdicts; rejected/not_applicable mean no
39
+ // verdict was produced (provenance reject or mock-incompatible fixture) → null.
40
+ const passed = gate.status === "passed" ? true : gate.status === "failed" ? false : null;
41
+ // q1/q2/q3 are populated together (passed/failed) or absent together
42
+ // (rejected/not_applicable); a verdict without metrics leaves quality null.
43
+ const qualityScore = gate.q1 && gate.q2 && gate.q3
44
+ ? (gate.q1.recall + gate.q2.support_rate + q3Health(gate.q3)) / 3
45
+ : null;
46
+ return { passed, qualityScore };
47
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Review effort-calibration adapter (P2). Distills a review unit-sweep run's
3
+ * whole-review semantic quality gate into the pipeline-agnostic gate signal the
4
+ * sweep aggregator consumes.
5
+ *
6
+ * Review quality is review-WIDE and fixture-anchored: the unit-sweep varies one
7
+ * LLM unit's effort over a fixed base effort and measures the whole-review
8
+ * semantic quality gate, attributing the result to that unit. So the swept
9
+ * review unit is the "stage", the "gate" is the gate's overall pass, and
10
+ * "quality" is the fraction of the 12-check gate that passed (a continuous 0..1
11
+ * "finding 양·질" proxy). Pure — no live LLM and no IO; aggregation and report
12
+ * assembly live in `effort-calibration-sweep.ts`.
13
+ */
14
+ /** Distill the calibration gate signal from a full semantic-quality-gate result. */
15
+ export function reviewRunGateSignal(gate) {
16
+ const passed = gate.status === "not_applicable" ? null : gate.status === "passed";
17
+ const total = gate.checks.length;
18
+ const qualityScore = total
19
+ ? gate.checks.filter((c) => c.status === "passed").length / total
20
+ : null;
21
+ return { passed, qualityScore };
22
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Effort-calibration sweep aggregation — pipeline-agnostic.
3
+ *
4
+ * A sweep run is one (swept stage, candidate effort) trial whose result has been
5
+ * distilled to a gate signal by a pipeline-specific adapter (review or
6
+ * reconstruct). This module owns the part that does NOT depend on the pipeline:
7
+ * grouping runs into per-stage per-effort buckets, aggregating each bucket into
8
+ * the `EffortSample` series the frontier classifier consumes, and assembling the
9
+ * per-model report. The pipeline adapters own only the gate-signal distillation
10
+ * (their gate result types differ); the frontier classifier owns the decision.
11
+ *
12
+ * Pure — no live LLM and no IO. The sweep harness (scripts/) supplies the runs
13
+ * and writes the report artifact.
14
+ */
15
+ import { classifyEffortCalibration, } from "./effort-frontier.js";
16
+ const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
17
+ const stdev = (xs) => {
18
+ if (xs.length < 2)
19
+ return 0;
20
+ const m = mean(xs);
21
+ return Math.sqrt(mean(xs.map((x) => (x - m) ** 2)));
22
+ };
23
+ const meanCost = (costs) => {
24
+ if (costs.length === 0)
25
+ return undefined;
26
+ const avg = (pick) => {
27
+ const vals = costs
28
+ .map(pick)
29
+ .filter((v) => typeof v === "number");
30
+ return vals.length ? mean(vals) : undefined;
31
+ };
32
+ const promptChars = avg((c) => c.promptChars);
33
+ const outputChars = avg((c) => c.outputChars);
34
+ const providerTokens = avg((c) => c.providerTokens);
35
+ const durationMs = avg((c) => c.durationMs);
36
+ const out = {};
37
+ if (promptChars !== undefined)
38
+ out.promptChars = promptChars;
39
+ if (outputChars !== undefined)
40
+ out.outputChars = outputChars;
41
+ if (providerTokens !== undefined)
42
+ out.providerTokens = providerTokens;
43
+ if (durationMs !== undefined)
44
+ out.durationMs = durationMs;
45
+ return out;
46
+ };
47
+ /**
48
+ * Aggregate sweep runs into per-stage EffortSample series, each ordered
49
+ * ascending by `effortOrder` (efforts absent from the order are dropped — no
50
+ * silent reordering). Stages are sorted by id for a deterministic report.
51
+ *
52
+ * Unjudged (passed === null) runs count as NON-passing in gatePassRate: an
53
+ * effort can only clear the quorum if every run produced a passing verdict, so a
54
+ * bucket that mixes a pass with an unjudged/mock run is downgraded rather than
55
+ * given a free pass (the denominator is the full run count, matching `runs`).
56
+ * Quality is averaged only over runs whose gate produced a score.
57
+ */
58
+ export function aggregateEffortSweep(runs, effortOrder) {
59
+ const rank = new Map(effortOrder.map((e, i) => [e, i]));
60
+ const byStage = new Map();
61
+ for (const r of runs) {
62
+ if (!rank.has(r.effort))
63
+ continue; // effort not in canonical order → drop
64
+ let byEffort = byStage.get(r.stage);
65
+ if (!byEffort) {
66
+ byEffort = new Map();
67
+ byStage.set(r.stage, byEffort);
68
+ }
69
+ const list = byEffort.get(r.effort) ?? [];
70
+ list.push(r);
71
+ byEffort.set(r.effort, list);
72
+ }
73
+ const stages = [...byStage.entries()].map(([stage, byEffort]) => {
74
+ const samples = [...byEffort.entries()].map(([effort, group]) => {
75
+ // Denominator is the full run count: unjudged (passed === null) runs are
76
+ // non-passing so missing scored evidence cannot overstate viability.
77
+ const passedRuns = group.filter((r) => r.gate.passed === true).length;
78
+ const gatePassRate = group.length ? passedRuns / group.length : 0;
79
+ const qualities = group
80
+ .map((r) => r.gate.qualityScore)
81
+ .filter((q) => typeof q === "number");
82
+ const cost = meanCost(group
83
+ .map((r) => r.cost)
84
+ .filter((c) => c !== undefined));
85
+ return {
86
+ effort,
87
+ gatePassRate,
88
+ quality: mean(qualities),
89
+ qualityStdev: stdev(qualities),
90
+ runs: group.length,
91
+ ...(cost ? { cost } : {}),
92
+ };
93
+ });
94
+ samples.sort((a, b) => rank.get(a.effort) - rank.get(b.effort));
95
+ return { stage, samples };
96
+ });
97
+ stages.sort((a, b) => (a.stage < b.stage ? -1 : a.stage > b.stage ? 1 : 0));
98
+ return stages;
99
+ }
100
+ /** Build a per-model effort-calibration report from sweep runs (pure). */
101
+ export function buildEffortCalibrationReport(args) {
102
+ return classifyEffortCalibration({
103
+ pipeline: args.pipeline,
104
+ provider: args.provider,
105
+ model: args.model,
106
+ route: args.route,
107
+ thresholds: args.thresholds,
108
+ stages: aggregateEffortSweep(args.runs, args.effortOrder),
109
+ });
110
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Effort frontier calibration — pure, deterministic core.
3
+ *
4
+ * For each LLM stage of a pipeline (review or reconstruct), given a series of
5
+ * per-effort aggregated samples (one per effort level, aggregated over N runs),
6
+ * classify the reasoning-effort frontier:
7
+ *
8
+ * - minViableEffort — the lowest effort whose gate passes (>= passQuorum of
9
+ * runs). Below it the stage fails.
10
+ * - effectiveMaxEffort — the lowest gate-passing effort beyond which no higher
11
+ * effort improves quality by more than plateauThreshold
12
+ * (the knee of the cost/quality curve). Above it more
13
+ * effort buys ~nothing.
14
+ * - recommendedEffort — effectiveMaxEffort: the cheapest effort at the quality
15
+ * plateau that still passes the gate.
16
+ *
17
+ * Pipeline-agnostic: callers supply already-aggregated samples (quality is a
18
+ * pipeline-defined "finding 양·질" metric, higher = better) in ascending effort
19
+ * order. No live LLM calls and no I/O — fully unit-testable. The benchmark/sweep
20
+ * harnesses (scripts/) produce the samples; this module decides the frontier and
21
+ * assembles the per-model calibration record that is re-run per new model.
22
+ */
23
+ const DEFAULT_PASS_QUORUM = 1;
24
+ // Tolerance for the plateau comparison. Quality deltas from averaged 0..1
25
+ // metrics carry float noise (e.g. 0.90 - 0.85 === 0.050000000000000044), so a
26
+ // gain mathematically equal to the threshold must NOT count as an improvement —
27
+ // the contract treats gains at or below the threshold as a plateau.
28
+ const QUALITY_EPSILON = 1e-9;
29
+ const curveOf = (samples) => samples.map((s) => ({
30
+ effort: s.effort,
31
+ gatePassRate: s.gatePassRate,
32
+ quality: s.quality,
33
+ qualityStdev: s.qualityStdev,
34
+ runs: s.runs,
35
+ ...(s.cost ? { cost: s.cost } : {}),
36
+ }));
37
+ /**
38
+ * Classify the effort frontier for one stage. `samples` must be in ascending
39
+ * effort order (cheapest first); the caller owns the effort ordering.
40
+ */
41
+ export function classifyStageFrontier(stage, samples, thresholds) {
42
+ const passQuorum = thresholds.passQuorum ?? DEFAULT_PASS_QUORUM;
43
+ const plateau = thresholds.plateauThreshold;
44
+ const curve = curveOf(samples);
45
+ if (samples.length === 0) {
46
+ return {
47
+ stage,
48
+ minViableEffort: null,
49
+ effectiveMaxEffort: null,
50
+ recommendedEffort: null,
51
+ plateauReached: false,
52
+ rationale: "no samples provided",
53
+ curve,
54
+ };
55
+ }
56
+ // min viable: the lowest effort whose gate clears the quorum.
57
+ const minViableIdx = samples.findIndex((s) => s.gatePassRate >= passQuorum);
58
+ if (minViableIdx === -1) {
59
+ return {
60
+ stage,
61
+ minViableEffort: null,
62
+ effectiveMaxEffort: null,
63
+ recommendedEffort: null,
64
+ plateauReached: false,
65
+ rationale: `gate never reaches passQuorum=${passQuorum} at any tested effort`,
66
+ curve,
67
+ };
68
+ }
69
+ // candidates: gate-passing efforts at or above min viable, ascending. (A
70
+ // higher effort can dip below the quorum, e.g. overthinking; such efforts are
71
+ // not viable recommendations and are excluded from the plateau search.)
72
+ const candidates = samples
73
+ .slice(minViableIdx)
74
+ .filter((s) => s.gatePassRate >= passQuorum);
75
+ // effective max: the lowest candidate whose quality is not beaten by more than
76
+ // the plateau threshold by ANY higher candidate (checking all higher ones, not
77
+ // just the adjacent, makes this robust to non-monotonic noise). The highest
78
+ // candidate satisfies this vacuously, so a result always exists.
79
+ let effIdx = candidates.length - 1;
80
+ for (let i = 0; i < candidates.length; i++) {
81
+ const base = candidates[i].quality;
82
+ const beatenLater = candidates
83
+ .slice(i + 1)
84
+ .some((c) => c.quality - base > plateau + QUALITY_EPSILON);
85
+ if (!beatenLater) {
86
+ effIdx = i;
87
+ break;
88
+ }
89
+ }
90
+ const minViable = samples[minViableIdx].effort;
91
+ const effective = candidates[effIdx].effort;
92
+ const highestViable = candidates[candidates.length - 1].effort;
93
+ const plateauReached = candidates.length > 1 && effective !== highestViable;
94
+ let rationale;
95
+ if (candidates.length === 1) {
96
+ rationale = `only "${minViable}" is viable in the tested range; plateau indeterminate`;
97
+ }
98
+ else if (plateauReached) {
99
+ rationale = `gate first passes at "${minViable}"; quality plateaus at "${effective}" (no higher effort gains >${plateau})`;
100
+ }
101
+ else {
102
+ rationale = `gate first passes at "${minViable}"; quality still climbing at the top of the viable range "${highestViable}" — plateau not captured; re-run with higher effort if available`;
103
+ }
104
+ return {
105
+ stage,
106
+ minViableEffort: minViable,
107
+ effectiveMaxEffort: effective,
108
+ recommendedEffort: effective,
109
+ plateauReached,
110
+ rationale,
111
+ curve,
112
+ };
113
+ }
114
+ /**
115
+ * Assemble a per-model calibration record from per-stage sample series. The
116
+ * caller (harness) stamps identity/time metadata when serializing the artifact;
117
+ * this function stays deterministic (no clock, no I/O).
118
+ */
119
+ export function classifyEffortCalibration(args) {
120
+ return {
121
+ pipeline: args.pipeline,
122
+ provider: args.provider,
123
+ model: args.model,
124
+ route: args.route,
125
+ // Persist the *effective* thresholds (resolve the default quorum) so a
126
+ // serialized record is self-describing — a reader/replay can tell an
127
+ // all-runs quorum from a relaxed one without knowing this module's default.
128
+ thresholds: {
129
+ passQuorum: args.thresholds.passQuorum ?? DEFAULT_PASS_QUORUM,
130
+ plateauThreshold: args.thresholds.plateauThreshold,
131
+ },
132
+ stages: args.stages.map((s) => classifyStageFrontier(s.stage, s.samples, args.thresholds)),
133
+ };
134
+ }
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Resolve the Claude Code CLI binary path, accommodating per-user install
6
+ * locations. Anthropic OAuth review/reconstruct workers spawn this binary, and
7
+ * its location varies by machine and install method — and a host like Claude
8
+ * Desktop may launch the server with a minimal `PATH` that does not include the
9
+ * user's `claude`. So a bare `spawn("claude")` is not reliable across users.
10
+ *
11
+ * Resolution order (first hit wins):
12
+ * 1. `ONTO_CLAUDE_BIN` — explicit operator override (absolute path or name).
13
+ * 2. `claude` discoverable on `PATH` (as an executable file — shell aliases
14
+ * and functions are never consulted because spawn does not use them).
15
+ * 3. Common Claude Code install locations (native installer, older local
16
+ * install, Homebrew).
17
+ * 4. The literal `"claude"` — so `spawn` fails with the existing actionable
18
+ * ENOENT message ("Claude Code CLI not found … set ONTO_CLAUDE_BIN") rather
19
+ * than this resolver inventing a different error.
20
+ *
21
+ * Pure function of `env`: resolution is a handful of `stat`/`access` syscalls and
22
+ * each call site invokes it at most once per spawn (or once at module load), so
23
+ * it is NOT cached — a cache keyed on nothing would return a stale path when a
24
+ * later call passes a different `env.PATH` (the injected `env` would silently
25
+ * stop controlling discovery). The same applies to the codex OAuth direct route
26
+ * (`callCodexCli` still spawns a bare `"codex"`); extending this resolver to
27
+ * codex is a symmetric, deferred follow-up.
28
+ */
29
+ function isExecutableFile(candidate) {
30
+ try {
31
+ fs.accessSync(candidate, fs.constants.X_OK);
32
+ return fs.statSync(candidate).isFile();
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** `claude` resolvable as an executable file on `PATH` (not a shell alias). */
39
+ function findClaudeOnPath(env) {
40
+ const rawPath = env.PATH ?? env.Path ?? "";
41
+ for (const dir of rawPath.split(path.delimiter)) {
42
+ if (!dir)
43
+ continue;
44
+ const candidate = path.join(dir, "claude");
45
+ if (isExecutableFile(candidate))
46
+ return candidate;
47
+ }
48
+ return undefined;
49
+ }
50
+ /** Common Claude Code install locations, checked when PATH lookup misses. */
51
+ function commonInstallLocations() {
52
+ const home = os.homedir();
53
+ return [
54
+ path.join(home, ".local", "bin", "claude"), // native installer symlink
55
+ path.join(home, ".claude", "local", "claude"), // older local install
56
+ "/opt/homebrew/bin/claude", // Homebrew (Apple silicon)
57
+ "/usr/local/bin/claude", // Homebrew (Intel) / manual
58
+ ];
59
+ }
60
+ export function resolveClaudeBin(env = process.env) {
61
+ const override = env.ONTO_CLAUDE_BIN?.trim();
62
+ if (override)
63
+ return override;
64
+ const onPath = findClaudeOnPath(env);
65
+ if (onPath)
66
+ return onPath;
67
+ for (const location of commonInstallLocations()) {
68
+ if (isExecutableFile(location))
69
+ return location;
70
+ }
71
+ // Not found — return the bare name so spawn ENOENT carries the actionable
72
+ // "set ONTO_CLAUDE_BIN" guidance.
73
+ // POSIX-scoped: the bundle targets macOS/Linux (manifest compatibility.platforms
74
+ // = darwin/linux) and discovers a `claude` binary (no Windows `claude.exe` /
75
+ // %LOCALAPPDATA% lookup). Windows is out of scope here.
76
+ return "claude";
77
+ }