@momentiq/dark-factory-cli 0.1.0-alpha.4

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 (132) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +150 -0
  3. package/dist/adapters/_shared.d.ts +45 -0
  4. package/dist/adapters/_shared.d.ts.map +1 -0
  5. package/dist/adapters/_shared.js +200 -0
  6. package/dist/adapters/_shared.js.map +1 -0
  7. package/dist/adapters/codex-sdk.d.ts +279 -0
  8. package/dist/adapters/codex-sdk.d.ts.map +1 -0
  9. package/dist/adapters/codex-sdk.js +930 -0
  10. package/dist/adapters/codex-sdk.js.map +1 -0
  11. package/dist/adapters/critic-result-schema.d.ts +215 -0
  12. package/dist/adapters/critic-result-schema.d.ts.map +1 -0
  13. package/dist/adapters/critic-result-schema.js +153 -0
  14. package/dist/adapters/critic-result-schema.js.map +1 -0
  15. package/dist/adapters/critic.d.ts +62 -0
  16. package/dist/adapters/critic.d.ts.map +1 -0
  17. package/dist/adapters/critic.js +79 -0
  18. package/dist/adapters/critic.js.map +1 -0
  19. package/dist/adapters/cursor-sdk.d.ts +153 -0
  20. package/dist/adapters/cursor-sdk.d.ts.map +1 -0
  21. package/dist/adapters/cursor-sdk.js +818 -0
  22. package/dist/adapters/cursor-sdk.js.map +1 -0
  23. package/dist/adapters/gemini-sdk.d.ts +113 -0
  24. package/dist/adapters/gemini-sdk.d.ts.map +1 -0
  25. package/dist/adapters/gemini-sdk.js +532 -0
  26. package/dist/adapters/gemini-sdk.js.map +1 -0
  27. package/dist/adapters/grok-direct-sdk.d.ts +148 -0
  28. package/dist/adapters/grok-direct-sdk.d.ts.map +1 -0
  29. package/dist/adapters/grok-direct-sdk.js +694 -0
  30. package/dist/adapters/grok-direct-sdk.js.map +1 -0
  31. package/dist/branch-protection/audit_branch_protection.py +759 -0
  32. package/dist/branch-protection/index.d.ts +25 -0
  33. package/dist/branch-protection/index.d.ts.map +1 -0
  34. package/dist/branch-protection/index.js +70 -0
  35. package/dist/branch-protection/index.js.map +1 -0
  36. package/dist/branch-protection/spec-default.yaml +314 -0
  37. package/dist/cli.d.ts +3 -0
  38. package/dist/cli.d.ts.map +1 -0
  39. package/dist/cli.js +581 -0
  40. package/dist/cli.js.map +1 -0
  41. package/dist/cycle-doc-validator/index.d.ts +43 -0
  42. package/dist/cycle-doc-validator/index.d.ts.map +1 -0
  43. package/dist/cycle-doc-validator/index.js +69 -0
  44. package/dist/cycle-doc-validator/index.js.map +1 -0
  45. package/dist/cycle-doc-validator/validate_cycle_doc.py +1260 -0
  46. package/dist/cycle-tracker-sync/attribute_pr_cycle_ref.py +384 -0
  47. package/dist/cycle-tracker-sync/index.d.ts +20 -0
  48. package/dist/cycle-tracker-sync/index.d.ts.map +1 -0
  49. package/dist/cycle-tracker-sync/index.js +71 -0
  50. package/dist/cycle-tracker-sync/index.js.map +1 -0
  51. package/dist/cycle-tracker-sync/sync_cycle_trackers.py +1093 -0
  52. package/dist/evidence/audit-trail.d.ts +59 -0
  53. package/dist/evidence/audit-trail.d.ts.map +1 -0
  54. package/dist/evidence/audit-trail.js +283 -0
  55. package/dist/evidence/audit-trail.js.map +1 -0
  56. package/dist/evidence/index.d.ts +4 -0
  57. package/dist/evidence/index.d.ts.map +1 -0
  58. package/dist/evidence/index.js +29 -0
  59. package/dist/evidence/index.js.map +1 -0
  60. package/dist/evidence/per-sha.d.ts +13 -0
  61. package/dist/evidence/per-sha.d.ts.map +1 -0
  62. package/dist/evidence/per-sha.js +97 -0
  63. package/dist/evidence/per-sha.js.map +1 -0
  64. package/dist/evidence/quality-gates.d.ts +19 -0
  65. package/dist/evidence/quality-gates.d.ts.map +1 -0
  66. package/dist/evidence/quality-gates.js +212 -0
  67. package/dist/evidence/quality-gates.js.map +1 -0
  68. package/dist/git.d.ts +40 -0
  69. package/dist/git.d.ts.map +1 -0
  70. package/dist/git.js +414 -0
  71. package/dist/git.js.map +1 -0
  72. package/dist/glob.d.ts +4 -0
  73. package/dist/glob.d.ts.map +1 -0
  74. package/dist/glob.js +99 -0
  75. package/dist/glob.js.map +1 -0
  76. package/dist/index.d.ts +17 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +33 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/paths.d.ts +12 -0
  81. package/dist/paths.d.ts.map +1 -0
  82. package/dist/paths.js +42 -0
  83. package/dist/paths.js.map +1 -0
  84. package/dist/policy/baseline.d.ts +15 -0
  85. package/dist/policy/baseline.d.ts.map +1 -0
  86. package/dist/policy/baseline.js +115 -0
  87. package/dist/policy/baseline.js.map +1 -0
  88. package/dist/policy/config.d.ts +64 -0
  89. package/dist/policy/config.d.ts.map +1 -0
  90. package/dist/policy/config.js +363 -0
  91. package/dist/policy/config.js.map +1 -0
  92. package/dist/policy/gate.d.ts +80 -0
  93. package/dist/policy/gate.d.ts.map +1 -0
  94. package/dist/policy/gate.js +1019 -0
  95. package/dist/policy/gate.js.map +1 -0
  96. package/dist/policy/index.d.ts +7 -0
  97. package/dist/policy/index.d.ts.map +1 -0
  98. package/dist/policy/index.js +14 -0
  99. package/dist/policy/index.js.map +1 -0
  100. package/dist/policy/merge-queue.d.ts +183 -0
  101. package/dist/policy/merge-queue.d.ts.map +1 -0
  102. package/dist/policy/merge-queue.js +310 -0
  103. package/dist/policy/merge-queue.js.map +1 -0
  104. package/dist/policy/profile.d.ts +98 -0
  105. package/dist/policy/profile.d.ts.map +1 -0
  106. package/dist/policy/profile.js +156 -0
  107. package/dist/policy/profile.js.map +1 -0
  108. package/dist/policy/tdd-classifier.d.ts +18 -0
  109. package/dist/policy/tdd-classifier.d.ts.map +1 -0
  110. package/dist/policy/tdd-classifier.js +79 -0
  111. package/dist/policy/tdd-classifier.js.map +1 -0
  112. package/dist/prompt.d.ts +13 -0
  113. package/dist/prompt.d.ts.map +1 -0
  114. package/dist/prompt.js +175 -0
  115. package/dist/prompt.js.map +1 -0
  116. package/dist/report.d.ts +88 -0
  117. package/dist/report.d.ts.map +1 -0
  118. package/dist/report.js +376 -0
  119. package/dist/report.js.map +1 -0
  120. package/dist/runner.d.ts +30 -0
  121. package/dist/runner.d.ts.map +1 -0
  122. package/dist/runner.js +456 -0
  123. package/dist/runner.js.map +1 -0
  124. package/dist/security.d.ts +2 -0
  125. package/dist/security.d.ts.map +1 -0
  126. package/dist/security.js +19 -0
  127. package/dist/security.js.map +1 -0
  128. package/dist/trusted-surface/rebind.d.ts +10 -0
  129. package/dist/trusted-surface/rebind.d.ts.map +1 -0
  130. package/dist/trusted-surface/rebind.js +154 -0
  131. package/dist/trusted-surface/rebind.js.map +1 -0
  132. package/package.json +78 -0
@@ -0,0 +1,694 @@
1
+ // Cycle 322.3 — xAI Grok direct-API adapter via the `openai` npm package.
2
+ //
3
+ // Why a third adapter (manifesto §11 + §12): after 322.2 added the
4
+ // second critic family (Gemini), Sage's `block-if-any` aggregation
5
+ // still hard-blocks the gate whenever Cursor's upstream incident
6
+ // hits the `required: true` critic. The §11 SOTA is three critic
7
+ // families across four vendor lineages so a single-vendor outage
8
+ // can never paralyze the gate; 322.3 adds xAI Grok as the third
9
+ // family. xAI's training data, RLHF process, and reasoning
10
+ // architecture (always-on chain-of-thought) are uncorrelated with
11
+ // OpenAI/Google/Anthropic, so the disagreements between Grok and the
12
+ // other two critics carry the §11 information value.
13
+ //
14
+ // The adapter ships in shadow mode (`required: false` in
15
+ // `.agent-review/config.json` — Cycle 322.3 Phase D); a follow-up
16
+ // cycle 322.3.1 promotes the aggregation policy from `block-if-any`
17
+ // to `min-complete-quorum` once the calibration window data
18
+ // justifies it (Cycle 322.3 §"Two-step promotion").
19
+ //
20
+ // The adapter:
21
+ // - implements `CriticAdapter` from `critic.ts` (the post-322.2-
22
+ // migration shape with `requiredEnvVars`)
23
+ // - uses xAI's documented Responses API
24
+ // (`client.responses.create({ ..., reasoning: { effort }, stream: true })`)
25
+ // rather than Chat Completions, because the Responses API exposes a
26
+ // 3-tier `reasoning.effort` (low/medium/high) where Chat Completions
27
+ // only exposes a binary low/high — the 3-tier matches the
28
+ // chief-engineer critic role's match for Cursor's `reasoning=extra-high`
29
+ // posture. See `tools/agent-review/evals/spike-grok-responses-2026-05.md`
30
+ // for the API decision artifact.
31
+ // - mirrors the 322.1 retry shape (`runRetryLoop` + `attemptReview`)
32
+ // from a single source of truth in `cursor-sdk.ts`, so the policy +
33
+ // budget are byte-identical across adapters and any future fix lands
34
+ // in one place
35
+ // - routes diagnostic-redaction + JSON parsing + reviewer-metadata merge
36
+ // + error-result construction through `_shared.ts` so the security
37
+ // boundary (redactSecrets at the only writeFileSync site) cannot drift
38
+ // - is read-only by structure: no `tools` configured on the request;
39
+ // the only output channel is the JSON response itself (so a malicious
40
+ // diff convincing the model to "run a command" has no command surface)
41
+ //
42
+ // The implementation uses the dependency-injection ESCAPE hatch on the
43
+ // constructor (`createClient` factory) so unit tests can supply a mock
44
+ // OpenAI client without forcing the SDK to be present at test time —
45
+ // this matches the testing posture of the cycle 322.1 / 322.2 adapter
46
+ // tests. The constructor is the ONLY place the real `openai` SDK is
47
+ // referenced for production use.
48
+ import OpenAI, { APIError } from "openai";
49
+ import { compileCriticPrompt } from "../prompt.js";
50
+ import { parseCriticResult, } from "@momentiq/dark-factory-schemas";
51
+ import { buildErrorResult, mergeAdapterMetadata, normalizeCriticEcho, parseAssistantJson, writeRedactedDiagnostic, } from "./_shared.js";
52
+ import { runRetryLoop, } from "./cursor-sdk.js";
53
+ export const GROK_DIRECT_SDK_ADAPTER_ID = "grok-direct-sdk";
54
+ export const XAI_API_KEY_ENV = "XAI_API_KEY";
55
+ export const XAI_BASE_URL = "https://api.x.ai/v1";
56
+ // xAI Responses API permanent-failure HTTP statuses (mirrors OpenAI
57
+ // status codes since xAI is OpenAI-compatible at the surface). Same
58
+ // classification as the Gemini permanent set — burning retry budget
59
+ // on these wastes wall-clock AND can mask the real fault (e.g., a
60
+ // wrong API key would silently exhaust retries before surfacing).
61
+ // 400 invalid_request — bad request shape, model id typo
62
+ // 401 / 403 — auth failure
63
+ // 404 model_not_found — model id not in the API key's allowed list
64
+ // 429 rate_limit — quota / rate-limit (retrying within 20s
65
+ // burns budget; surface immediately so the
66
+ // operator can investigate)
67
+ // Anything else (5xx, 504, transient network) is retryable. The retry
68
+ // budget is bounded by `runRetryLoop` to 2 retries / 20s wall-clock.
69
+ export const GROK_PERMANENT_STATUS = new Set([
70
+ 400,
71
+ 401,
72
+ 403,
73
+ 404,
74
+ 429,
75
+ ]);
76
+ // xAI Responses API reasoning-effort options. The 3-tier control is
77
+ // the documented xAI surface (https://docs.x.ai/docs/api-reference);
78
+ // Chat Completions exposes only a binary low/high which would defeat
79
+ // the cycle's match-Cursor-extra-high reasoning posture.
80
+ //
81
+ // xAI Grok 4.3 documentation
82
+ // (https://docs.x.ai/developers/models/grok-4.3#capabilities) lists
83
+ // `none` alongside `low | medium | high` as a valid effort value —
84
+ // `none` is the escape hatch for non-reasoning use cases. xAI's
85
+ // docs say an unspecified effort can default to low reasoning, so
86
+ // the adapter must send the explicit `none` value when configured
87
+ // instead of dropping the `reasoning` object.
88
+ const ALLOWED_REASONING_EFFORTS = new Set([
89
+ "low",
90
+ "medium",
91
+ "high",
92
+ "none",
93
+ ]);
94
+ export const DEFAULT_REASONING_EFFORT = "high";
95
+ /**
96
+ * Resolve the Grok reasoning-effort from the critic config's
97
+ * `model.params`. Falls back to {@link DEFAULT_REASONING_EFFORT}
98
+ * ("high") when unset. Validates against the documented
99
+ * Responses-API surface — invalid values fall back to the default
100
+ * rather than corrupting the request body.
101
+ *
102
+ * Coerces string inputs (the config schema's `value` is
103
+ * `string|number|boolean`; reasoning effort is always a string
104
+ * enum). Booleans and numbers are operator typos and use the
105
+ * default.
106
+ *
107
+ * Exported for direct unit testing (no SDK mock required).
108
+ */
109
+ export function resolveReasoningEffort(critic) {
110
+ const param = critic.model.params.find((p) => p.id === "reasoning_effort");
111
+ if (!param)
112
+ return DEFAULT_REASONING_EFFORT;
113
+ const v = param.value;
114
+ if (typeof v !== "string")
115
+ return DEFAULT_REASONING_EFFORT;
116
+ const norm = v.toLowerCase();
117
+ if (ALLOWED_REASONING_EFFORTS.has(norm)) {
118
+ return norm;
119
+ }
120
+ return DEFAULT_REASONING_EFFORT;
121
+ }
122
+ /**
123
+ * Probe a thrown error for an xAI Responses API HTTP status code.
124
+ * Returns `null` when the error didn't carry one (network error,
125
+ * non-API exception). Exported for direct unit testing.
126
+ *
127
+ * The `openai` SDK throws `APIError extends Error` with a `status:
128
+ * number` field on HTTP-level failures; a non-API failure (DNS,
129
+ * timeout in fetch, etc.) won't have this field set. Treating "no
130
+ * status" as retryable lets the loop catch real transient blips
131
+ * while not silently retrying logic errors.
132
+ */
133
+ export function extractXaiApiErrorStatus(err) {
134
+ if (!err || typeof err !== "object")
135
+ return null;
136
+ const e = err;
137
+ if (typeof e["status"] === "number")
138
+ return e["status"];
139
+ // Some SDK error shapes nest the status under `response.status`:
140
+ const response = e["response"];
141
+ if (response && typeof response === "object") {
142
+ const status = response["status"];
143
+ if (typeof status === "number")
144
+ return status;
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * Policy gate: decide whether an xAI Responses API failure is
150
+ * retryable. Returns `false` for HTTP statuses in
151
+ * {@link GROK_PERMANENT_STATUS}, `true` otherwise (including
152
+ * no-status network errors).
153
+ *
154
+ * Exported for direct unit testing.
155
+ */
156
+ export function isGrokPermanentFailure(status) {
157
+ if (status === null)
158
+ return false; // no status → treat as transient
159
+ return GROK_PERMANENT_STATUS.has(status);
160
+ }
161
+ export class GrokDirectSdkAdapter {
162
+ options;
163
+ id = GROK_DIRECT_SDK_ADAPTER_ID;
164
+ requiredEnvVars = [XAI_API_KEY_ENV];
165
+ createClient;
166
+ constructor(options = {}) {
167
+ this.options = options;
168
+ this.createClient =
169
+ options.createClient ??
170
+ ((apiKey) => new OpenAI({
171
+ apiKey,
172
+ baseURL: XAI_BASE_URL,
173
+ }));
174
+ }
175
+ async review(packet, critic, options) {
176
+ return runRetryLoop({
177
+ attempt: (idx) => this.attemptReview(packet, critic, options, idx),
178
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
179
+ ...(this.options.sleep !== undefined ? { sleep: this.options.sleep } : {}),
180
+ buildExhausted: ({ last, totalAttempts, aborted }) => {
181
+ const retriesUsed = Math.max(0, totalAttempts - 1);
182
+ const summary = aborted
183
+ ? last
184
+ ? `grok SDK run aborted after ${retriesUsed} retries: ${last.message}`
185
+ : "grok SDK run aborted before any attempt completed"
186
+ : last
187
+ ? `grok SDK run failed after ${retriesUsed} retries: ${last.message}`
188
+ : "grok SDK run failed with no captured failure metadata";
189
+ return buildErrorResult({
190
+ critic,
191
+ message: summary,
192
+ retryable: true,
193
+ ...(last?.errorCode != null ? { code: last.errorCode } : {}),
194
+ retryCount: retriesUsed,
195
+ });
196
+ },
197
+ });
198
+ }
199
+ // One attempt. Mirrors `GeminiSdkAdapter.attemptReview` shape so
200
+ // the outer retry loop dispatches identically; differences are
201
+ // Grok-specific (Responses API event shape, reasoning.effort param,
202
+ // refusal-event safety-block path) and surface here.
203
+ async attemptReview(packet, critic, options, attemptIdx) {
204
+ const apiKey = this.options.apiKey ?? process.env[XAI_API_KEY_ENV];
205
+ if (!apiKey) {
206
+ // Missing key is permanent — no retry can fix a missing secret.
207
+ return {
208
+ kind: "permanent_failure",
209
+ errorCode: null,
210
+ statusMessage: null,
211
+ result: buildErrorResult({
212
+ critic,
213
+ message: `${XAI_API_KEY_ENV} is not set; cannot run Grok critic`,
214
+ retryable: false,
215
+ retryCount: attemptIdx,
216
+ }),
217
+ };
218
+ }
219
+ const prompt = compileCriticPrompt({
220
+ packet,
221
+ critic,
222
+ blockingSeverities: options.blockingSeverities,
223
+ treatDiffAsUntrusted: true,
224
+ });
225
+ const startMs = Date.now();
226
+ if (attemptIdx === 0) {
227
+ options.emit?.({
228
+ ts: new Date().toISOString(),
229
+ event: "critic_run_started",
230
+ commit: packet.commit.sha,
231
+ criticId: critic.id,
232
+ adapter: this.id,
233
+ model: critic.model.id,
234
+ });
235
+ }
236
+ const client = this.createClient(apiKey);
237
+ const reasoningEffort = resolveReasoningEffort(critic);
238
+ let assistantText = "";
239
+ let lastUsage;
240
+ let refusalText;
241
+ // Cycle 322.3 Cursor critic feedback (367476d3 finding #4 §0): the
242
+ // xAI Responses API emits `response.incomplete` when output was
243
+ // truncated (max_output_tokens or content_filter). Treat as a
244
+ // permanent failure with preserved accumulated text — retrying
245
+ // the same prompt re-trips the same truncation, AND the partial
246
+ // text is the most informative thing the operator can read.
247
+ let incompleteReason;
248
+ try {
249
+ // `responses.create` returns either a single Response (for
250
+ // non-streaming) or an AsyncIterable<ResponseStreamEvent>
251
+ // (for streaming). We always stream so the result is the
252
+ // async-iterable. Request options second-arg carries the
253
+ // AbortSignal — the SDK threads it to the underlying fetch.
254
+ const stream = await client.responses.create({
255
+ model: critic.model.id,
256
+ // Always send the configured effort, including the
257
+ // documented `none` escape hatch. Omitting this object can
258
+ // let the API default to low reasoning.
259
+ reasoning: {
260
+ effort: reasoningEffort,
261
+ },
262
+ input: [
263
+ {
264
+ role: "user",
265
+ content: prompt.text,
266
+ },
267
+ ],
268
+ // Force JSON-only response. `parseAssistantJson` still
269
+ // runs as a safety net for occasional format drift —
270
+ // adapters never trust the structured-output guarantee
271
+ // because a malformed terminal text would otherwise
272
+ // produce an unparseable artifact that the gate can't
273
+ // evaluate.
274
+ text: { format: { type: "json_object" } },
275
+ // Keep proprietary PR review prompts/results local. xAI's
276
+ // Responses API stores request/response data by default;
277
+ // `store: false` is the documented opt-out for stateless calls.
278
+ store: false,
279
+ stream: true,
280
+ }, options.signal !== undefined ? { signal: options.signal } : {});
281
+ for await (const event of stream) {
282
+ if (options.signal?.aborted)
283
+ break;
284
+ const type = event.type ?? "";
285
+ // Primary path: accumulate text deltas. `response.output_text.delta`
286
+ // is the documented xAI Responses API stream event for token
287
+ // deltas (https://docs.x.ai/docs/api-reference + OpenAI
288
+ // Responses API contract). The spike artifact records the
289
+ // exact event shape.
290
+ if (type === "response.output_text.delta" && typeof event.delta === "string") {
291
+ assistantText += event.delta;
292
+ continue;
293
+ }
294
+ // Refusal events — the model declined to comply with the
295
+ // prompt (e.g., policy-blocked content). The adapter
296
+ // treats this as a permanent failure: retrying the same
297
+ // prompt re-trips the same refusal.
298
+ if ((type === "response.refusal.delta" || type === "response.refusal.done") &&
299
+ typeof event.refusal === "string") {
300
+ refusalText = refusalText
301
+ ? refusalText + event.refusal
302
+ : event.refusal;
303
+ continue;
304
+ }
305
+ // Terminal event — extract token usage for telemetry.
306
+ if (type === "response.completed" && event.response) {
307
+ const usage = event.response.usage;
308
+ if (usage)
309
+ lastUsage = usage;
310
+ // Fallback: if the stream didn't emit deltas (e.g., a
311
+ // truncated stream that only delivered the terminal
312
+ // event), walk `response.output[].content[].text` to
313
+ // recover the assistant text. The deltas path is
314
+ // preferred for backpressure-friendly accumulation.
315
+ if (!assistantText && Array.isArray(event.response.output)) {
316
+ for (const item of event.response.output) {
317
+ const parts = item.content;
318
+ if (!Array.isArray(parts))
319
+ continue;
320
+ for (const part of parts) {
321
+ if (typeof part.text === "string") {
322
+ assistantText += part.text;
323
+ }
324
+ }
325
+ }
326
+ }
327
+ }
328
+ // `response.failed` is xAI's terminal-error event. We
329
+ // synthesize a thrown error so the outer catch handles it
330
+ // uniformly with HTTP-level failures.
331
+ if (type === "response.failed") {
332
+ throw new Error("xAI Responses API returned response.failed event");
333
+ }
334
+ // `response.incomplete` is the xAI/OpenAI Responses API
335
+ // terminal event for truncated output (max_output_tokens
336
+ // hit, content filter triggered, etc.). Capture the reason
337
+ // and any final usage; the post-loop branch treats this as
338
+ // a permanent failure (retrying with the same prompt
339
+ // re-trips the same truncation).
340
+ if (type === "response.incomplete" && event.response) {
341
+ const usage = event.response.usage;
342
+ if (usage)
343
+ lastUsage = usage;
344
+ incompleteReason =
345
+ event.response.incomplete_details?.reason ?? "unknown";
346
+ }
347
+ }
348
+ }
349
+ catch (err) {
350
+ const e = err;
351
+ const status = err instanceof APIError ? err.status : extractXaiApiErrorStatus(err);
352
+ const permanent = isGrokPermanentFailure(status);
353
+ const codeStr = status !== null ? `http_${status}` : "transport_error";
354
+ options.emit?.({
355
+ ts: new Date().toISOString(),
356
+ event: "critic_run_error",
357
+ commit: packet.commit.sha,
358
+ criticId: critic.id,
359
+ adapter: this.id,
360
+ model: critic.model.id,
361
+ durationMs: Date.now() - startMs,
362
+ error: e.message,
363
+ status: permanent ? "run_failure_permanent" : "run_failure",
364
+ retryCount: attemptIdx,
365
+ errorCode: codeStr,
366
+ });
367
+ if (permanent) {
368
+ return {
369
+ kind: "permanent_failure",
370
+ errorCode: codeStr,
371
+ statusMessage: null,
372
+ result: buildErrorResult({
373
+ critic,
374
+ message: `grok SDK run failed (permanent, status=${status ?? "?"}): ${e.message}`,
375
+ retryable: false,
376
+ code: codeStr,
377
+ retryCount: attemptIdx,
378
+ }),
379
+ };
380
+ }
381
+ return {
382
+ kind: "retryable_failure",
383
+ errorCode: codeStr,
384
+ statusMessage: null,
385
+ message: `grok SDK run failed: ${e.message}`,
386
+ runId: null,
387
+ agentId: null,
388
+ };
389
+ }
390
+ if (incompleteReason) {
391
+ // Truncation — permanent. Preserve the accumulated partial
392
+ // text in the diagnostic artifact for operator inspection
393
+ // (the truncated content is what would let the operator
394
+ // raise `max_output_tokens` or revise the prompt). The
395
+ // distinct `incomplete` errorCode lets operators
396
+ // discriminate truncation patterns from transport failures
397
+ // in `_runs.ndjson` — important because the remediation
398
+ // differs (raise max_output_tokens vs. fix vendor incident).
399
+ const diagPath = writeRedactedDiagnostic({
400
+ diagnosticsDir: options.diagnosticsDir,
401
+ criticId: critic.id,
402
+ commit: packet.commit.sha,
403
+ rawText: assistantText,
404
+ });
405
+ const msg = `grok critic response truncated: ${incompleteReason} (partial text preserved)`;
406
+ options.emit?.({
407
+ ts: new Date().toISOString(),
408
+ event: "critic_run_error",
409
+ commit: packet.commit.sha,
410
+ criticId: critic.id,
411
+ adapter: this.id,
412
+ model: critic.model.id,
413
+ durationMs: Date.now() - startMs,
414
+ error: msg,
415
+ status: "incomplete",
416
+ retryCount: attemptIdx,
417
+ errorCode: "incomplete",
418
+ });
419
+ return {
420
+ kind: "permanent_failure",
421
+ errorCode: "incomplete",
422
+ statusMessage: null,
423
+ result: buildErrorResult({
424
+ critic,
425
+ message: msg,
426
+ retryable: false,
427
+ code: "incomplete",
428
+ ...(diagPath !== undefined ? { rawSamplePath: diagPath } : {}),
429
+ retryCount: attemptIdx,
430
+ }),
431
+ };
432
+ }
433
+ if (refusalText) {
434
+ // Model refusal — permanent. Retrying the same prompt
435
+ // re-trips the same refusal. The refusal text goes into the
436
+ // critic-result envelope so operators can inspect it; the
437
+ // diagnostic file is best-effort.
438
+ const msg = `grok critic response refused: ${refusalText.slice(0, 200)}`;
439
+ options.emit?.({
440
+ ts: new Date().toISOString(),
441
+ event: "critic_run_error",
442
+ commit: packet.commit.sha,
443
+ criticId: critic.id,
444
+ adapter: this.id,
445
+ model: critic.model.id,
446
+ durationMs: Date.now() - startMs,
447
+ error: msg,
448
+ status: "refused",
449
+ retryCount: attemptIdx,
450
+ errorCode: "refused",
451
+ });
452
+ return {
453
+ kind: "permanent_failure",
454
+ errorCode: "refused",
455
+ statusMessage: null,
456
+ result: buildErrorResult({
457
+ critic,
458
+ message: msg,
459
+ retryable: false,
460
+ code: "refused",
461
+ retryCount: attemptIdx,
462
+ }),
463
+ };
464
+ }
465
+ const parseOutcome = parseAssistantJson(assistantText);
466
+ if (!parseOutcome.ok) {
467
+ const diagPath = writeRedactedDiagnostic({
468
+ diagnosticsDir: options.diagnosticsDir,
469
+ criticId: critic.id,
470
+ commit: packet.commit.sha,
471
+ rawText: assistantText,
472
+ });
473
+ options.emit?.({
474
+ ts: new Date().toISOString(),
475
+ event: "critic_run_error",
476
+ commit: packet.commit.sha,
477
+ criticId: critic.id,
478
+ adapter: this.id,
479
+ model: critic.model.id,
480
+ durationMs: Date.now() - startMs,
481
+ error: `invalid critic JSON: ${parseOutcome.message}`,
482
+ status: "invalid_json",
483
+ retryCount: attemptIdx,
484
+ });
485
+ return {
486
+ kind: "permanent_failure",
487
+ errorCode: null,
488
+ statusMessage: null,
489
+ result: buildErrorResult({
490
+ critic,
491
+ message: `grok critic returned invalid JSON: ${parseOutcome.message}`,
492
+ retryable: false,
493
+ ...(diagPath !== undefined ? { rawSamplePath: diagPath } : {}),
494
+ retryCount: attemptIdx,
495
+ }),
496
+ };
497
+ }
498
+ let result;
499
+ try {
500
+ // Drop schema-invalid `validation.qualityGateResults[]` entries
501
+ // (e.g. model emits `gate` instead of `command`) BEFORE strict
502
+ // parsing — the validation block is informational and gets
503
+ // overwritten below with deterministic packet evidence. Issue #1484.
504
+ const normalized = normalizeCriticEcho(parseOutcome.value);
505
+ const enriched = mergeAdapterMetadata(normalized, { critic });
506
+ result = parseCriticResult(enriched, options.blockingSeverities);
507
+ }
508
+ catch (err) {
509
+ const e = err;
510
+ const diagPath = writeRedactedDiagnostic({
511
+ diagnosticsDir: options.diagnosticsDir,
512
+ criticId: critic.id,
513
+ commit: packet.commit.sha,
514
+ rawText: assistantText,
515
+ });
516
+ options.emit?.({
517
+ ts: new Date().toISOString(),
518
+ event: "critic_run_error",
519
+ commit: packet.commit.sha,
520
+ criticId: critic.id,
521
+ adapter: this.id,
522
+ model: critic.model.id,
523
+ durationMs: Date.now() - startMs,
524
+ error: `schema validation failed: ${e.message}`,
525
+ status: "schema_violation",
526
+ retryCount: attemptIdx,
527
+ });
528
+ return {
529
+ kind: "permanent_failure",
530
+ errorCode: null,
531
+ statusMessage: null,
532
+ result: buildErrorResult({
533
+ critic,
534
+ message: `grok critic JSON failed schema validation: ${e.message}`,
535
+ retryable: false,
536
+ ...(diagPath !== undefined ? { rawSamplePath: diagPath } : {}),
537
+ retryCount: attemptIdx,
538
+ }),
539
+ };
540
+ }
541
+ const durationMs = Date.now() - startMs;
542
+ const enriched = {
543
+ ...result,
544
+ durationMs,
545
+ validation: {
546
+ qualityGateResults: packet.validation.evidence,
547
+ qualityGatesMissing: packet.validation.missing,
548
+ },
549
+ };
550
+ const blockerCount = enriched.findings.filter((f) => f.severity === "blocker").length;
551
+ const highCount = enriched.findings.filter((f) => f.severity === "high").length;
552
+ options.emit?.({
553
+ ts: new Date().toISOString(),
554
+ event: "critic_run_finished",
555
+ commit: packet.commit.sha,
556
+ criticId: critic.id,
557
+ adapter: this.id,
558
+ model: critic.model.id,
559
+ durationMs,
560
+ ...(enriched.verdict !== undefined ? { verdict: enriched.verdict } : {}),
561
+ findingCount: enriched.findings.length,
562
+ blockerCount,
563
+ highCount,
564
+ ...(typeof lastUsage?.input_tokens === "number"
565
+ ? { tokensIn: lastUsage.input_tokens }
566
+ : {}),
567
+ ...(typeof lastUsage?.output_tokens === "number"
568
+ ? { tokensOut: lastUsage.output_tokens }
569
+ : {}),
570
+ status: "complete",
571
+ retryCount: attemptIdx,
572
+ });
573
+ return { kind: "success", result: enriched };
574
+ }
575
+ async doctor(critic) {
576
+ const checks = [];
577
+ const apiKey = this.options.apiKey ?? process.env[XAI_API_KEY_ENV];
578
+ const missingOptionalKey = !apiKey && !critic.required;
579
+ checks.push({
580
+ name: "xai_api_key",
581
+ passed: Boolean(apiKey) || missingOptionalKey,
582
+ detail: apiKey
583
+ ? `${XAI_API_KEY_ENV} present`
584
+ : missingOptionalKey
585
+ ? `${XAI_API_KEY_ENV} missing; optional shadow critic will be skipped at review time`
586
+ : `${XAI_API_KEY_ENV} missing`,
587
+ ...(apiKey || missingOptionalKey
588
+ ? {}
589
+ : {
590
+ remediation: `export ${XAI_API_KEY_ENV}=... or add it to the Doppler scope (sage/dev). See spike artifact tools/agent-review/evals/spike-grok-models-2026-05.md §"Operator runbook" for the provisioning one-liner.`,
591
+ }),
592
+ });
593
+ let sdkLoaded = false;
594
+ try {
595
+ // The dynamic import path catches both "package missing on
596
+ // disk" and "package present but no exports we recognize"
597
+ // (older shape) cases.
598
+ const mod = (await import("openai"));
599
+ sdkLoaded = typeof mod["default"] === "function" || typeof mod["OpenAI"] === "function";
600
+ }
601
+ catch {
602
+ sdkLoaded = false;
603
+ }
604
+ checks.push({
605
+ name: "grok_sdk_loaded",
606
+ passed: sdkLoaded,
607
+ detail: sdkLoaded
608
+ ? "openai SDK imported (used as xAI client via baseURL)"
609
+ : "openai SDK missing or shape unexpected",
610
+ ...(sdkLoaded
611
+ ? {}
612
+ : { remediation: "make agent-review-deps && make agent-review-build" }),
613
+ });
614
+ // Diagnostic family-prefix check: a stale config that pins to the
615
+ // retired `grok-4` (or `grok-4-fast`, `grok-4-1-fast`,
616
+ // `grok-code-fast-1`) cohort can be flagged BEFORE the live
617
+ // models.list() call below — useful when the operator's API key
618
+ // isn't yet provisioned but the doctor is still expected to
619
+ // catch obvious config errors.
620
+ const familyOk = critic.model.id.toLowerCase().startsWith("grok");
621
+ checks.push({
622
+ name: "grok_model_id_family",
623
+ passed: familyOk,
624
+ detail: familyOk
625
+ ? `${critic.model.id} matches grok-* family pattern`
626
+ : `${critic.model.id} does NOT match grok-* family pattern`,
627
+ ...(familyOk
628
+ ? {}
629
+ : {
630
+ remediation: "the configured Grok critic's model.id should start with 'grok-' (e.g., 'grok-4.3'). Update .agent-review/config.json:critics[].model.id.",
631
+ }),
632
+ });
633
+ if (!sdkLoaded || !apiKey)
634
+ return checks;
635
+ // Verify the configured model id resolves via models.list(). The
636
+ // xAI 2026-05-15 retirement cohort makes this check load-bearing
637
+ // — without it, a first-run after the retirement date would fail
638
+ // at review time with a less-actionable HTTP 404. Per
639
+ // spike-grok-models-2026-05.md, the live list call records the
640
+ // canonical id; the doctor confirms the configured id is in the
641
+ // live catalog.
642
+ try {
643
+ const client = this.createClient(apiKey);
644
+ const list = client.models.list;
645
+ if (typeof list !== "function") {
646
+ checks.push({
647
+ name: "grok_model_listing",
648
+ passed: false,
649
+ detail: "openai SDK models.list not exposed; cannot verify model id",
650
+ remediation: "Run `make agent-review-spike-grok` (source at tools/agent-review/src/spikes/grok-models-list-2026-05.ts) and confirm the configured id is current.",
651
+ });
652
+ return checks;
653
+ }
654
+ // The SDK exposes `models.list` as either a function returning
655
+ // an AsyncIterable directly OR a function returning a Promise
656
+ // of an AsyncIterable (Pager). Handle both shapes — the
657
+ // production OpenAI SDK returns a Page that is itself async-
658
+ // iterable; older mocks may return a Promise. Same defensive
659
+ // handling as the Gemini adapter's doctor.
660
+ const listed = list.call(client.models);
661
+ const iterable = listed && typeof listed.then === "function"
662
+ ? await listed
663
+ : listed;
664
+ const ids = [];
665
+ for await (const m of iterable) {
666
+ if (typeof m.id === "string")
667
+ ids.push(m.id);
668
+ }
669
+ const matched = ids.some((n) => n === critic.model.id);
670
+ checks.push({
671
+ name: "grok_model_id",
672
+ passed: matched,
673
+ detail: matched
674
+ ? `model ${critic.model.id} available`
675
+ : `model ${critic.model.id} not in available list (${ids.slice(0, 8).join(", ")}${ids.length > 8 ? "..." : ""})`,
676
+ ...(matched
677
+ ? {}
678
+ : {
679
+ remediation: "update .agent-review/config.json:critics[].model.id (see tools/agent-review/evals/spike-grok-models-2026-05.md or run `make agent-review-spike-grok` to refresh)",
680
+ }),
681
+ });
682
+ }
683
+ catch (err) {
684
+ checks.push({
685
+ name: "grok_model_id",
686
+ passed: false,
687
+ detail: `models.list() failed: ${err.message}`,
688
+ remediation: `verify ${XAI_API_KEY_ENV} and network connectivity (xAI endpoint: ${XAI_BASE_URL})`,
689
+ });
690
+ }
691
+ return checks;
692
+ }
693
+ }
694
+ //# sourceMappingURL=grok-direct-sdk.js.map