instar 1.3.483 → 1.3.485

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.
@@ -4236,7 +4236,10 @@
4236
4236
  }
4237
4237
  historyLoading = true;
4238
4238
  showHistorySpinner();
4239
- wsSend({ type: 'history', session: activeSession, lines: nextBatch });
4239
+ // Remote sessions carry their machineId so the server relays the
4240
+ // scrollback fetch to the owning machine (§2.2) instead of capturing
4241
+ // locally (which can only ever return empty for a remote session).
4242
+ wsSend({ type: 'history', session: activeSession, lines: nextBatch, ...(activeMachineId ? { machineId: activeMachineId } : {}) });
4240
4243
  }
4241
4244
 
4242
4245
  function showHistorySpinner() {
@@ -22,10 +22,27 @@
22
22
  * from `degraded` (framework present, this call failed) and
23
23
  * `skipped-abbreviated` (author chose the fast path).
24
24
  *
25
- * codex is the FIRST supported framework; the registry
26
- * (`SUPPORTED_REVIEWER_FRAMEWORKS`) is the single seam for gemini-cli and
27
- * others to plug in later (Out of Scope here). Adding a framework is one
28
- * registry entry + one `id`-union extension no skill change.
25
+ * codex is the FIRST supported framework; gemini-cli is the SECOND (Piece 3 of
26
+ * docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md cross-model convergence
27
+ * hardening). The registry (`SUPPORTED_REVIEWER_FRAMEWORKS`) remains the single
28
+ * seam for further frameworks. Adding a framework is one registry entry + one
29
+ * `id`-union extension — no skill change.
30
+ *
31
+ * Piece 3 additions (all signal-only, never-throw, same invariants as above):
32
+ * - `detectGeminiReviewer` + the gemini registry entry (family diversity).
33
+ * - `detectAllCrossModelReviewers` — collect EVERY available framework, not
34
+ * just the first match, so the skill runs one external pass per family.
35
+ * - `isConcreteReviewerModel` — the fail-loud model canary: a tier word
36
+ * ('capable', 'fast', …) falling through model resolution degrades the
37
+ * review LOUDLY instead of silently selecting a dead reviewer.
38
+ * - `hashSpecReviewableBody` — delta-gating: externals re-run only when the
39
+ * spec's reviewable body (frontmatter stripped) actually changed.
40
+ * - `recordFrameworkActivationObservation` / `wasNonClaudeFrameworkActiveWithin`
41
+ * — the durable standing-framework baseline: activation is judged against
42
+ * a lookback window of recorded observations, not a just-in-time reading,
43
+ * so a just-before-converge framework deactivation cannot exempt a spec.
44
+ * - `TRUSTED_REVIEWER_FRAMEWORKS` — the provider allowlist (no spec egress
45
+ * to untrusted/custom endpoints).
29
46
  */
30
47
  import { type IntelligenceFramework } from './intelligenceProviderFactory.js';
31
48
  /**
@@ -70,7 +87,7 @@ export declare function orderContextDeterministically(context: readonly Referenc
70
87
  * Rule-1 / auth-probe vocabulary so a report can render a specific
71
88
  * remediation.
72
89
  */
73
- export type CrossModelUnavailableReason = 'codex-not-installed' | 'codex-not-authed' | 'codex-auth-apikey-forbidden' | 'no-supported-framework';
90
+ export type CrossModelUnavailableReason = 'codex-not-installed' | 'codex-not-authed' | 'codex-auth-apikey-forbidden' | 'gemini-not-installed' | 'gemini-not-authed' | 'no-supported-framework';
74
91
  export interface CrossModelDetectionResult {
75
92
  available: boolean;
76
93
  /** Present when available; the framework id that will run the review. */
@@ -99,6 +116,16 @@ export interface CrossModelDetectInputs {
99
116
  env?: NodeJS.ProcessEnv;
100
117
  /** Clock injection for the Rule-1 killswitch sunset check. */
101
118
  now?: Date;
119
+ /**
120
+ * Path to the gemini binary if detected, else null. Defaults to
121
+ * `detectGeminiPath()` (PATH + known-location resolution).
122
+ */
123
+ geminiPathDetected?: string | null;
124
+ /**
125
+ * Path to the gemini CLI's cached OAuth credentials. Defaults to
126
+ * `${GEMINI_HOME || ~/.gemini}/oauth_creds.json`.
127
+ */
128
+ geminiOauthCredsPath?: string;
102
129
  }
103
130
  /**
104
131
  * Detect a codex reviewer. Returns `{ available: true, framework, model }`
@@ -109,6 +136,17 @@ export interface CrossModelDetectInputs {
109
136
  * real host. It NEVER throws.
110
137
  */
111
138
  export declare function detectCodexReviewer(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
139
+ /**
140
+ * Detect a gemini reviewer (Piece 3 — the second family in the registry).
141
+ * Returns `{ available: true, framework, model }` iff BOTH of: gemini binary
142
+ * detected, cached OAuth credentials present (`access_token` or
143
+ * `refresh_token`). Any miss → a specific reason.
144
+ *
145
+ * Pure-ish: all external inputs are injectable (mirrors
146
+ * `detectCodexReviewer`). With no inputs it probes the real host. It NEVER
147
+ * throws.
148
+ */
149
+ export declare function detectGeminiReviewer(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
112
150
  export interface ReviewerResult {
113
151
  /** Outcome class for the cross-model pass. */
114
152
  status: 'ok' | 'degraded' | 'unavailable';
@@ -162,22 +200,66 @@ export interface ReviewerInvokeArgs {
162
200
  timeoutMs?: number;
163
201
  }): Promise<string>;
164
202
  };
203
+ /**
204
+ * Optional detection override — `runCrossModelReview` passes the detection
205
+ * it already computed (so review never re-probes the host), and tests inject
206
+ * synthetic detections (e.g. a tier-word model to exercise the canary).
207
+ * Absent → the entry runs its own real-host detect, as before (back-compat).
208
+ */
209
+ detectionOverride?: CrossModelDetectionResult;
165
210
  }
211
+ /**
212
+ * Fail-loud model canary (Piece 3). A cross-model review must run on a
213
+ * CONCRETE model id — never a bare tier word that fell through a
214
+ * tier→model resolution map (the `resolveModelForFramework` fall-through
215
+ * failure class: the literal string 'capable' is not a model, and silently
216
+ * passing it selects a dead reviewer). Returns false for undefined/empty
217
+ * strings and for bare tier words (case-insensitive). Both registry entries
218
+ * check this BEFORE invoking the provider and degrade LOUDLY
219
+ * (`model-resolution-canary`) on a failure.
220
+ */
221
+ export declare function isConcreteReviewerModel(model: string | undefined): boolean;
166
222
  /**
167
223
  * The supported-reviewer registry. codex first — the order IS the preference
168
- * order. gemini-cli and others land here as later steps (Out of Scope).
224
+ * order. gemini second (Piece 3). Further frameworks land here as later
225
+ * registry entries. NOTE: the registry only ever carries first-party OAuth
226
+ * CLI adapters — see `TRUSTED_REVIEWER_FRAMEWORKS` below.
169
227
  */
170
228
  export declare const SUPPORTED_REVIEWER_FRAMEWORKS: SupportedReviewerFramework[];
229
+ /**
230
+ * Trusted-provider allowlist (Piece 3 — no spec egress to untrusted
231
+ * endpoints). The registry only ever carries FIRST-PARTY OAuth CLI adapters:
232
+ * the full spec text is handed to the reviewer model, so it must NEVER be
233
+ * sent to a custom/base-URL endpoint an operator (or attacker) pointed a
234
+ * framework at. The pi-cli multi-provider case is deliberately EXCLUDED from
235
+ * cross-model review for exactly this reason — its provider may be a custom
236
+ * endpoint. A framework id outside this list is refused by the script
237
+ * wrapper (`--family`) with reason `untrusted-framework`.
238
+ */
239
+ export declare const TRUSTED_REVIEWER_FRAMEWORKS: readonly string[];
240
+ /** Is `id` on the trusted first-party reviewer allowlist? */
241
+ export declare function isTrustedReviewerFramework(id: string): boolean;
171
242
  /**
172
243
  * Walk the registry in preference order and return the FIRST available
173
- * framework's detection result. If none is available, returns the codex
174
- * reason when there's exactly one entry (so the report can be specific), else
175
- * the generic `no-supported-framework`.
244
+ * framework's detection result (back-compat single-reviewer entry point
245
+ * the multi-family collection is `detectAllCrossModelReviewers`). If none is
246
+ * available, returns the preference-leader's specific reason (codex today) so
247
+ * the report can render a concrete remediation, rather than the generic
248
+ * `no-supported-framework`.
176
249
  *
177
250
  * SIGNAL-ONLY: never throws, never blocks. A `false` simply routes the skill
178
251
  * to the internal-only fallback (spec §4).
179
252
  */
180
253
  export declare function detectCrossModelReviewer(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
254
+ /**
255
+ * Collect EVERY available reviewer framework, in registry preference order
256
+ * (Piece 3 — family diversity: GPT and Gemini catch different failure
257
+ * classes, so the externals pass runs one review PER available family, not
258
+ * first-match-only). Returns an empty array when none is available.
259
+ *
260
+ * SIGNAL-ONLY: never throws, never blocks.
261
+ */
262
+ export declare function detectAllCrossModelReviewers(inputs?: CrossModelDetectInputs): CrossModelDetectionResult[];
181
263
  /**
182
264
  * Map a provider rejection into a coarse `degraded` reason. The provider
183
265
  * surfaces timeouts, non-zero exits, and (via the circuit breaker) rate
@@ -306,5 +388,53 @@ export declare function runCrossModelReview(args: {
306
388
  detectInputs?: CrossModelDetectInputs;
307
389
  providerOverride?: ReviewerInvokeArgs['providerOverride'];
308
390
  }): Promise<ReviewerResult>;
391
+ /**
392
+ * Hash the spec's REVIEWABLE body (Piece 3 delta-gating): sha256 hex of the
393
+ * spec text with the leading YAML frontmatter block stripped and line endings
394
+ * normalized (\r\n → \n). Frontmatter is excluded so tag-writes
395
+ * (`review-convergence`, `approved: true`, cross-model flags) and other
396
+ * metadata edits do NOT change the hash — externals re-run only when the
397
+ * content a reviewer would actually read changed. The skill runs externals on
398
+ * round 1 and on any round where this hash differs from the last external
399
+ * pass's hash; an unchanged round records a skip-with-logged-note.
400
+ */
401
+ export declare function hashSpecReviewableBody(specText: string): string;
402
+ /**
403
+ * One recorded observation of which reviewer frameworks were available at a
404
+ * moment in time. Appended (JSONL) to
405
+ * `<stateDir>/state/framework-activation-history.jsonl` by the script
406
+ * wrapper's `--detect-only --state-dir` path on every detection.
407
+ */
408
+ export interface FrameworkActivationObservation {
409
+ /** ISO timestamp; defaults to now. */
410
+ ts?: string;
411
+ /** framework id → was it available/active at observation time. */
412
+ frameworks: Record<string, boolean>;
413
+ }
414
+ /**
415
+ * Append ONE observation line to the durable framework-activation history
416
+ * (Piece 3 — the standing-framework baseline). The externals-mandatory check
417
+ * is judged against this recorded history over a lookback window, NOT a
418
+ * just-in-time reading — so deactivating a framework right before converging
419
+ * cannot present the agent as "genuinely single-framework."
420
+ *
421
+ * mkdir -p's the state dir; caps the file at the most recent
422
+ * `ACTIVATION_HISTORY_MAX_LINES` lines on every write. Filesystem errors
423
+ * propagate — a silently-unrecorded baseline would quietly weaken the
424
+ * mandatory check (fail-loud).
425
+ */
426
+ export declare function recordFrameworkActivationObservation(stateDir: string, observation: FrameworkActivationObservation): void;
427
+ /**
428
+ * Was ANY non-Claude reviewer framework active at ANY point within the
429
+ * lookback window, per the durable activation history? This is the
430
+ * externals-mandatory check (Piece 3): `true` means the cross-model pass is
431
+ * NON-SKIPPABLE for the spec — including when a framework was deactivated
432
+ * inside the window (a just-before-converge deactivation does not exempt the
433
+ * spec). The advisory "externals unavailable" floor is legitimate only when
434
+ * this returns `false` across the whole lookback.
435
+ *
436
+ * NEVER throws: a missing file → false; corrupt lines are skipped.
437
+ */
438
+ export declare function wasNonClaudeFrameworkActiveWithin(stateDir: string, lookbackDays: number, now?: Date): boolean;
309
439
  export {};
310
440
  //# sourceMappingURL=crossModelReviewer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crossModelReviewer.d.ts","sourceRoot":"","sources":["../../src/core/crossModelReviewer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crossModelReviewer.d.ts","sourceRoot":"","sources":["../../src/core/crossModelReviewer.ts"],"names":[],"mappings":"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"}
@@ -22,17 +22,36 @@
22
22
  * from `degraded` (framework present, this call failed) and
23
23
  * `skipped-abbreviated` (author chose the fast path).
24
24
  *
25
- * codex is the FIRST supported framework; the registry
26
- * (`SUPPORTED_REVIEWER_FRAMEWORKS`) is the single seam for gemini-cli and
27
- * others to plug in later (Out of Scope here). Adding a framework is one
28
- * registry entry + one `id`-union extension no skill change.
25
+ * codex is the FIRST supported framework; gemini-cli is the SECOND (Piece 3 of
26
+ * docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md cross-model convergence
27
+ * hardening). The registry (`SUPPORTED_REVIEWER_FRAMEWORKS`) remains the single
28
+ * seam for further frameworks. Adding a framework is one registry entry + one
29
+ * `id`-union extension — no skill change.
30
+ *
31
+ * Piece 3 additions (all signal-only, never-throw, same invariants as above):
32
+ * - `detectGeminiReviewer` + the gemini registry entry (family diversity).
33
+ * - `detectAllCrossModelReviewers` — collect EVERY available framework, not
34
+ * just the first match, so the skill runs one external pass per family.
35
+ * - `isConcreteReviewerModel` — the fail-loud model canary: a tier word
36
+ * ('capable', 'fast', …) falling through model resolution degrades the
37
+ * review LOUDLY instead of silently selecting a dead reviewer.
38
+ * - `hashSpecReviewableBody` — delta-gating: externals re-run only when the
39
+ * spec's reviewable body (frontmatter stripped) actually changed.
40
+ * - `recordFrameworkActivationObservation` / `wasNonClaudeFrameworkActiveWithin`
41
+ * — the durable standing-framework baseline: activation is judged against
42
+ * a lookback window of recorded observations, not a just-in-time reading,
43
+ * so a just-before-converge framework deactivation cannot exempt a spec.
44
+ * - `TRUSTED_REVIEWER_FRAMEWORKS` — the provider allowlist (no spec egress
45
+ * to untrusted/custom endpoints).
29
46
  */
47
+ import crypto from 'node:crypto';
30
48
  import fs from 'node:fs';
31
49
  import os from 'node:os';
32
50
  import path from 'node:path';
33
- import { detectCodexPath } from './Config.js';
51
+ import { detectCodexPath, detectGeminiPath } from './Config.js';
34
52
  import { validateRule1 } from '../providers/adapters/openai-codex/credentials.js';
35
53
  import { resolveCliModelFlag } from '../providers/adapters/openai-codex/models.js';
54
+ import { resolveCliModelFlag as resolveGeminiModelFlag } from '../providers/adapters/gemini-cli/models.js';
36
55
  import { buildIntelligenceProvider, } from './intelligenceProviderFactory.js';
37
56
  // ── Constants (tunable) ─────────────────────────────────────────────────
38
57
  /**
@@ -154,6 +173,86 @@ export function detectCodexReviewer(inputs = {}) {
154
173
  model: resolveCliModelFlag(REVIEW_MODEL_TIER),
155
174
  };
156
175
  }
176
+ /**
177
+ * Resolve the default gemini oauth_creds.json path. DELIBERATELY no env-var
178
+ * override: the gemini CLI (verified v0.25.2) resolves creds at
179
+ * `~/.gemini/oauth_creds.json` UNCONDITIONALLY — there is no GEMINI_HOME.
180
+ * Honoring one here would make detection probe a path the CLI never reads
181
+ * (false-unavailable on an authed host -> the gemini pass silently skipped AND
182
+ * a false `gemini-cli:false` recorded into the activation baseline — the exact
183
+ * suppression Piece 3 exists to prevent). Tests inject `geminiOauthCredsPath`.
184
+ */
185
+ function defaultGeminiOauthCredsPath() {
186
+ return path.join(os.homedir(), '.gemini', 'oauth_creds.json');
187
+ }
188
+ /**
189
+ * Is the gemini CLI's cached OAuth credentials file an authed shape? Authed
190
+ * iff the file parses as JSON with a non-empty string `access_token` OR
191
+ * `refresh_token` (the CLI refreshes an expired access token from the refresh
192
+ * token, so either is a usable seat). A missing / unreadable / malformed file
193
+ * → false (not authed). Never throws.
194
+ */
195
+ function geminiOauthCredsAuthed(credsPath) {
196
+ try {
197
+ const raw = fs.readFileSync(credsPath, 'utf-8');
198
+ const parsed = JSON.parse(raw);
199
+ const nonEmptyString = (v) => typeof v === 'string' && v.length > 0;
200
+ return nonEmptyString(parsed?.access_token) || nonEmptyString(parsed?.refresh_token);
201
+ }
202
+ catch {
203
+ // @silent-fallback-ok — deny-safe: missing/unreadable/malformed creds mean
204
+ // "not authed" (the reviewer is reported unavailable with a named reason);
205
+ // mirrors the codex auth probe above.
206
+ return false;
207
+ }
208
+ }
209
+ /**
210
+ * Detect a gemini reviewer (Piece 3 — the second family in the registry).
211
+ * Returns `{ available: true, framework, model }` iff BOTH of: gemini binary
212
+ * detected, cached OAuth credentials present (`access_token` or
213
+ * `refresh_token`). Any miss → a specific reason.
214
+ *
215
+ * Pure-ish: all external inputs are injectable (mirrors
216
+ * `detectCodexReviewer`). With no inputs it probes the real host. It NEVER
217
+ * throws.
218
+ */
219
+ export function detectGeminiReviewer(inputs = {}) {
220
+ const env = inputs.env ?? process.env;
221
+ const geminiPath = inputs.geminiPathDetected !== undefined ? inputs.geminiPathDetected : detectGeminiPath();
222
+ const credsPath = inputs.geminiOauthCredsPath ?? defaultGeminiOauthCredsPath();
223
+ // 1. Binary present?
224
+ if (!geminiPath) {
225
+ return { available: false, reason: 'gemini-not-installed' };
226
+ }
227
+ // 2. Authed via the CLI's cached OAuth?
228
+ if (!geminiOauthCredsAuthed(credsPath)) {
229
+ return { available: false, reason: 'gemini-not-authed' };
230
+ }
231
+ return {
232
+ available: true,
233
+ framework: 'gemini-cli',
234
+ model: resolveGeminiModelFlag(REVIEW_MODEL_TIER),
235
+ };
236
+ }
237
+ /**
238
+ * Fail-loud model canary (Piece 3). A cross-model review must run on a
239
+ * CONCRETE model id — never a bare tier word that fell through a
240
+ * tier→model resolution map (the `resolveModelForFramework` fall-through
241
+ * failure class: the literal string 'capable' is not a model, and silently
242
+ * passing it selects a dead reviewer). Returns false for undefined/empty
243
+ * strings and for bare tier words (case-insensitive). Both registry entries
244
+ * check this BEFORE invoking the provider and degrade LOUDLY
245
+ * (`model-resolution-canary`) on a failure.
246
+ */
247
+ export function isConcreteReviewerModel(model) {
248
+ if (typeof model !== 'string')
249
+ return false;
250
+ const trimmed = model.trim();
251
+ if (trimmed.length === 0)
252
+ return false;
253
+ const TIER_WORDS = new Set(['fast', 'balanced', 'capable', 'haiku', 'sonnet', 'opus']);
254
+ return !TIER_WORDS.has(trimmed.toLowerCase());
255
+ }
157
256
  /**
158
257
  * The codex reviewer entry. Detection delegates to `detectCodexReviewer`;
159
258
  * `review` routes through the factory-built `CodexCliIntelligenceProvider`.
@@ -162,9 +261,20 @@ const codexReviewer = {
162
261
  id: 'codex-cli',
163
262
  detect: (inputs) => detectCodexReviewer(inputs),
164
263
  review: async (args) => {
165
- const detection = detectCodexReviewer();
264
+ const detection = args.detectionOverride ?? detectCodexReviewer();
166
265
  const model = detection.model ?? resolveCliModelFlag(REVIEW_MODEL_TIER);
167
266
  const tag = `cross-model:codex-cli:${model}`;
267
+ // Fail-loud model canary (Piece 3): NEVER silently review with a
268
+ // tier-word model — a fall-through 'capable' is a dead reviewer.
269
+ if (!isConcreteReviewerModel(model)) {
270
+ return {
271
+ status: 'degraded',
272
+ framework: 'codex-cli',
273
+ model,
274
+ reason: 'model-resolution-canary',
275
+ flag: `cross-model-review: codex-cli:${model} (degraded: model-resolution-canary)`,
276
+ };
277
+ }
168
278
  // Build (or accept an injected) provider. The factory wraps it in the
169
279
  // account-global circuit breaker, so a rate-limited review degrades the
170
280
  // same way every other instar LLM call does.
@@ -210,16 +320,107 @@ const codexReviewer = {
210
320
  };
211
321
  },
212
322
  };
323
+ /**
324
+ * The gemini reviewer entry (Piece 3 — family diversity: a second non-Claude
325
+ * model family alongside GPT). Detection delegates to `detectGeminiReviewer`;
326
+ * `review` routes through the factory-built `GeminiCliIntelligenceProvider`
327
+ * (same circuit-breaker wrapping, same degraded semantics as codex).
328
+ */
329
+ const geminiReviewer = {
330
+ id: 'gemini-cli',
331
+ detect: (inputs) => detectGeminiReviewer(inputs),
332
+ review: async (args) => {
333
+ const detection = args.detectionOverride ?? detectGeminiReviewer();
334
+ const model = detection.model ?? resolveGeminiModelFlag(REVIEW_MODEL_TIER);
335
+ const tag = `cross-model:gemini-cli:${model}`;
336
+ // Fail-loud model canary (Piece 3): NEVER silently review with a
337
+ // tier-word model — a fall-through 'capable' is a dead reviewer.
338
+ if (!isConcreteReviewerModel(model)) {
339
+ return {
340
+ status: 'degraded',
341
+ framework: 'gemini-cli',
342
+ model,
343
+ reason: 'model-resolution-canary',
344
+ flag: `cross-model-review: gemini-cli:${model} (degraded: model-resolution-canary)`,
345
+ };
346
+ }
347
+ // Build (or accept an injected) provider. The factory wraps it in the
348
+ // account-global circuit breaker, so a rate-limited review degrades the
349
+ // same way every other instar LLM call does.
350
+ const provider = args.providerOverride ??
351
+ buildIntelligenceProvider({ framework: 'gemini-cli' });
352
+ if (!provider) {
353
+ // Binary vanished between detect and review (or detection said
354
+ // unavailable and review was called anyway). Degraded, not a throw.
355
+ return {
356
+ status: 'degraded',
357
+ framework: 'gemini-cli',
358
+ model,
359
+ reason: 'provider-unavailable',
360
+ flag: `cross-model-review: gemini-cli:${model} (degraded: provider-unavailable)`,
361
+ };
362
+ }
363
+ let raw;
364
+ try {
365
+ raw = await provider.evaluate(args.promptText, {
366
+ model: REVIEW_MODEL_TIER,
367
+ timeoutMs: args.timeoutMs,
368
+ attribution: { component: 'crossModelReviewer' }, // attribution for /metrics/features
369
+ });
370
+ }
371
+ catch (err) {
372
+ const reason = classifyReviewFailure(err);
373
+ return {
374
+ status: 'degraded',
375
+ framework: 'gemini-cli',
376
+ model,
377
+ reason,
378
+ flag: `cross-model-review: gemini-cli:${model} (degraded: ${reason})`,
379
+ };
380
+ }
381
+ const parsed = parseReviewerReply(raw, tag);
382
+ return {
383
+ status: 'ok',
384
+ framework: 'gemini-cli',
385
+ model,
386
+ verdict: parsed.verdict,
387
+ findings: [parsed],
388
+ flag: `cross-model-review: gemini-cli:${model}`,
389
+ };
390
+ },
391
+ };
213
392
  /**
214
393
  * The supported-reviewer registry. codex first — the order IS the preference
215
- * order. gemini-cli and others land here as later steps (Out of Scope).
394
+ * order. gemini second (Piece 3). Further frameworks land here as later
395
+ * registry entries. NOTE: the registry only ever carries first-party OAuth
396
+ * CLI adapters — see `TRUSTED_REVIEWER_FRAMEWORKS` below.
216
397
  */
217
- export const SUPPORTED_REVIEWER_FRAMEWORKS = [codexReviewer];
398
+ export const SUPPORTED_REVIEWER_FRAMEWORKS = [
399
+ codexReviewer,
400
+ geminiReviewer,
401
+ ];
402
+ /**
403
+ * Trusted-provider allowlist (Piece 3 — no spec egress to untrusted
404
+ * endpoints). The registry only ever carries FIRST-PARTY OAuth CLI adapters:
405
+ * the full spec text is handed to the reviewer model, so it must NEVER be
406
+ * sent to a custom/base-URL endpoint an operator (or attacker) pointed a
407
+ * framework at. The pi-cli multi-provider case is deliberately EXCLUDED from
408
+ * cross-model review for exactly this reason — its provider may be a custom
409
+ * endpoint. A framework id outside this list is refused by the script
410
+ * wrapper (`--family`) with reason `untrusted-framework`.
411
+ */
412
+ export const TRUSTED_REVIEWER_FRAMEWORKS = ['codex-cli', 'gemini-cli'];
413
+ /** Is `id` on the trusted first-party reviewer allowlist? */
414
+ export function isTrustedReviewerFramework(id) {
415
+ return TRUSTED_REVIEWER_FRAMEWORKS.includes(id);
416
+ }
218
417
  /**
219
418
  * Walk the registry in preference order and return the FIRST available
220
- * framework's detection result. If none is available, returns the codex
221
- * reason when there's exactly one entry (so the report can be specific), else
222
- * the generic `no-supported-framework`.
419
+ * framework's detection result (back-compat single-reviewer entry point
420
+ * the multi-family collection is `detectAllCrossModelReviewers`). If none is
421
+ * available, returns the preference-leader's specific reason (codex today) so
422
+ * the report can render a concrete remediation, rather than the generic
423
+ * `no-supported-framework`.
223
424
  *
224
425
  * SIGNAL-ONLY: never throws, never blocks. A `false` simply routes the skill
225
426
  * to the internal-only fallback (spec §4).
@@ -230,13 +431,29 @@ export function detectCrossModelReviewer(inputs = {}) {
230
431
  if (result.available)
231
432
  return result;
232
433
  }
233
- // Nothing available. Surface the single framework's specific reason when
234
- // there's only one entry (codex today); otherwise the generic reason.
235
- if (SUPPORTED_REVIEWER_FRAMEWORKS.length === 1) {
236
- return SUPPORTED_REVIEWER_FRAMEWORKS[0].detect(inputs);
237
- }
434
+ // Nothing available. Surface the preference-leader's specific reason.
435
+ const leader = SUPPORTED_REVIEWER_FRAMEWORKS[0];
436
+ if (leader)
437
+ return leader.detect(inputs);
238
438
  return { available: false, reason: 'no-supported-framework' };
239
439
  }
440
+ /**
441
+ * Collect EVERY available reviewer framework, in registry preference order
442
+ * (Piece 3 — family diversity: GPT and Gemini catch different failure
443
+ * classes, so the externals pass runs one review PER available family, not
444
+ * first-match-only). Returns an empty array when none is available.
445
+ *
446
+ * SIGNAL-ONLY: never throws, never blocks.
447
+ */
448
+ export function detectAllCrossModelReviewers(inputs = {}) {
449
+ const available = [];
450
+ for (const framework of SUPPORTED_REVIEWER_FRAMEWORKS) {
451
+ const result = framework.detect(inputs);
452
+ if (result.available)
453
+ available.push(result);
454
+ }
455
+ return available;
456
+ }
240
457
  // ── Failure classification ──────────────────────────────────────────────
241
458
  /**
242
459
  * Map a provider rejection into a coarse `degraded` reason. The provider
@@ -484,7 +701,116 @@ export async function runCrossModelReview(args) {
484
701
  return framework.review({
485
702
  promptText: args.assembled.promptText,
486
703
  timeoutMs: args.timeoutMs ?? REVIEW_TIMEOUT_MS,
704
+ // Hand the already-computed detection down so the entry never re-probes
705
+ // the host (and tests stay hermetic to the injected inputs).
706
+ detectionOverride: detection,
487
707
  ...(args.providerOverride ? { providerOverride: args.providerOverride } : {}),
488
708
  });
489
709
  }
710
+ // ── Delta-gating (reviewable-body hash) ─────────────────────────────────
711
+ /**
712
+ * Hash the spec's REVIEWABLE body (Piece 3 delta-gating): sha256 hex of the
713
+ * spec text with the leading YAML frontmatter block stripped and line endings
714
+ * normalized (\r\n → \n). Frontmatter is excluded so tag-writes
715
+ * (`review-convergence`, `approved: true`, cross-model flags) and other
716
+ * metadata edits do NOT change the hash — externals re-run only when the
717
+ * content a reviewer would actually read changed. The skill runs externals on
718
+ * round 1 and on any round where this hash differs from the last external
719
+ * pass's hash; an unchanged round records a skip-with-logged-note.
720
+ */
721
+ export function hashSpecReviewableBody(specText) {
722
+ const normalized = (specText ?? '').replace(/\r\n/g, '\n');
723
+ // Strip ONE leading frontmatter block: `---\n ... \n---` at the very top,
724
+ // where the close fence is a WHOLE line (anchored `(\n|$)`) — `\n---\n?`
725
+ // could terminate mid-line on `--- text` / `----` inside the block
726
+ // (second-pass finding, PR 3).
727
+ const body = normalized.replace(/^---\n[\s\S]*?\n---(\n|$)/, '');
728
+ return crypto.createHash('sha256').update(body, 'utf-8').digest('hex');
729
+ }
730
+ /** Max JSONL lines retained in the activation-history file. */
731
+ const ACTIVATION_HISTORY_MAX_LINES = 2000;
732
+ function activationHistoryPath(stateDir) {
733
+ return path.join(stateDir, 'state', 'framework-activation-history.jsonl');
734
+ }
735
+ /**
736
+ * Append ONE observation line to the durable framework-activation history
737
+ * (Piece 3 — the standing-framework baseline). The externals-mandatory check
738
+ * is judged against this recorded history over a lookback window, NOT a
739
+ * just-in-time reading — so deactivating a framework right before converging
740
+ * cannot present the agent as "genuinely single-framework."
741
+ *
742
+ * mkdir -p's the state dir; caps the file at the most recent
743
+ * `ACTIVATION_HISTORY_MAX_LINES` lines on every write. Filesystem errors
744
+ * propagate — a silently-unrecorded baseline would quietly weaken the
745
+ * mandatory check (fail-loud).
746
+ */
747
+ export function recordFrameworkActivationObservation(stateDir, observation) {
748
+ const file = activationHistoryPath(stateDir);
749
+ fs.mkdirSync(path.dirname(file), { recursive: true });
750
+ const entry = JSON.stringify({
751
+ ts: observation.ts ?? new Date().toISOString(),
752
+ frameworks: observation.frameworks,
753
+ });
754
+ let lines = [];
755
+ try {
756
+ lines = fs.readFileSync(file, 'utf-8').split('\n').filter((l) => l.trim().length > 0);
757
+ }
758
+ catch {
759
+ // No file yet — first observation.
760
+ }
761
+ lines.push(entry);
762
+ if (lines.length > ACTIVATION_HISTORY_MAX_LINES) {
763
+ lines = lines.slice(-ACTIVATION_HISTORY_MAX_LINES);
764
+ }
765
+ fs.writeFileSync(file, lines.join('\n') + '\n', 'utf-8');
766
+ }
767
+ /**
768
+ * Was ANY non-Claude reviewer framework active at ANY point within the
769
+ * lookback window, per the durable activation history? This is the
770
+ * externals-mandatory check (Piece 3): `true` means the cross-model pass is
771
+ * NON-SKIPPABLE for the spec — including when a framework was deactivated
772
+ * inside the window (a just-before-converge deactivation does not exempt the
773
+ * spec). The advisory "externals unavailable" floor is legitimate only when
774
+ * this returns `false` across the whole lookback.
775
+ *
776
+ * NEVER throws: a missing file → false; corrupt lines are skipped.
777
+ */
778
+ export function wasNonClaudeFrameworkActiveWithin(stateDir, lookbackDays, now) {
779
+ try {
780
+ const file = activationHistoryPath(stateDir);
781
+ const raw = fs.readFileSync(file, 'utf-8');
782
+ const cutoff = (now ?? new Date()).getTime() - lookbackDays * 24 * 60 * 60 * 1000;
783
+ for (const line of raw.split('\n')) {
784
+ if (!line.trim())
785
+ continue;
786
+ try {
787
+ const parsed = JSON.parse(line);
788
+ const ts = typeof parsed.ts === 'string' ? Date.parse(parsed.ts) : NaN;
789
+ if (!Number.isFinite(ts) || ts < cutoff)
790
+ continue;
791
+ const frameworks = parsed.frameworks;
792
+ if (frameworks && typeof frameworks === 'object') {
793
+ // Only TRUSTED reviewer framework ids count toward the baseline — a
794
+ // stray/hand-written key (e.g. "claude-code": true) must not flip
795
+ // the externals-mandatory decision (second-pass finding, PR 3).
796
+ const entries = Object.entries(frameworks);
797
+ if (entries.some(([id, v]) => v === true && isTrustedReviewerFramework(id))) {
798
+ return true;
799
+ }
800
+ }
801
+ }
802
+ catch {
803
+ // @silent-fallback-ok — a corrupt history line is skipped (never throws);
804
+ // the read is a union-over-time, so a lost line can only UNDER-report
805
+ // activation, which keeps externals mandatory-safe, never lies them off.
806
+ }
807
+ }
808
+ return false;
809
+ }
810
+ catch {
811
+ // @silent-fallback-ok — a missing/unreadable history file is the expected
812
+ // pre-first-run state: no recorded activation is the correct answer.
813
+ return false;
814
+ }
815
+ }
490
816
  //# sourceMappingURL=crossModelReviewer.js.map