@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.
- package/LICENSE +201 -0
- package/README.md +150 -0
- package/dist/adapters/_shared.d.ts +45 -0
- package/dist/adapters/_shared.d.ts.map +1 -0
- package/dist/adapters/_shared.js +200 -0
- package/dist/adapters/_shared.js.map +1 -0
- package/dist/adapters/codex-sdk.d.ts +279 -0
- package/dist/adapters/codex-sdk.d.ts.map +1 -0
- package/dist/adapters/codex-sdk.js +930 -0
- package/dist/adapters/codex-sdk.js.map +1 -0
- package/dist/adapters/critic-result-schema.d.ts +215 -0
- package/dist/adapters/critic-result-schema.d.ts.map +1 -0
- package/dist/adapters/critic-result-schema.js +153 -0
- package/dist/adapters/critic-result-schema.js.map +1 -0
- package/dist/adapters/critic.d.ts +62 -0
- package/dist/adapters/critic.d.ts.map +1 -0
- package/dist/adapters/critic.js +79 -0
- package/dist/adapters/critic.js.map +1 -0
- package/dist/adapters/cursor-sdk.d.ts +153 -0
- package/dist/adapters/cursor-sdk.d.ts.map +1 -0
- package/dist/adapters/cursor-sdk.js +818 -0
- package/dist/adapters/cursor-sdk.js.map +1 -0
- package/dist/adapters/gemini-sdk.d.ts +113 -0
- package/dist/adapters/gemini-sdk.d.ts.map +1 -0
- package/dist/adapters/gemini-sdk.js +532 -0
- package/dist/adapters/gemini-sdk.js.map +1 -0
- package/dist/adapters/grok-direct-sdk.d.ts +148 -0
- package/dist/adapters/grok-direct-sdk.d.ts.map +1 -0
- package/dist/adapters/grok-direct-sdk.js +694 -0
- package/dist/adapters/grok-direct-sdk.js.map +1 -0
- package/dist/branch-protection/audit_branch_protection.py +759 -0
- package/dist/branch-protection/index.d.ts +25 -0
- package/dist/branch-protection/index.d.ts.map +1 -0
- package/dist/branch-protection/index.js +70 -0
- package/dist/branch-protection/index.js.map +1 -0
- package/dist/branch-protection/spec-default.yaml +314 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +581 -0
- package/dist/cli.js.map +1 -0
- package/dist/cycle-doc-validator/index.d.ts +43 -0
- package/dist/cycle-doc-validator/index.d.ts.map +1 -0
- package/dist/cycle-doc-validator/index.js +69 -0
- package/dist/cycle-doc-validator/index.js.map +1 -0
- package/dist/cycle-doc-validator/validate_cycle_doc.py +1260 -0
- package/dist/cycle-tracker-sync/attribute_pr_cycle_ref.py +384 -0
- package/dist/cycle-tracker-sync/index.d.ts +20 -0
- package/dist/cycle-tracker-sync/index.d.ts.map +1 -0
- package/dist/cycle-tracker-sync/index.js +71 -0
- package/dist/cycle-tracker-sync/index.js.map +1 -0
- package/dist/cycle-tracker-sync/sync_cycle_trackers.py +1093 -0
- package/dist/evidence/audit-trail.d.ts +59 -0
- package/dist/evidence/audit-trail.d.ts.map +1 -0
- package/dist/evidence/audit-trail.js +283 -0
- package/dist/evidence/audit-trail.js.map +1 -0
- package/dist/evidence/index.d.ts +4 -0
- package/dist/evidence/index.d.ts.map +1 -0
- package/dist/evidence/index.js +29 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/per-sha.d.ts +13 -0
- package/dist/evidence/per-sha.d.ts.map +1 -0
- package/dist/evidence/per-sha.js +97 -0
- package/dist/evidence/per-sha.js.map +1 -0
- package/dist/evidence/quality-gates.d.ts +19 -0
- package/dist/evidence/quality-gates.d.ts.map +1 -0
- package/dist/evidence/quality-gates.js +212 -0
- package/dist/evidence/quality-gates.js.map +1 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +414 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +4 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +99 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +42 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy/baseline.d.ts +15 -0
- package/dist/policy/baseline.d.ts.map +1 -0
- package/dist/policy/baseline.js +115 -0
- package/dist/policy/baseline.js.map +1 -0
- package/dist/policy/config.d.ts +64 -0
- package/dist/policy/config.d.ts.map +1 -0
- package/dist/policy/config.js +363 -0
- package/dist/policy/config.js.map +1 -0
- package/dist/policy/gate.d.ts +80 -0
- package/dist/policy/gate.d.ts.map +1 -0
- package/dist/policy/gate.js +1019 -0
- package/dist/policy/gate.js.map +1 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +14 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/merge-queue.d.ts +183 -0
- package/dist/policy/merge-queue.d.ts.map +1 -0
- package/dist/policy/merge-queue.js +310 -0
- package/dist/policy/merge-queue.js.map +1 -0
- package/dist/policy/profile.d.ts +98 -0
- package/dist/policy/profile.d.ts.map +1 -0
- package/dist/policy/profile.js +156 -0
- package/dist/policy/profile.js.map +1 -0
- package/dist/policy/tdd-classifier.d.ts +18 -0
- package/dist/policy/tdd-classifier.d.ts.map +1 -0
- package/dist/policy/tdd-classifier.js +79 -0
- package/dist/policy/tdd-classifier.js.map +1 -0
- package/dist/prompt.d.ts +13 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +175 -0
- package/dist/prompt.js.map +1 -0
- package/dist/report.d.ts +88 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +376 -0
- package/dist/report.js.map +1 -0
- package/dist/runner.d.ts +30 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +456 -0
- package/dist/runner.js.map +1 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +19 -0
- package/dist/security.js.map +1 -0
- package/dist/trusted-surface/rebind.d.ts +10 -0
- package/dist/trusted-surface/rebind.d.ts.map +1 -0
- package/dist/trusted-surface/rebind.js +154 -0
- package/dist/trusted-surface/rebind.js.map +1 -0
- 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
|