instar 1.3.482 → 1.3.484
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/dist/core/crossModelReviewer.d.ts +139 -9
- package/dist/core/crossModelReviewer.d.ts.map +1 -1
- package/dist/core/crossModelReviewer.js +342 -16
- package/dist/core/crossModelReviewer.js.map +1 -1
- package/package.json +1 -1
- package/skills/spec-converge/SKILL.md +57 -17
- package/skills/spec-converge/scripts/cross-model-review.mjs +113 -15
- package/skills/spec-converge/scripts/write-convergence-tag.mjs +111 -5
- package/skills/spec-converge/templates/reviewer-decision-completeness.md +83 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.483.md +28 -0
- package/upgrades/1.3.484.md +37 -0
- package/upgrades/side-effects/cross-model-hardening.md +93 -0
- package/upgrades/side-effects/decision-completeness-gate.md +85 -0
|
@@ -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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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
|
|
174
|
-
*
|
|
175
|
-
* the
|
|
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":"AAAA
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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 = [
|
|
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
|
|
221
|
-
*
|
|
222
|
-
* the
|
|
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
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
return
|
|
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
|