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
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `onto seats` — print a read-only inventory of every LLM model seat the
3
+ * runtime can dispatch, resolved against the settings.json chain for the
4
+ * current project. Discovery only: it writes nothing and changes no behaviour.
5
+ */
6
+ import { resolveSettingsChain } from "../discovery/settings-chain.js";
7
+ import { collectSeatInventory, renderSeatInventoryTable, } from "../discovery/seat-inventory.js";
8
+ export async function runSeats(argv) {
9
+ const asJson = argv.includes("--json");
10
+ const settings = await resolveSettingsChain(process.cwd(), process.cwd());
11
+ const rows = collectSeatInventory(settings);
12
+ if (asJson) {
13
+ console.log(JSON.stringify(rows, null, 2));
14
+ return 0;
15
+ }
16
+ console.log(renderSeatInventoryTable(rows));
17
+ return 0;
18
+ }
@@ -129,8 +129,11 @@ export function parseRuntimeIssueStanceSchemaContext(rawPacketText) {
129
129
  const lensId = requireStringValue(finding.lens_id, `Runtime Issue Stance Input Projection.finding_summaries[${index}].lens_id`);
130
130
  if (lensId !== requestedLensId)
131
131
  continue;
132
- const findingId = requireStringValue(finding.finding_id, `Runtime Issue Stance Input Projection.finding_summaries[${index}].finding_id`);
133
- addPromptRefVariants({ refs: requestedLensFindingRefs, artifactRef: findingLedgerRef, anchor: findingId });
132
+ // Issue-strict parity with the on-disk validator (issue-artifact-runtime
133
+ // buildStanceEvidenceRefs): the lens's RAW finding evidence refs are a
134
+ // per-lens union, but finding-ledger#findingId anchors are registered ONLY
135
+ // per issue (surface_finding_ids loop below) — a cross-issue anchor that
136
+ // passed submit here used to be rejected at the on-disk re-validation.
134
137
  for (const evidenceRef of stringArray(finding.evidence_refs, `Runtime Issue Stance Input Projection.finding_summaries[${index}].evidence_refs`)) {
135
138
  requestedLensFindingRefs.add(evidenceRef);
136
139
  }
@@ -173,6 +176,10 @@ export function parseRuntimeIssueStanceSchemaContext(rawPacketText) {
173
176
  for (const findingId of stringArray(issue.surface_finding_ids, `Runtime Issue Stance Input Projection.issues[${index}].surface_finding_ids`)) {
174
177
  addPromptRefVariants({ refs, artifactRef: findingLedgerRef, anchor: findingId });
175
178
  for (const relationId of relationRefsByFindingId.get(findingId) ?? []) {
179
+ // Bare relation id parity with the on-disk validator, which registers
180
+ // the raw id for graph-endpoint relations — a bare `rel-N` submit used
181
+ // to be rejected here but accepted on disk (live-observed class).
182
+ refs.add(relationId);
176
183
  addPromptRefVariants({
177
184
  refs,
178
185
  artifactRef: relationGraphRef,
@@ -189,6 +196,7 @@ export function parseRuntimeIssueStanceSchemaContext(rawPacketText) {
189
196
  });
190
197
  }
191
198
  for (const relationId of dependencyRelationRefsByIssueId.get(issueId) ?? []) {
199
+ refs.add(relationId); // bare-id parity with the on-disk dependency loop
192
200
  addPromptRefVariants({
193
201
  refs,
194
202
  artifactRef: relationGraphRef,
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Bounded unit resubmit — pure decision/projection logic (no I/O, no LLM
3
+ * calls). Unit-agnostic: the error-spec construction and packet projection are
4
+ * shared across review units; per-unit failure classifiers select which
5
+ * validation rejections are correctable.
6
+ *
7
+ * When `review.execution.retry.resubmit.enabled` is on, a unit whose structured
8
+ * submit was rejected by a deterministic evidence-ref whitelist is re-requested
9
+ * through the unit's existing retry budget with an error spec injected into its
10
+ * prompt packet, instead of retrying blind.
11
+ *
12
+ * Wired units (§4-6a, §4-2c/2-A):
13
+ * - issue-stance: `issue_evidence_refs` whitelist. Exhaustion demotes the unit
14
+ * to complete-with-failure (degradation-summary, disclosed non-blockingly)
15
+ * rather than halting — unless the same validation class fails a strict
16
+ * majority of stance units (structural defect → whole-run halt with
17
+ * `halt_reason = correlated_validation: …`). The demotion/correlated
18
+ * machinery is stance-only and reads the terminal outcome; the rare-poison
19
+ * stance gate therefore retries only correctable validation failures and does
20
+ * not reinterpret a later infra terminal failure as validation.
21
+ * - deliberation-response: `allowed_evidence_refs` whitelist. Exhaustion reuses
22
+ * deliberation's existing non-halting degrade (unavailable-completion), so it
23
+ * needs no demotion/correlated machinery. Only the submit-time rejection is
24
+ * correctable; on-disk rejections run post-pool and degrade.
25
+ * - synthesis-response: `allowed_source_refs` whitelist (§4-2c/2-A). Its
26
+ * rejection message always carries `source_refs_used` → substring-classified
27
+ * output_contract, so its in-loop resubmit depends on the structural retry
28
+ * gate. Exhaustion reuses synthesis's existing non-halting degrade
29
+ * (completeUnavailableSynthesisResponseUnit); no demotion/correlated
30
+ * machinery.
31
+ *
32
+ * This module owns the deterministic parts: failure classification, error
33
+ * spec construction, idempotent packet projection, and the correlated
34
+ * escalation decision. File I/O and dispatch stay in
35
+ * run-review-prompt-execution.
36
+ *
37
+ * Design: development-records/design/20260704-review-unit-resubmit-and-limit-breaker-design.md (설계 A),
38
+ * development-records/design/20260707-s4-6a-deliberation-resubmit-design.md (§4-6a 확대).
39
+ */
40
+ /** Marker delimiting the runtime-owned error spec section inside a packet.
41
+ * Replacement is idempotent: a later resubmit replaces the section instead of
42
+ * stacking a second copy. */
43
+ export const RESUBMIT_ERROR_SPEC_BEGIN = "<!-- onto:resubmit-error-spec:begin -->";
44
+ export const RESUBMIT_ERROR_SPEC_END = "<!-- onto:resubmit-error-spec:end -->";
45
+ /** halt_reason prefix for the correlated-failure whole-run halt (F-A2). */
46
+ export const CORRELATED_VALIDATION_HALT_REASON = "correlated_validation";
47
+ /** Submit-time validator throw site
48
+ * (structured-output-tools `normalizeIssueStanceResponseSubmitArgs`). */
49
+ const SUBMIT_UNSUPPORTED_REF_PATTERN = /submit_issue_stance_response\.stances\[(\d+)\]\.evidence_refs contains unsupported ref for (\S+): (.*)$/m;
50
+ /** On-disk per-lens validator throw site
51
+ * (issue-artifact-runtime `validateIssueStanceResponseObject`). */
52
+ const ON_DISK_UNSUPPORTED_REF_PATTERN = /issue-stance response for issue (\S+) and lens \S+ references unsupported evidence: (.*)$/m;
53
+ /**
54
+ * Classify a unit failure message as an issue-stance unsupported-evidence-ref
55
+ * validation rejection. Anchors on BOTH runtime-owned throw sites: the
56
+ * submit-time whitelist (per-issue union set) and the stricter on-disk
57
+ * per-lens re-validation — either rejection is the same failure class for
58
+ * promotion purposes. Message text is not guaranteed to survive the worker
59
+ * adapters' stderr (see readFrozenUnsupportedRefViolation for the structural
60
+ * freeze-file source); this classifier is the in-process/message half.
61
+ * Returns null for every other failure class so infra failures (timeout,
62
+ * transport, empty output) keep their current semantics.
63
+ */
64
+ export function classifyUnsupportedEvidenceRefFailure(message) {
65
+ const submitMatch = SUBMIT_UNSUPPORTED_REF_PATTERN.exec(message);
66
+ if (submitMatch) {
67
+ const stanceIndex = Number.parseInt(submitMatch[1], 10);
68
+ const issueId = submitMatch[2];
69
+ const evidenceRef = submitMatch[3].trim();
70
+ if (!Number.isFinite(stanceIndex) || issueId.length === 0)
71
+ return null;
72
+ return { stanceIndex, issueId, evidenceRef };
73
+ }
74
+ const onDiskMatch = ON_DISK_UNSUPPORTED_REF_PATTERN.exec(message);
75
+ if (onDiskMatch) {
76
+ const issueId = onDiskMatch[1];
77
+ const evidenceRef = onDiskMatch[2].trim();
78
+ if (issueId.length === 0)
79
+ return null;
80
+ return { stanceIndex: null, issueId, evidenceRef };
81
+ }
82
+ return null;
83
+ }
84
+ export function isUnsupportedEvidenceRefFailureMessage(message) {
85
+ return (typeof message === "string" &&
86
+ classifyUnsupportedEvidenceRefFailure(message) !== null);
87
+ }
88
+ /** Submit-time throw site (structured-output-tools `assertAllowedRefs` via
89
+ * `normalizeIssueDeliberationResponseSubmitArgs`). This is the only deliberation
90
+ * evidence_refs rejection reachable by resubmit: the executor throws it during
91
+ * the dispatch (retryable) and freezes it as the salvage input. The on-disk
92
+ * validator (`validateIssueDeliberationResponseObject`) runs post-pool and is
93
+ * caught into a non-halting degrade — it never re-enters the retry loop — so it
94
+ * is deliberately not anchored here. */
95
+ const SUBMIT_DELIBERATION_UNSUPPORTED_REF_PATTERN = /submit_issue_deliberation_response\.evidence_refs contains unsupported ref: (.*)$/m;
96
+ /**
97
+ * Classify a unit failure message as a submit-time deliberation-response
98
+ * unsupported-evidence-ref rejection. Returns null for every other class so
99
+ * infra failures — and on-disk rejections, which degrade rather than resubmit —
100
+ * keep their current semantics.
101
+ */
102
+ export function classifyDeliberationUnsupportedEvidenceRefFailure(message) {
103
+ const submitMatch = SUBMIT_DELIBERATION_UNSUPPORTED_REF_PATTERN.exec(message);
104
+ if (submitMatch) {
105
+ const evidenceRef = submitMatch[1].trim();
106
+ if (evidenceRef.length === 0)
107
+ return null;
108
+ return { evidenceRef };
109
+ }
110
+ return null;
111
+ }
112
+ /** Submit-time throw sites in `normalizeIssueSynthesisResponseSubmitArgs`
113
+ * (structured-output-tools): `assertAllowedRefs` for a bad ref, and the
114
+ * "must include at least one" guard for an all-invalid set. The empty
115
+ * allowed-set rejection ("cannot validate … because allowed_source_refs is
116
+ * empty") is deliberately NOT matched — an empty allowed set is a runtime/context
117
+ * condition a resubmit cannot correct, so it stays non-retryable. */
118
+ const SUBMIT_SYNTHESIS_UNSUPPORTED_REF_PATTERN = /submit_issue_synthesis_response\.source_refs_used contains unsupported ref: (.*)$/m;
119
+ const SUBMIT_SYNTHESIS_MISSING_ALLOWED_REF_PATTERN = /submit_issue_synthesis_response\.source_refs_used must include at least one allowed source ref\./;
120
+ /**
121
+ * Classify a unit failure message as a submit-time synthesis-response
122
+ * unsupported/missing source-ref rejection. Returns null for every other class
123
+ * (including the non-correctable empty-allowed-set rejection) so infra failures
124
+ * keep their current semantics.
125
+ */
126
+ export function classifySynthesisUnsupportedSourceRefFailure(message) {
127
+ const unsupported = SUBMIT_SYNTHESIS_UNSUPPORTED_REF_PATTERN.exec(message);
128
+ if (unsupported) {
129
+ const sourceRef = unsupported[1].trim();
130
+ if (sourceRef.length === 0)
131
+ return null;
132
+ return { sourceRef };
133
+ }
134
+ if (SUBMIT_SYNTHESIS_MISSING_ALLOWED_REF_PATTERN.test(message)) {
135
+ return { sourceRef: null };
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Neutralize the runtime-owned section markers inside a value before it is
141
+ * interpolated into the spec. The rejected `evidence_ref` is model-controlled;
142
+ * a value literally containing an END (or BEGIN) marker would land as a
143
+ * premature delimiter inside the spec and fool the idempotent strip
144
+ * (`indexOf(END, begin)` matches the injected marker first), orphaning spec
145
+ * fragments and breaking the at-most-one-spec invariant across resubmit rounds.
146
+ * Marker-free values (every normal ref) are returned unchanged, so this is a
147
+ * no-op for the common path. Applied to every dynamic interpolation for
148
+ * defense-in-depth even though only the ref is model-controlled today.
149
+ */
150
+ function neutralizeSpecMarkers(value) {
151
+ return value
152
+ .split(RESUBMIT_ERROR_SPEC_BEGIN)
153
+ .join("(spec marker removed)")
154
+ .split(RESUBMIT_ERROR_SPEC_END)
155
+ .join("(spec marker removed)");
156
+ }
157
+ /**
158
+ * Render the bounded error spec for a resubmit attempt. Contains only the
159
+ * violation and the allowed set for the offending unit — never the failed
160
+ * output itself (design: no full-output retransmission; the model rebuilds
161
+ * the complete payload from the unchanged packet body). Unit-agnostic: the
162
+ * `unit` descriptor selects the submit tool, rejected-unit label, allowed-set
163
+ * header, and closing instruction; stance is the default and unchanged.
164
+ */
165
+ export function buildResubmitErrorSpec(args) {
166
+ const { violation } = args;
167
+ const evidenceRef = neutralizeSpecMarkers(violation.evidenceRef);
168
+ const issueId = neutralizeSpecMarkers(violation.issueId);
169
+ // Field-name vocabulary differs by unit: stance/deliberation reject
170
+ // `evidence_refs`, synthesis rejects `source_refs_used`. Defaulting to the
171
+ // evidence_refs vocabulary keeps stance/deliberation output byte-identical.
172
+ const refFieldPlural = args.unit?.kind === "synthesis" ? "source_refs_used" : "evidence_refs";
173
+ const refFieldSingular = args.unit?.kind === "synthesis" ? "source ref" : "evidence_ref";
174
+ const allowedBlock = args.allowedEvidenceRefs.length > 0
175
+ ? args.allowedEvidenceRefs
176
+ .map((ref) => `- ${neutralizeSpecMarkers(ref)}`)
177
+ .join("\n")
178
+ : `- (none — omit ${refFieldPlural} entries you cannot support)`;
179
+ const spec = args.unit?.kind === "deliberation"
180
+ ? {
181
+ submitTool: "submit_issue_deliberation_response",
182
+ rejectedLine: `- rejected: deliberation for issue_id: ${issueId}, lens_id: ${neutralizeSpecMarkers(args.unit.lensId)}`,
183
+ allowedHeader: "- allowed evidence_refs:",
184
+ closing: [
185
+ "Every evidence_ref must come from the allowed set in the schema",
186
+ "context above. Resubmit the full deliberation response, not only the",
187
+ "rejected entry.",
188
+ ],
189
+ }
190
+ : args.unit?.kind === "synthesis"
191
+ ? {
192
+ submitTool: "submit_issue_synthesis_response",
193
+ rejectedLine: `- rejected: synthesis response for issue_id: ${issueId}`,
194
+ allowedHeader: "- allowed source_refs_used:",
195
+ closing: [
196
+ "Every source ref must come from the allowed set in the schema",
197
+ "context above. Resubmit the full synthesis response, not only the",
198
+ "rejected entry.",
199
+ ],
200
+ }
201
+ : {
202
+ submitTool: "submit_issue_stance_response",
203
+ rejectedLine: `- rejected stance: ${violation.stanceIndex !== null
204
+ ? `stances[${violation.stanceIndex}] (issue_id: ${issueId})`
205
+ : `stance for issue_id: ${issueId}`}`,
206
+ allowedHeader: `- allowed evidence_refs for ${issueId}:`,
207
+ closing: [
208
+ "Every stance's evidence_refs must come from that issue's allowed set in",
209
+ "the schema context above. Resubmit the full stances array, not only the",
210
+ "rejected entry.",
211
+ ],
212
+ };
213
+ // A specific offending ref → name it; an empty ref (synthesis "must include at
214
+ // least one" rejection) → instruct citing from the allowed set instead.
215
+ const refLine = evidenceRef.length > 0
216
+ ? `- unsupported ${refFieldSingular}: ${evidenceRef}`
217
+ : `- you cited no allowed ${refFieldSingular}; include at least one from the allowed set below`;
218
+ return [
219
+ RESUBMIT_ERROR_SPEC_BEGIN,
220
+ "",
221
+ `## Resubmit required: ${refFieldPlural} validation rejected (attempt ${args.resubmitAttempt})`,
222
+ "",
223
+ `Your previous ${spec.submitTool} call was rejected by`,
224
+ "deterministic validation. Do not apologize or explain; call the submit",
225
+ "tool again with a complete corrected payload.",
226
+ "",
227
+ spec.rejectedLine,
228
+ refLine,
229
+ spec.allowedHeader,
230
+ allowedBlock,
231
+ "",
232
+ ...spec.closing,
233
+ "",
234
+ RESUBMIT_ERROR_SPEC_END,
235
+ ].join("\n");
236
+ }
237
+ /**
238
+ * Project the error spec into a packet idempotently: strips a previous
239
+ * runtime-owned spec section (if any) before appending the new one, so the
240
+ * packet carries at most one spec regardless of how many resubmit rounds ran.
241
+ */
242
+ export function applyResubmitErrorSpecToPacket(packetText, errorSpec) {
243
+ const stripped = stripResubmitErrorSpec(packetText);
244
+ return `${stripped.trimEnd()}\n\n${errorSpec.trimEnd()}\n`;
245
+ }
246
+ function stripResubmitErrorSpec(packetText) {
247
+ const begin = packetText.indexOf(RESUBMIT_ERROR_SPEC_BEGIN);
248
+ if (begin === -1)
249
+ return packetText;
250
+ const end = packetText.indexOf(RESUBMIT_ERROR_SPEC_END, begin);
251
+ if (end === -1)
252
+ return packetText.slice(0, begin);
253
+ return (packetText.slice(0, begin) +
254
+ packetText.slice(end + RESUBMIT_ERROR_SPEC_END.length));
255
+ }
256
+ export function packetHasResubmitErrorSpec(packetText) {
257
+ return packetText.includes(RESUBMIT_ERROR_SPEC_BEGIN);
258
+ }
259
+ /**
260
+ * Correlated escalation decision: the same validation class failing a strict
261
+ * majority (>50%) of stance units signals a structural defect (prompt,
262
+ * schema, or context assembly), not per-unit output noise — whole-run halt
263
+ * stays the correct promotion there.
264
+ */
265
+ export function correlatedValidationExceeded(args) {
266
+ if (args.totalUnitCount <= 0)
267
+ return false;
268
+ return args.validationFailedUnitCount * 2 > args.totalUnitCount;
269
+ }
@@ -115,8 +115,9 @@ function requireRecord(value, label) {
115
115
  }
116
116
  /**
117
117
  * Normalize a JSON Schema into the strict shape external workers require: every
118
- * object's properties become `required`. Mirrors what the Codex `--output-schema`
119
- * and Claude `--json-schema` flags expect for structured output.
118
+ * object's properties become `required`, and homogeneous `anyOf` object rows are
119
+ * collapsed to a single provider-compatible object. Runtime submit validation
120
+ * remains the authority for issue-specific allowed sets.
120
121
  */
121
122
  export function toWorkerStructuredOutputSchema(schema) {
122
123
  if (Array.isArray(schema)) {
@@ -129,12 +130,89 @@ export function toWorkerStructuredOutputSchema(schema) {
129
130
  for (const [key, value] of Object.entries(schema)) {
130
131
  normalized[key] = toWorkerStructuredOutputSchema(value);
131
132
  }
133
+ const collapsedAnyOf = collapseHomogeneousAnyOf(normalized);
134
+ if (collapsedAnyOf)
135
+ return collapsedAnyOf;
132
136
  const properties = normalized.properties;
133
137
  if (properties && typeof properties === "object" && !Array.isArray(properties)) {
134
138
  normalized.required = Object.keys(properties);
135
139
  }
136
140
  return normalized;
137
141
  }
142
+ function collapseHomogeneousAnyOf(schema) {
143
+ const variants = schema.anyOf;
144
+ if (!Array.isArray(variants) || variants.length === 0)
145
+ return null;
146
+ const records = variants.filter((variant) => !!variant && typeof variant === "object" && !Array.isArray(variant));
147
+ if (records.length !== variants.length)
148
+ return null;
149
+ if (!records.every((record) => record.type === "object"))
150
+ return null;
151
+ const propertySets = records.map((record) => record.properties &&
152
+ typeof record.properties === "object" &&
153
+ !Array.isArray(record.properties)
154
+ ? Object.keys(record.properties).sort()
155
+ : null);
156
+ const firstSet = propertySets[0];
157
+ if (!firstSet)
158
+ return null;
159
+ if (!propertySets.every((set) => !!set &&
160
+ set.length === firstSet.length &&
161
+ set.every((key, index) => key === firstSet[index]))) {
162
+ return null;
163
+ }
164
+ const mergedProperties = {};
165
+ for (const key of firstSet) {
166
+ const merged = mergeHomogeneousProperty(records.map((record) => record.properties[key]));
167
+ if (merged === null)
168
+ return null;
169
+ mergedProperties[key] = merged;
170
+ }
171
+ const collapsed = {
172
+ type: "object",
173
+ additionalProperties: records.every((record) => record.additionalProperties === false)
174
+ ? false
175
+ : schema.additionalProperties,
176
+ properties: mergedProperties,
177
+ required: Object.keys(mergedProperties),
178
+ };
179
+ if (typeof schema.description === "string") {
180
+ collapsed.description = schema.description;
181
+ }
182
+ return collapsed;
183
+ }
184
+ function mergeHomogeneousProperty(values) {
185
+ const records = values.filter((value) => !!value && typeof value === "object" && !Array.isArray(value));
186
+ if (records.length !== values.length)
187
+ return sameJson(values) ? values[0] : null;
188
+ const first = records[0];
189
+ if (!first)
190
+ return null;
191
+ const comparable = records.map(({ enum: _enum, description: _description, ...rest }) => rest);
192
+ if (sameJson(comparable)) {
193
+ const enumValues = records.flatMap((record) => Array.isArray(record.enum) ? record.enum : []);
194
+ const base = {
195
+ ...comparable[0],
196
+ ...(typeof first.description === "string"
197
+ ? { description: first.description }
198
+ : {}),
199
+ };
200
+ if (enumValues.length > 0) {
201
+ return {
202
+ ...base,
203
+ enum: Array.from(new Set(enumValues.map((value) => JSON.stringify(value)))).map((value) => JSON.parse(value)),
204
+ };
205
+ }
206
+ return base;
207
+ }
208
+ return sameJson(records) ? first : null;
209
+ }
210
+ function sameJson(values) {
211
+ if (values.length === 0)
212
+ return true;
213
+ const first = JSON.stringify(values[0]);
214
+ return values.every((value) => JSON.stringify(value) === first);
215
+ }
138
216
  /**
139
217
  * Build the submit-tool JSON Schema and the submission state for an output
140
218
  * format, without writing any file. Codex writes this to disk for its
@@ -1,6 +1,8 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import fsSync from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
5
+ import { resolveClaudeBin } from "../llm/claude-bin.js";
4
6
  const CODEX_ENV_SIGNALS = ["CODEX_THREAD_ID", "CODEX_CI"];
5
7
  /** True when the current process is running under a Codex-owned session. */
6
8
  export function detectCodexEnvSignal() {
@@ -31,40 +33,87 @@ export function detectCodexBinaryAvailable() {
31
33
  return false;
32
34
  return fsSync.existsSync(path.join(os.homedir(), ".codex", "auth.json"));
33
35
  }
34
- /** OAuth credential filenames Claude Code may write under its config dir. */
36
+ /** OAuth credential filenames older Claude Code builds write under its config
37
+ * dir. Newer builds store the credential in the OS keychain (macOS) instead, so
38
+ * this file probe is the backward-compatible fallback, not the primary check. */
35
39
  const CLAUDE_OAUTH_CREDENTIAL_FILENAMES = [
36
40
  ".credentials.json",
37
41
  ".oauth-token",
38
42
  ];
39
- function claudeConfigDir() {
40
- const configured = process.env.CLAUDE_CONFIG_DIR;
43
+ /** Bound on the `claude auth status` probe so a wedged CLI cannot hang review
44
+ * profile resolution. The command is a local credential read (~200ms observed),
45
+ * so this is a generous ceiling, not a normal latency. */
46
+ const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 5000;
47
+ function claudeConfigDir(env) {
48
+ const configured = env.CLAUDE_CONFIG_DIR;
41
49
  if (typeof configured === "string" && configured.trim().length > 0) {
42
50
  return configured.trim();
43
51
  }
44
52
  return path.join(os.homedir(), ".claude");
45
53
  }
46
- /**
47
- * True when the Claude Code worker path can be used from this process.
48
- *
49
- * Mirrors {@link detectCodexBinaryAvailable}: both the executable and an OAuth
50
- * credential are required so the external OAuth worker (`claude_code` adapter)
51
- * route stays fail-loud when the host-bound worker is unavailable. The config
52
- * dir honors CLAUDE_CONFIG_DIR; the credential filename is tolerant of the
53
- * known Claude Code variants.
54
- */
55
- export function detectClaudeBinaryAvailable() {
56
- const pathEnv = process.env.PATH ?? "";
57
- let claudeOnPath = false;
54
+ function claudeBinaryOnPath(env) {
55
+ const pathEnv = env.PATH ?? "";
58
56
  for (const dir of pathEnv.split(path.delimiter)) {
59
57
  if (!dir)
60
58
  continue;
61
- if (fsSync.existsSync(path.join(dir, "claude"))) {
62
- claudeOnPath = true;
63
- break;
64
- }
59
+ if (fsSync.existsSync(path.join(dir, "claude")))
60
+ return true;
65
61
  }
66
- if (!claudeOnPath)
67
- return false;
68
- const configDir = claudeConfigDir();
62
+ return false;
63
+ }
64
+ function hasClaudeCredentialFile(env) {
65
+ const configDir = claudeConfigDir(env);
69
66
  return CLAUDE_OAUTH_CREDENTIAL_FILENAMES.some((name) => fsSync.existsSync(path.join(configDir, name)));
70
67
  }
68
+ /**
69
+ * Parse the JSON `claude auth status` prints. Returns its `loggedIn` boolean
70
+ * when the output is the recognized shape, or null otherwise (older CLI without
71
+ * the command, an error, or a format change) so the caller can fall back to the
72
+ * file-credential probe. Pure — unit-testable without spawning the CLI.
73
+ */
74
+ export function parseClaudeAuthLoggedIn(stdout) {
75
+ try {
76
+ const parsed = JSON.parse(stdout);
77
+ return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * True when the Claude Code worker path can be used from this process.
85
+ *
86
+ * The authority on whether the worker can authenticate is the claude CLI
87
+ * itself: `claude auth status` reports login across every credential store
88
+ * (macOS Keychain, credential file, or env token) and every OS — which a
89
+ * file-only probe cannot (a keychain-backed login leaves no credential file).
90
+ * The worker spawns the same CLI, so this checks the exact auth path the run
91
+ * will use. Binary resolution mirrors the executor via {@link resolveClaudeBin}
92
+ * (ONTO_CLAUDE_BIN override + PATH).
93
+ *
94
+ * Falls back to the legacy file-credential probe when the CLI cannot answer
95
+ * (missing binary, timeout, or a build too old for `auth status`), so setups
96
+ * that resolved before this change keep resolving (backward compatible). Unlike
97
+ * {@link detectCodexBinaryAvailable}, which reads codex's on-disk
98
+ * `~/.codex/auth.json` directly, claude's credential is not reliably a file —
99
+ * hence the CLI delegation.
100
+ */
101
+ export function detectClaudeBinaryAvailable(env = process.env) {
102
+ const claudeBin = resolveClaudeBin(env);
103
+ try {
104
+ const stdout = execFileSync(claudeBin, ["auth", "status"], {
105
+ env,
106
+ timeout: CLAUDE_AUTH_STATUS_TIMEOUT_MS,
107
+ encoding: "utf8",
108
+ stdio: ["ignore", "pipe", "ignore"],
109
+ });
110
+ const loggedIn = parseClaudeAuthLoggedIn(stdout);
111
+ if (loggedIn !== null)
112
+ return loggedIn;
113
+ }
114
+ catch {
115
+ // ENOENT (no binary), timeout, non-zero exit, or a CLI too old to support
116
+ // `auth status` — fall through to the legacy file-credential probe.
117
+ }
118
+ return claudeBinaryOnPath(env) && hasClaudeCredentialFile(env);
119
+ }