instar 1.3.191 → 1.3.192
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 +310 -0
- package/dist/core/crossModelReviewer.d.ts.map +1 -0
- package/dist/core/crossModelReviewer.js +489 -0
- package/dist/core/crossModelReviewer.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.192.md +37 -0
- package/upgrades/side-effects/codex-crossreview-stepB.md +83 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crossModelReviewer — Step B of the tiered development process.
|
|
3
|
+
*
|
|
4
|
+
* Re-platforms the `/spec-converge` external "cross-model" reviewer onto the
|
|
5
|
+
* agent's own installed `codex` CLI. The external pass that the skill used to
|
|
6
|
+
* describe as running "via the /crossreview pattern" (a never-built
|
|
7
|
+
* placeholder) is now a grounded mechanism: detect whether a supported reviewer
|
|
8
|
+
* framework is installed + authed, assemble the cross-model reviewer prompt
|
|
9
|
+
* (spec + referenced context, bounded to a budget), and run it THROUGH the
|
|
10
|
+
* existing `CodexCliIntelligenceProvider` (the factory with
|
|
11
|
+
* `framework: 'codex-cli'`, model `capable` → GPT-tier).
|
|
12
|
+
*
|
|
13
|
+
* Design invariants (see docs/specs/codex-crossreview-stepB-spec.md):
|
|
14
|
+
* - Detection is a pure function with injectable inputs (no real spawns in
|
|
15
|
+
* unit tests). It is SIGNAL-ONLY — it never throws and never blocks.
|
|
16
|
+
* - Reviewer invocation reuses the provider (its scratch-dir clean-notepad,
|
|
17
|
+
* env allowlist, `--skip-git-repo-check`, and the account-global circuit
|
|
18
|
+
* breaker the factory wraps it in). The ONLY new spawn-adjacent code is
|
|
19
|
+
* prompt assembly + result parsing.
|
|
20
|
+
* - Every failure mode routes toward internal-only convergence or a captured
|
|
21
|
+
* raw finding — never a stall. `unavailable` (no framework) is distinct
|
|
22
|
+
* from `degraded` (framework present, this call failed) and
|
|
23
|
+
* `skipped-abbreviated` (author chose the fast path).
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
import { type IntelligenceFramework } from './intelligenceProviderFactory.js';
|
|
31
|
+
/**
|
|
32
|
+
* Per-call timeout for a cross-model spec review. A reasoning review of a
|
|
33
|
+
* full spec is far heavier than the provider's 30s judgment-call default, so
|
|
34
|
+
* Step B bumps it to 120s (spec §2).
|
|
35
|
+
*/
|
|
36
|
+
export declare const REVIEW_TIMEOUT_MS = 120000;
|
|
37
|
+
/**
|
|
38
|
+
* Total context budget (spec + referenced docs) inlined into the reviewer
|
|
39
|
+
* prompt. codex runs in an empty read-only scratch dir with no repo access,
|
|
40
|
+
* so referenced context MUST be inlined; this bounds the prompt size (spec §2).
|
|
41
|
+
* The spec is always included in full; referenced context fills the remainder
|
|
42
|
+
* and is truncated (with a loud note) if it overflows.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CONTEXT_BUDGET_BYTES: number;
|
|
45
|
+
/**
|
|
46
|
+
* Deterministic priority ordering for referenced context (spec §2, F4).
|
|
47
|
+
*
|
|
48
|
+
* When the 60KB budget can't hold every referenced doc, truncation MUST be
|
|
49
|
+
* deterministic — the same spec + same docs always drop the same docs — so a
|
|
50
|
+
* review is reproducible and the "what got dropped" note is stable. The
|
|
51
|
+
* constitutional / lessons docs are the highest-value context for a reviewer
|
|
52
|
+
* (they're what the lessons-aware internal reviewer reads), so they are kept
|
|
53
|
+
* FIRST; everything else keeps the spec-declared link order (the order the
|
|
54
|
+
* caller passed the docs in, which is the order they appear in the spec).
|
|
55
|
+
*
|
|
56
|
+
* A doc whose path contains one of these substrings sorts ahead of the rest,
|
|
57
|
+
* in THIS order. Ties (and all non-priority docs) preserve the caller's order
|
|
58
|
+
* via a stable sort.
|
|
59
|
+
*/
|
|
60
|
+
export declare const CONTEXT_PRIORITY_SUBSTRINGS: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Order referenced context deterministically: constitutional/lessons docs
|
|
63
|
+
* first (per CONTEXT_PRIORITY_SUBSTRINGS), then the caller's spec-declared link
|
|
64
|
+
* order for the rest. A stable sort on the priority rank achieves both — equal
|
|
65
|
+
* ranks keep their input order. Pure; never mutates the input.
|
|
66
|
+
*/
|
|
67
|
+
export declare function orderContextDeterministically(context: readonly ReferencedContextDoc[]): ReferencedContextDoc[];
|
|
68
|
+
/**
|
|
69
|
+
* Reasons a supported reviewer framework is unavailable. Mirrors the
|
|
70
|
+
* Rule-1 / auth-probe vocabulary so a report can render a specific
|
|
71
|
+
* remediation.
|
|
72
|
+
*/
|
|
73
|
+
export type CrossModelUnavailableReason = 'codex-not-installed' | 'codex-not-authed' | 'codex-auth-apikey-forbidden' | 'no-supported-framework';
|
|
74
|
+
export interface CrossModelDetectionResult {
|
|
75
|
+
available: boolean;
|
|
76
|
+
/** Present when available; the framework id that will run the review. */
|
|
77
|
+
framework?: IntelligenceFramework;
|
|
78
|
+
/** Present when available; the concrete model the review resolves to. */
|
|
79
|
+
model?: string;
|
|
80
|
+
/** Present when unavailable; a specific machine-readable reason. */
|
|
81
|
+
reason?: CrossModelUnavailableReason;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Injectable inputs for `detectCrossModelReviewer` so the detection logic is
|
|
85
|
+
* unit-testable without real spawns or a real `~/.codex/auth.json`.
|
|
86
|
+
*/
|
|
87
|
+
export interface CrossModelDetectInputs {
|
|
88
|
+
/**
|
|
89
|
+
* Path to the codex binary if detected, else null. Defaults to
|
|
90
|
+
* `detectCodexPath()` (PATH + asdf/nvm-shim resolution).
|
|
91
|
+
*/
|
|
92
|
+
codexPathDetected?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Path to the codex auth.json. Defaults to
|
|
95
|
+
* `${CODEX_HOME || ~/.codex}/auth.json`.
|
|
96
|
+
*/
|
|
97
|
+
authJsonPath?: string;
|
|
98
|
+
/** Process env (for the Rule-1 OPENAI_API_KEY probe). Defaults to process.env. */
|
|
99
|
+
env?: NodeJS.ProcessEnv;
|
|
100
|
+
/** Clock injection for the Rule-1 killswitch sunset check. */
|
|
101
|
+
now?: Date;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Detect a codex reviewer. Returns `{ available: true, framework, model }`
|
|
105
|
+
* iff ALL of: codex binary detected, OAuth `access_token` present, Rule-1
|
|
106
|
+
* clean (no raw API key in env or auth.json). Any miss → a specific reason.
|
|
107
|
+
*
|
|
108
|
+
* Pure-ish: all external inputs are injectable. With no inputs it probes the
|
|
109
|
+
* real host. It NEVER throws.
|
|
110
|
+
*/
|
|
111
|
+
export declare function detectCodexReviewer(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
|
|
112
|
+
export interface ReviewerResult {
|
|
113
|
+
/** Outcome class for the cross-model pass. */
|
|
114
|
+
status: 'ok' | 'degraded' | 'unavailable';
|
|
115
|
+
/** The framework that ran (or would have run). */
|
|
116
|
+
framework?: IntelligenceFramework;
|
|
117
|
+
/** Concrete model used. */
|
|
118
|
+
model?: string;
|
|
119
|
+
/** Parsed verdict, when the review returned. */
|
|
120
|
+
verdict?: ReviewVerdict;
|
|
121
|
+
/** Structured findings (one record), folded alongside internal reviewers. */
|
|
122
|
+
findings?: ReviewFinding[];
|
|
123
|
+
/** A reason string for degraded/unavailable outcomes. */
|
|
124
|
+
reason?: string;
|
|
125
|
+
/** The flag string that gets written to frontmatter + the report banner. */
|
|
126
|
+
flag: string;
|
|
127
|
+
}
|
|
128
|
+
export type ReviewVerdict = 'CLEAN' | 'MINOR ISSUES' | 'SERIOUS ISSUES' | 'UNKNOWN';
|
|
129
|
+
export interface ReviewFinding {
|
|
130
|
+
/** Reviewer tag, e.g. 'cross-model:codex-cli:gpt-5.5'. */
|
|
131
|
+
reviewer: string;
|
|
132
|
+
verdict: ReviewVerdict;
|
|
133
|
+
/** The findings body (verbatim text the reviewer produced). */
|
|
134
|
+
body: string;
|
|
135
|
+
/** True when the reply could not be parsed into a verdict (captured raw). */
|
|
136
|
+
unstructured?: boolean;
|
|
137
|
+
}
|
|
138
|
+
interface SupportedReviewerFramework {
|
|
139
|
+
/** Extend this union (in the type below) to add a framework. */
|
|
140
|
+
id: IntelligenceFramework;
|
|
141
|
+
/** Detection — does this framework's reviewer have what it needs to run? */
|
|
142
|
+
detect(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
|
|
143
|
+
/**
|
|
144
|
+
* Run the review: assemble already done by the caller; this builds the
|
|
145
|
+
* provider, evaluates the prompt, and parses the result. Returns a
|
|
146
|
+
* ReviewerResult (never throws — failures map to a degraded result).
|
|
147
|
+
*/
|
|
148
|
+
review(args: ReviewerInvokeArgs): Promise<ReviewerResult>;
|
|
149
|
+
}
|
|
150
|
+
export interface ReviewerInvokeArgs {
|
|
151
|
+
/** The fully-assembled cross-model reviewer prompt (prompt + spec + context). */
|
|
152
|
+
promptText: string;
|
|
153
|
+
/** Per-call timeout. */
|
|
154
|
+
timeoutMs: number;
|
|
155
|
+
/**
|
|
156
|
+
* Optional provider override — tests inject a stub so no real codex spawn
|
|
157
|
+
* happens. Production passes nothing and the factory builds the real one.
|
|
158
|
+
*/
|
|
159
|
+
providerOverride?: {
|
|
160
|
+
evaluate(prompt: string, options?: {
|
|
161
|
+
model?: 'fast' | 'balanced' | 'capable';
|
|
162
|
+
timeoutMs?: number;
|
|
163
|
+
}): Promise<string>;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 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).
|
|
169
|
+
*/
|
|
170
|
+
export declare const SUPPORTED_REVIEWER_FRAMEWORKS: SupportedReviewerFramework[];
|
|
171
|
+
/**
|
|
172
|
+
* 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`.
|
|
176
|
+
*
|
|
177
|
+
* SIGNAL-ONLY: never throws, never blocks. A `false` simply routes the skill
|
|
178
|
+
* to the internal-only fallback (spec §4).
|
|
179
|
+
*/
|
|
180
|
+
export declare function detectCrossModelReviewer(inputs?: CrossModelDetectInputs): CrossModelDetectionResult;
|
|
181
|
+
/**
|
|
182
|
+
* Map a provider rejection into a coarse `degraded` reason. The provider
|
|
183
|
+
* surfaces timeouts, non-zero exits, and (via the circuit breaker) rate
|
|
184
|
+
* limits as thrown Errors; we classify on the message text the same way the
|
|
185
|
+
* rate-limit classifier does.
|
|
186
|
+
*/
|
|
187
|
+
export declare function classifyReviewFailure(err: unknown): string;
|
|
188
|
+
/**
|
|
189
|
+
* Parse a reviewer reply into a structured finding. The prompt mandates a
|
|
190
|
+
* `Verdict: CLEAN | MINOR ISSUES | SERIOUS ISSUES` line + a findings list. If
|
|
191
|
+
* the verdict line is unparseable (or the reply is blank), the whole reply is
|
|
192
|
+
* captured as one raw "unstructured external review — read manually" finding
|
|
193
|
+
* (never dropped, never thrown, never zero).
|
|
194
|
+
*/
|
|
195
|
+
export declare function parseReviewerReply(raw: string, reviewerTag: string): ReviewFinding;
|
|
196
|
+
export interface ReferencedContextDoc {
|
|
197
|
+
/** Repo-relative path used as the `--- CONTEXT: <path> ---` header. */
|
|
198
|
+
path: string;
|
|
199
|
+
/** The doc's contents (already read by the caller). */
|
|
200
|
+
content: string;
|
|
201
|
+
}
|
|
202
|
+
export interface AssemblePromptInputs {
|
|
203
|
+
/** Contents of skills/spec-converge/templates/reviewer-cross-model.md. */
|
|
204
|
+
reviewerTemplate: string;
|
|
205
|
+
/** The full spec markdown. */
|
|
206
|
+
specMarkdown: string;
|
|
207
|
+
/** Repo-relative spec path, substituted for {SPEC_PATH} in the template. */
|
|
208
|
+
specPath: string;
|
|
209
|
+
/** Referenced architectural context docs (same set internal reviewers see). */
|
|
210
|
+
context?: ReferencedContextDoc[];
|
|
211
|
+
/** Total budget in bytes. Defaults to CONTEXT_BUDGET_BYTES. */
|
|
212
|
+
budgetBytes?: number;
|
|
213
|
+
}
|
|
214
|
+
export interface AssembledPrompt {
|
|
215
|
+
/** The final prompt string fed to the provider. */
|
|
216
|
+
promptText: string;
|
|
217
|
+
/** True when referenced context had to be truncated to fit the budget. */
|
|
218
|
+
truncated: boolean;
|
|
219
|
+
/** Byte size of the assembled prompt. */
|
|
220
|
+
bytes: number;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Assemble the cross-model reviewer prompt: the reviewer template (with
|
|
224
|
+
* `{SPEC_PATH}` substituted) + the full spec + as much referenced context as
|
|
225
|
+
* fits the budget. codex runs with NO repo access, so context is inlined under
|
|
226
|
+
* `--- CONTEXT: <path> ---` headers. The spec is ALWAYS included in full; if
|
|
227
|
+
* referenced docs overflow the budget, they are truncated and a loud
|
|
228
|
+
* truncation note is added so the reviewer knows its view was partial (a
|
|
229
|
+
* silently-truncated review is a trap; a disclosed-partial one is still signal).
|
|
230
|
+
*
|
|
231
|
+
* Truncation is DETERMINISTIC (spec §2, F4): referenced docs are ordered by
|
|
232
|
+
* `orderContextDeterministically` (constitutional/lessons docs first, then the
|
|
233
|
+
* spec-declared link order) BEFORE the budget walk, so the same spec + docs
|
|
234
|
+
* always drop the same docs. When a doc is fully or partially dropped, the
|
|
235
|
+
* truncation note NAMES the affected docs (which were partial, which were fully
|
|
236
|
+
* omitted) — a reviewer must know exactly which context it could not see, not
|
|
237
|
+
* just that "something" was cut.
|
|
238
|
+
*/
|
|
239
|
+
export declare function assembleReviewerPrompt(inputs: AssemblePromptInputs): AssembledPrompt;
|
|
240
|
+
/**
|
|
241
|
+
* The discrete cross-model review outcome states the report + frontmatter
|
|
242
|
+
* record. Distinct so "you have no cross-model reviewer" reads differently
|
|
243
|
+
* from "your reviewer was rate-limited this round" from "you chose the fast
|
|
244
|
+
* path" from "the framework was present but NOT ONE round ever succeeded".
|
|
245
|
+
*
|
|
246
|
+
* `degraded-all-rounds` (spec §2/§4, F2) is the SPEC-LEVEL aggregate: a single
|
|
247
|
+
* round's `degraded` lives on a ReviewerResult, but convergence runs many
|
|
248
|
+
* rounds and the spec gets ONE final `cross-model-review:` value. When a
|
|
249
|
+
* framework was present every round but ZERO rounds produced a successful
|
|
250
|
+
* external pass (all degraded), the final flag is `degraded-all-rounds` —
|
|
251
|
+
* treated as loud as `unavailable`, because the spec converged having never
|
|
252
|
+
* once received a real external opinion. This must surface at SPEC level, not
|
|
253
|
+
* hide in per-round notes.
|
|
254
|
+
*/
|
|
255
|
+
export type CrossModelFlagStatus = 'available' | 'unavailable' | 'degraded' | 'degraded-all-rounds' | 'skipped-abbreviated';
|
|
256
|
+
export interface CrossModelFlag {
|
|
257
|
+
status: CrossModelFlagStatus;
|
|
258
|
+
/** The `cross-model-review:` frontmatter value. */
|
|
259
|
+
flag: string;
|
|
260
|
+
/** Optional `cross-model-review-reason:` value. */
|
|
261
|
+
reason?: string;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Build the fallback flag for the unavailable / skipped / degraded-all-rounds
|
|
265
|
+
* states. (The available and per-round degraded flags come back on the
|
|
266
|
+
* ReviewerResult.) Centralizes the exact strings the frontmatter writer +
|
|
267
|
+
* report banner consume.
|
|
268
|
+
*
|
|
269
|
+
* `degraded-all-rounds` is the spec-level aggregate the skill writes when a
|
|
270
|
+
* framework was present but no round ever succeeded (see
|
|
271
|
+
* `aggregateRoundOutcomes`).
|
|
272
|
+
*/
|
|
273
|
+
export declare function buildCrossModelFlag(status: 'unavailable' | 'skipped-abbreviated' | 'degraded-all-rounds', reason?: string): CrossModelFlag;
|
|
274
|
+
/**
|
|
275
|
+
* Aggregate per-round cross-model outcomes into the ONE final spec-level flag
|
|
276
|
+
* (spec §2/§4, F2). Convergence runs multiple rounds; each round yields a
|
|
277
|
+
* `ReviewerResult` (`ok` / `degraded` / `unavailable`). The skill collects the
|
|
278
|
+
* per-round statuses and calls this to decide what `write-convergence-tag.mjs`
|
|
279
|
+
* stamps:
|
|
280
|
+
*
|
|
281
|
+
* - `skipped-abbreviated` if the author opted out (passed explicitly) — wins
|
|
282
|
+
* over everything, since no external pass was attempted by choice.
|
|
283
|
+
* - `codex-cli:<model>` (the LAST successful round's flag) if ANY round got a
|
|
284
|
+
* real external pass — one genuine outside opinion is enough to say the spec
|
|
285
|
+
* received cross-model review.
|
|
286
|
+
* - `degraded-all-rounds` if a framework was present every round but ZERO
|
|
287
|
+
* rounds succeeded (all degraded) — as loud as `unavailable`.
|
|
288
|
+
* - `unavailable` if no framework was ever available (all rounds unavailable).
|
|
289
|
+
*
|
|
290
|
+
* Returns the `{ flag, reason }` the tag writer + report banner consume.
|
|
291
|
+
*/
|
|
292
|
+
export declare function aggregateRoundOutcomes(rounds: ReviewerResult[], opts?: {
|
|
293
|
+
skippedAbbreviated?: boolean;
|
|
294
|
+
}): CrossModelFlag;
|
|
295
|
+
/**
|
|
296
|
+
* The high-level entry the skill driver calls: detect, and if available run
|
|
297
|
+
* the first available framework's reviewer with the assembled prompt;
|
|
298
|
+
* otherwise return the `unavailable` flag. NEVER throws, NEVER blocks.
|
|
299
|
+
*
|
|
300
|
+
* `assembled` is produced by `assembleReviewerPrompt`. `detectInputs` and
|
|
301
|
+
* `providerOverride` exist for tests; production omits them.
|
|
302
|
+
*/
|
|
303
|
+
export declare function runCrossModelReview(args: {
|
|
304
|
+
assembled: AssembledPrompt;
|
|
305
|
+
timeoutMs?: number;
|
|
306
|
+
detectInputs?: CrossModelDetectInputs;
|
|
307
|
+
providerOverride?: ReviewerInvokeArgs['providerOverride'];
|
|
308
|
+
}): Promise<ReviewerResult>;
|
|
309
|
+
export {};
|
|
310
|
+
//# sourceMappingURL=crossModelReviewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossModelReviewer.d.ts","sourceRoot":"","sources":["../../src/core/crossModelReviewer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crossModelReviewer — Step B of the tiered development process.
|
|
3
|
+
*
|
|
4
|
+
* Re-platforms the `/spec-converge` external "cross-model" reviewer onto the
|
|
5
|
+
* agent's own installed `codex` CLI. The external pass that the skill used to
|
|
6
|
+
* describe as running "via the /crossreview pattern" (a never-built
|
|
7
|
+
* placeholder) is now a grounded mechanism: detect whether a supported reviewer
|
|
8
|
+
* framework is installed + authed, assemble the cross-model reviewer prompt
|
|
9
|
+
* (spec + referenced context, bounded to a budget), and run it THROUGH the
|
|
10
|
+
* existing `CodexCliIntelligenceProvider` (the factory with
|
|
11
|
+
* `framework: 'codex-cli'`, model `capable` → GPT-tier).
|
|
12
|
+
*
|
|
13
|
+
* Design invariants (see docs/specs/codex-crossreview-stepB-spec.md):
|
|
14
|
+
* - Detection is a pure function with injectable inputs (no real spawns in
|
|
15
|
+
* unit tests). It is SIGNAL-ONLY — it never throws and never blocks.
|
|
16
|
+
* - Reviewer invocation reuses the provider (its scratch-dir clean-notepad,
|
|
17
|
+
* env allowlist, `--skip-git-repo-check`, and the account-global circuit
|
|
18
|
+
* breaker the factory wraps it in). The ONLY new spawn-adjacent code is
|
|
19
|
+
* prompt assembly + result parsing.
|
|
20
|
+
* - Every failure mode routes toward internal-only convergence or a captured
|
|
21
|
+
* raw finding — never a stall. `unavailable` (no framework) is distinct
|
|
22
|
+
* from `degraded` (framework present, this call failed) and
|
|
23
|
+
* `skipped-abbreviated` (author chose the fast path).
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { detectCodexPath } from './Config.js';
|
|
34
|
+
import { validateRule1 } from '../providers/adapters/openai-codex/credentials.js';
|
|
35
|
+
import { resolveCliModelFlag } from '../providers/adapters/openai-codex/models.js';
|
|
36
|
+
import { buildIntelligenceProvider, } from './intelligenceProviderFactory.js';
|
|
37
|
+
// ── Constants (tunable) ─────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Per-call timeout for a cross-model spec review. A reasoning review of a
|
|
40
|
+
* full spec is far heavier than the provider's 30s judgment-call default, so
|
|
41
|
+
* Step B bumps it to 120s (spec §2).
|
|
42
|
+
*/
|
|
43
|
+
export const REVIEW_TIMEOUT_MS = 120_000;
|
|
44
|
+
/**
|
|
45
|
+
* Total context budget (spec + referenced docs) inlined into the reviewer
|
|
46
|
+
* prompt. codex runs in an empty read-only scratch dir with no repo access,
|
|
47
|
+
* so referenced context MUST be inlined; this bounds the prompt size (spec §2).
|
|
48
|
+
* The spec is always included in full; referenced context fills the remainder
|
|
49
|
+
* and is truncated (with a loud note) if it overflows.
|
|
50
|
+
*/
|
|
51
|
+
export const CONTEXT_BUDGET_BYTES = 60 * 1024;
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic priority ordering for referenced context (spec §2, F4).
|
|
54
|
+
*
|
|
55
|
+
* When the 60KB budget can't hold every referenced doc, truncation MUST be
|
|
56
|
+
* deterministic — the same spec + same docs always drop the same docs — so a
|
|
57
|
+
* review is reproducible and the "what got dropped" note is stable. The
|
|
58
|
+
* constitutional / lessons docs are the highest-value context for a reviewer
|
|
59
|
+
* (they're what the lessons-aware internal reviewer reads), so they are kept
|
|
60
|
+
* FIRST; everything else keeps the spec-declared link order (the order the
|
|
61
|
+
* caller passed the docs in, which is the order they appear in the spec).
|
|
62
|
+
*
|
|
63
|
+
* A doc whose path contains one of these substrings sorts ahead of the rest,
|
|
64
|
+
* in THIS order. Ties (and all non-priority docs) preserve the caller's order
|
|
65
|
+
* via a stable sort.
|
|
66
|
+
*/
|
|
67
|
+
export const CONTEXT_PRIORITY_SUBSTRINGS = [
|
|
68
|
+
'signal-vs-authority',
|
|
69
|
+
'INSTAR-DESIGN-PRINCIPLES-AND-LESSONS',
|
|
70
|
+
'STANDARDS-REGISTRY',
|
|
71
|
+
'integrated-being',
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Return a deterministic priority rank for a context doc path: a small index
|
|
75
|
+
* for a constitutional/lessons doc (earlier substring = smaller rank), or a
|
|
76
|
+
* large sentinel for everything else (so non-priority docs keep their relative
|
|
77
|
+
* order behind the priority ones under a stable sort).
|
|
78
|
+
*/
|
|
79
|
+
function contextPriorityRank(docPath) {
|
|
80
|
+
const lower = docPath.toLowerCase();
|
|
81
|
+
for (let i = 0; i < CONTEXT_PRIORITY_SUBSTRINGS.length; i++) {
|
|
82
|
+
if (lower.includes(CONTEXT_PRIORITY_SUBSTRINGS[i].toLowerCase()))
|
|
83
|
+
return i;
|
|
84
|
+
}
|
|
85
|
+
return CONTEXT_PRIORITY_SUBSTRINGS.length;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Order referenced context deterministically: constitutional/lessons docs
|
|
89
|
+
* first (per CONTEXT_PRIORITY_SUBSTRINGS), then the caller's spec-declared link
|
|
90
|
+
* order for the rest. A stable sort on the priority rank achieves both — equal
|
|
91
|
+
* ranks keep their input order. Pure; never mutates the input.
|
|
92
|
+
*/
|
|
93
|
+
export function orderContextDeterministically(context) {
|
|
94
|
+
return context
|
|
95
|
+
.map((doc, idx) => ({ doc, idx, rank: contextPriorityRank(doc.path) }))
|
|
96
|
+
.sort((a, b) => a.rank - b.rank || a.idx - b.idx)
|
|
97
|
+
.map((e) => e.doc);
|
|
98
|
+
}
|
|
99
|
+
/** The canonical model tier a heavyweight cross-model review requests. */
|
|
100
|
+
const REVIEW_MODEL_TIER = 'capable';
|
|
101
|
+
/** Resolve the default codex auth.json path (CODEX_HOME-aware). */
|
|
102
|
+
function defaultAuthJsonPath(env) {
|
|
103
|
+
const home = env['CODEX_HOME'] || path.join(os.homedir(), '.codex');
|
|
104
|
+
return path.join(home, 'auth.json');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Is the codex auth.json an OAuth (`tokens.access_token`) shape? This is the
|
|
108
|
+
* subscription-OAuth shape D3 requires. A missing / unreadable / malformed
|
|
109
|
+
* file → false (not authed). Uses the same probe shape as the codex smoketest.
|
|
110
|
+
*/
|
|
111
|
+
function authHasOAuthAccessToken(authJsonPath) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = fs.readFileSync(authJsonPath, 'utf-8');
|
|
114
|
+
const parsed = JSON.parse(raw);
|
|
115
|
+
return typeof parsed?.tokens?.access_token === 'string' && parsed.tokens.access_token.length > 0;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// missing / unreadable / malformed → not authed.
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Detect a codex reviewer. Returns `{ available: true, framework, model }`
|
|
124
|
+
* iff ALL of: codex binary detected, OAuth `access_token` present, Rule-1
|
|
125
|
+
* clean (no raw API key in env or auth.json). Any miss → a specific reason.
|
|
126
|
+
*
|
|
127
|
+
* Pure-ish: all external inputs are injectable. With no inputs it probes the
|
|
128
|
+
* real host. It NEVER throws.
|
|
129
|
+
*/
|
|
130
|
+
export function detectCodexReviewer(inputs = {}) {
|
|
131
|
+
const env = inputs.env ?? process.env;
|
|
132
|
+
const now = inputs.now ?? new Date();
|
|
133
|
+
const codexPath = inputs.codexPathDetected !== undefined ? inputs.codexPathDetected : detectCodexPath();
|
|
134
|
+
const authJsonPath = inputs.authJsonPath ?? defaultAuthJsonPath(env);
|
|
135
|
+
// 1. Binary present?
|
|
136
|
+
if (!codexPath) {
|
|
137
|
+
return { available: false, reason: 'codex-not-installed' };
|
|
138
|
+
}
|
|
139
|
+
// 2. Rule-1 clean? (API-key forbidden — env OPENAI_API_KEY or auth.json
|
|
140
|
+
// API-key shape). Reuses existing policy rather than inventing one.
|
|
141
|
+
const rule1 = validateRule1(env, authJsonPath, now);
|
|
142
|
+
if (!rule1.ok) {
|
|
143
|
+
// The killswitch-expired / apikey-detected codes all collapse to the same
|
|
144
|
+
// policy outcome here: a forbidden credential shape → reviewer unavailable.
|
|
145
|
+
return { available: false, reason: 'codex-auth-apikey-forbidden' };
|
|
146
|
+
}
|
|
147
|
+
// 3. Authed via subscription OAuth?
|
|
148
|
+
if (!authHasOAuthAccessToken(authJsonPath)) {
|
|
149
|
+
return { available: false, reason: 'codex-not-authed' };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
available: true,
|
|
153
|
+
framework: 'codex-cli',
|
|
154
|
+
model: resolveCliModelFlag(REVIEW_MODEL_TIER),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The codex reviewer entry. Detection delegates to `detectCodexReviewer`;
|
|
159
|
+
* `review` routes through the factory-built `CodexCliIntelligenceProvider`.
|
|
160
|
+
*/
|
|
161
|
+
const codexReviewer = {
|
|
162
|
+
id: 'codex-cli',
|
|
163
|
+
detect: (inputs) => detectCodexReviewer(inputs),
|
|
164
|
+
review: async (args) => {
|
|
165
|
+
const detection = detectCodexReviewer();
|
|
166
|
+
const model = detection.model ?? resolveCliModelFlag(REVIEW_MODEL_TIER);
|
|
167
|
+
const tag = `cross-model:codex-cli:${model}`;
|
|
168
|
+
// Build (or accept an injected) provider. The factory wraps it in the
|
|
169
|
+
// account-global circuit breaker, so a rate-limited review degrades the
|
|
170
|
+
// same way every other instar LLM call does.
|
|
171
|
+
const provider = args.providerOverride ??
|
|
172
|
+
buildIntelligenceProvider({ framework: 'codex-cli' });
|
|
173
|
+
if (!provider) {
|
|
174
|
+
// Binary vanished between detect and review (or detection said
|
|
175
|
+
// unavailable and review was called anyway). Degraded, not a throw.
|
|
176
|
+
return {
|
|
177
|
+
status: 'degraded',
|
|
178
|
+
framework: 'codex-cli',
|
|
179
|
+
model,
|
|
180
|
+
reason: 'provider-unavailable',
|
|
181
|
+
flag: `cross-model-review: codex-cli:${model} (degraded: provider-unavailable)`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
let raw;
|
|
185
|
+
try {
|
|
186
|
+
raw = await provider.evaluate(args.promptText, {
|
|
187
|
+
model: REVIEW_MODEL_TIER,
|
|
188
|
+
timeoutMs: args.timeoutMs,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const reason = classifyReviewFailure(err);
|
|
193
|
+
return {
|
|
194
|
+
status: 'degraded',
|
|
195
|
+
framework: 'codex-cli',
|
|
196
|
+
model,
|
|
197
|
+
reason,
|
|
198
|
+
flag: `cross-model-review: codex-cli:${model} (degraded: ${reason})`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const parsed = parseReviewerReply(raw, tag);
|
|
202
|
+
return {
|
|
203
|
+
status: 'ok',
|
|
204
|
+
framework: 'codex-cli',
|
|
205
|
+
model,
|
|
206
|
+
verdict: parsed.verdict,
|
|
207
|
+
findings: [parsed],
|
|
208
|
+
flag: `cross-model-review: codex-cli:${model}`,
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* The supported-reviewer registry. codex first — the order IS the preference
|
|
214
|
+
* order. gemini-cli and others land here as later steps (Out of Scope).
|
|
215
|
+
*/
|
|
216
|
+
export const SUPPORTED_REVIEWER_FRAMEWORKS = [codexReviewer];
|
|
217
|
+
/**
|
|
218
|
+
* Walk the registry in preference order and return the FIRST available
|
|
219
|
+
* framework's detection result. If none is available, returns the codex
|
|
220
|
+
* reason when there's exactly one entry (so the report can be specific), else
|
|
221
|
+
* the generic `no-supported-framework`.
|
|
222
|
+
*
|
|
223
|
+
* SIGNAL-ONLY: never throws, never blocks. A `false` simply routes the skill
|
|
224
|
+
* to the internal-only fallback (spec §4).
|
|
225
|
+
*/
|
|
226
|
+
export function detectCrossModelReviewer(inputs = {}) {
|
|
227
|
+
for (const framework of SUPPORTED_REVIEWER_FRAMEWORKS) {
|
|
228
|
+
const result = framework.detect(inputs);
|
|
229
|
+
if (result.available)
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
// Nothing available. Surface the single framework's specific reason when
|
|
233
|
+
// there's only one entry (codex today); otherwise the generic reason.
|
|
234
|
+
if (SUPPORTED_REVIEWER_FRAMEWORKS.length === 1) {
|
|
235
|
+
return SUPPORTED_REVIEWER_FRAMEWORKS[0].detect(inputs);
|
|
236
|
+
}
|
|
237
|
+
return { available: false, reason: 'no-supported-framework' };
|
|
238
|
+
}
|
|
239
|
+
// ── Failure classification ──────────────────────────────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* Map a provider rejection into a coarse `degraded` reason. The provider
|
|
242
|
+
* surfaces timeouts, non-zero exits, and (via the circuit breaker) rate
|
|
243
|
+
* limits as thrown Errors; we classify on the message text the same way the
|
|
244
|
+
* rate-limit classifier does.
|
|
245
|
+
*/
|
|
246
|
+
export function classifyReviewFailure(err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
const lower = msg.toLowerCase();
|
|
249
|
+
if (/circuit breaker|rate.?limit|usage limit|quota|429|too many requests/.test(lower)) {
|
|
250
|
+
return 'rate-limited';
|
|
251
|
+
}
|
|
252
|
+
if (/timed out|timeout|etimedout|killed/.test(lower)) {
|
|
253
|
+
return 'timeout';
|
|
254
|
+
}
|
|
255
|
+
return 'error';
|
|
256
|
+
}
|
|
257
|
+
// ── Reviewer reply parsing ──────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Parse a reviewer reply into a structured finding. The prompt mandates a
|
|
260
|
+
* `Verdict: CLEAN | MINOR ISSUES | SERIOUS ISSUES` line + a findings list. If
|
|
261
|
+
* the verdict line is unparseable (or the reply is blank), the whole reply is
|
|
262
|
+
* captured as one raw "unstructured external review — read manually" finding
|
|
263
|
+
* (never dropped, never thrown, never zero).
|
|
264
|
+
*/
|
|
265
|
+
export function parseReviewerReply(raw, reviewerTag) {
|
|
266
|
+
const text = (raw ?? '').trim();
|
|
267
|
+
if (!text) {
|
|
268
|
+
return {
|
|
269
|
+
reviewer: reviewerTag,
|
|
270
|
+
verdict: 'UNKNOWN',
|
|
271
|
+
body: '(empty reviewer reply — codex returned no output; read manually)',
|
|
272
|
+
unstructured: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const verdict = extractVerdict(text);
|
|
276
|
+
if (verdict === 'UNKNOWN') {
|
|
277
|
+
return {
|
|
278
|
+
reviewer: reviewerTag,
|
|
279
|
+
verdict: 'UNKNOWN',
|
|
280
|
+
body: `unstructured external review — read manually:\n${text}`,
|
|
281
|
+
unstructured: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
reviewer: reviewerTag,
|
|
286
|
+
verdict,
|
|
287
|
+
body: text,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract the verdict from a reviewer reply. Looks for a `Verdict:` line and
|
|
292
|
+
* matches one of the three canonical values (case-insensitive, tolerant of
|
|
293
|
+
* surrounding markdown like `**Verdict: SERIOUS ISSUES**`). Returns 'UNKNOWN'
|
|
294
|
+
* when none is found.
|
|
295
|
+
*/
|
|
296
|
+
function extractVerdict(text) {
|
|
297
|
+
// Find a line mentioning "Verdict" and inspect its content.
|
|
298
|
+
const verdictLine = text
|
|
299
|
+
.split('\n')
|
|
300
|
+
.find((l) => /verdict/i.test(l));
|
|
301
|
+
const haystack = (verdictLine ?? text).toUpperCase();
|
|
302
|
+
// Order matters: check the most specific multi-word verdicts first.
|
|
303
|
+
if (haystack.includes('SERIOUS ISSUES'))
|
|
304
|
+
return 'SERIOUS ISSUES';
|
|
305
|
+
if (haystack.includes('MINOR ISSUES'))
|
|
306
|
+
return 'MINOR ISSUES';
|
|
307
|
+
if (haystack.includes('CLEAN'))
|
|
308
|
+
return 'CLEAN';
|
|
309
|
+
return 'UNKNOWN';
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Assemble the cross-model reviewer prompt: the reviewer template (with
|
|
313
|
+
* `{SPEC_PATH}` substituted) + the full spec + as much referenced context as
|
|
314
|
+
* fits the budget. codex runs with NO repo access, so context is inlined under
|
|
315
|
+
* `--- CONTEXT: <path> ---` headers. The spec is ALWAYS included in full; if
|
|
316
|
+
* referenced docs overflow the budget, they are truncated and a loud
|
|
317
|
+
* truncation note is added so the reviewer knows its view was partial (a
|
|
318
|
+
* silently-truncated review is a trap; a disclosed-partial one is still signal).
|
|
319
|
+
*
|
|
320
|
+
* Truncation is DETERMINISTIC (spec §2, F4): referenced docs are ordered by
|
|
321
|
+
* `orderContextDeterministically` (constitutional/lessons docs first, then the
|
|
322
|
+
* spec-declared link order) BEFORE the budget walk, so the same spec + docs
|
|
323
|
+
* always drop the same docs. When a doc is fully or partially dropped, the
|
|
324
|
+
* truncation note NAMES the affected docs (which were partial, which were fully
|
|
325
|
+
* omitted) — a reviewer must know exactly which context it could not see, not
|
|
326
|
+
* just that "something" was cut.
|
|
327
|
+
*/
|
|
328
|
+
export function assembleReviewerPrompt(inputs) {
|
|
329
|
+
const budget = inputs.budgetBytes ?? CONTEXT_BUDGET_BYTES;
|
|
330
|
+
const template = inputs.reviewerTemplate.replace(/\{SPEC_PATH\}/g, inputs.specPath);
|
|
331
|
+
const header = `${template}\n\n--- SPEC UNDER REVIEW: ${inputs.specPath} ---\n${inputs.specMarkdown}\n`;
|
|
332
|
+
const parts = [header];
|
|
333
|
+
let used = Buffer.byteLength(header, 'utf-8');
|
|
334
|
+
let truncated = false;
|
|
335
|
+
// Deterministic priority order: constitutional/lessons docs first, then the
|
|
336
|
+
// spec-declared link order. Same inputs always drop the same docs.
|
|
337
|
+
const context = orderContextDeterministically(inputs.context ?? []);
|
|
338
|
+
// Track exactly which docs were partially included vs fully dropped so the
|
|
339
|
+
// truncation note can NAME them (F4 — a named-partial review is signal; a
|
|
340
|
+
// "something was cut" review is a trap).
|
|
341
|
+
let partialDoc = null;
|
|
342
|
+
const droppedDocs = [];
|
|
343
|
+
for (let i = 0; i < context.length; i++) {
|
|
344
|
+
const doc = context[i];
|
|
345
|
+
const docHeader = `\n--- CONTEXT: ${doc.path} ---\n`;
|
|
346
|
+
const docBlock = `${docHeader}${doc.content}\n`;
|
|
347
|
+
const docBytes = Buffer.byteLength(docBlock, 'utf-8');
|
|
348
|
+
if (!truncated && used + docBytes <= budget) {
|
|
349
|
+
parts.push(docBlock);
|
|
350
|
+
used += docBytes;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
// Budget exceeded at this doc. Include as much of THIS doc as the remaining
|
|
354
|
+
// budget allows (header always; body sliced), record it as PARTIAL, then
|
|
355
|
+
// mark every remaining doc as fully DROPPED. We do NOT break — we keep
|
|
356
|
+
// walking so the note can name all the dropped docs, not just the first.
|
|
357
|
+
if (!truncated) {
|
|
358
|
+
const remaining = budget - used - Buffer.byteLength(docHeader, 'utf-8');
|
|
359
|
+
if (remaining > 0) {
|
|
360
|
+
// Slice by bytes safely (avoid splitting a multibyte char by slicing
|
|
361
|
+
// the buffer then decoding with replacement tolerated).
|
|
362
|
+
const sliced = Buffer.from(doc.content, 'utf-8').subarray(0, remaining).toString('utf-8');
|
|
363
|
+
parts.push(`${docHeader}${sliced}`);
|
|
364
|
+
partialDoc = doc.path;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Not even the header fits — this doc is fully dropped too.
|
|
368
|
+
droppedDocs.push(doc.path);
|
|
369
|
+
}
|
|
370
|
+
truncated = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// Already truncated — every subsequent doc is fully omitted.
|
|
374
|
+
droppedDocs.push(doc.path);
|
|
375
|
+
}
|
|
376
|
+
if (truncated) {
|
|
377
|
+
const detail = [];
|
|
378
|
+
if (partialDoc)
|
|
379
|
+
detail.push(`PARTIAL (cut mid-document): ${partialDoc}`);
|
|
380
|
+
if (droppedDocs.length > 0)
|
|
381
|
+
detail.push(`FULLY OMITTED: ${droppedDocs.join(', ')}`);
|
|
382
|
+
const named = detail.length > 0 ? ` ${detail.join('. ')}.` : '';
|
|
383
|
+
parts.push('\n\n--- NOTE: referenced context was TRUNCATED to fit the review budget.' +
|
|
384
|
+
named +
|
|
385
|
+
' Your view of the supporting docs is PARTIAL — flag any finding that ' +
|
|
386
|
+
'depends on context you could not see. ---\n');
|
|
387
|
+
}
|
|
388
|
+
const promptText = parts.join('');
|
|
389
|
+
return {
|
|
390
|
+
promptText,
|
|
391
|
+
truncated,
|
|
392
|
+
bytes: Buffer.byteLength(promptText, 'utf-8'),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Build the fallback flag for the unavailable / skipped / degraded-all-rounds
|
|
397
|
+
* states. (The available and per-round degraded flags come back on the
|
|
398
|
+
* ReviewerResult.) Centralizes the exact strings the frontmatter writer +
|
|
399
|
+
* report banner consume.
|
|
400
|
+
*
|
|
401
|
+
* `degraded-all-rounds` is the spec-level aggregate the skill writes when a
|
|
402
|
+
* framework was present but no round ever succeeded (see
|
|
403
|
+
* `aggregateRoundOutcomes`).
|
|
404
|
+
*/
|
|
405
|
+
export function buildCrossModelFlag(status, reason) {
|
|
406
|
+
if (status === 'unavailable') {
|
|
407
|
+
return { status, flag: 'cross-model-review: unavailable', reason };
|
|
408
|
+
}
|
|
409
|
+
if (status === 'degraded-all-rounds') {
|
|
410
|
+
return { status, flag: 'cross-model-review: degraded-all-rounds', reason };
|
|
411
|
+
}
|
|
412
|
+
return { status, flag: 'cross-model-review: skipped-abbreviated', reason };
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Aggregate per-round cross-model outcomes into the ONE final spec-level flag
|
|
416
|
+
* (spec §2/§4, F2). Convergence runs multiple rounds; each round yields a
|
|
417
|
+
* `ReviewerResult` (`ok` / `degraded` / `unavailable`). The skill collects the
|
|
418
|
+
* per-round statuses and calls this to decide what `write-convergence-tag.mjs`
|
|
419
|
+
* stamps:
|
|
420
|
+
*
|
|
421
|
+
* - `skipped-abbreviated` if the author opted out (passed explicitly) — wins
|
|
422
|
+
* over everything, since no external pass was attempted by choice.
|
|
423
|
+
* - `codex-cli:<model>` (the LAST successful round's flag) if ANY round got a
|
|
424
|
+
* real external pass — one genuine outside opinion is enough to say the spec
|
|
425
|
+
* received cross-model review.
|
|
426
|
+
* - `degraded-all-rounds` if a framework was present every round but ZERO
|
|
427
|
+
* rounds succeeded (all degraded) — as loud as `unavailable`.
|
|
428
|
+
* - `unavailable` if no framework was ever available (all rounds unavailable).
|
|
429
|
+
*
|
|
430
|
+
* Returns the `{ flag, reason }` the tag writer + report banner consume.
|
|
431
|
+
*/
|
|
432
|
+
export function aggregateRoundOutcomes(rounds, opts = {}) {
|
|
433
|
+
if (opts.skippedAbbreviated) {
|
|
434
|
+
return buildCrossModelFlag('skipped-abbreviated');
|
|
435
|
+
}
|
|
436
|
+
if (rounds.length === 0) {
|
|
437
|
+
// No rounds recorded at all — treat as no external reviewer available.
|
|
438
|
+
return buildCrossModelFlag('unavailable', 'no-rounds-recorded');
|
|
439
|
+
}
|
|
440
|
+
// Any successful round → the spec received a real external opinion. Use the
|
|
441
|
+
// LAST successful round's flag (the freshest pass on the most-converged spec).
|
|
442
|
+
const successful = rounds.filter((r) => r.status === 'ok');
|
|
443
|
+
if (successful.length > 0) {
|
|
444
|
+
const last = successful[successful.length - 1];
|
|
445
|
+
return { status: 'available', flag: last.flag, ...(last.reason ? { reason: last.reason } : {}) };
|
|
446
|
+
}
|
|
447
|
+
// No successes. Was a framework ever present? If ANY round degraded (vs
|
|
448
|
+
// unavailable), the framework was there but never delivered → all-rounds.
|
|
449
|
+
const anyDegraded = rounds.some((r) => r.status === 'degraded');
|
|
450
|
+
if (anyDegraded) {
|
|
451
|
+
// Surface the most recent degraded reason for the `-reason` field.
|
|
452
|
+
const lastDegraded = [...rounds].reverse().find((r) => r.status === 'degraded');
|
|
453
|
+
return buildCrossModelFlag('degraded-all-rounds', lastDegraded?.reason);
|
|
454
|
+
}
|
|
455
|
+
// Every round was unavailable (no framework, ever).
|
|
456
|
+
const lastUnavailable = [...rounds].reverse().find((r) => r.status === 'unavailable');
|
|
457
|
+
return buildCrossModelFlag('unavailable', lastUnavailable?.reason);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The high-level entry the skill driver calls: detect, and if available run
|
|
461
|
+
* the first available framework's reviewer with the assembled prompt;
|
|
462
|
+
* otherwise return the `unavailable` flag. NEVER throws, NEVER blocks.
|
|
463
|
+
*
|
|
464
|
+
* `assembled` is produced by `assembleReviewerPrompt`. `detectInputs` and
|
|
465
|
+
* `providerOverride` exist for tests; production omits them.
|
|
466
|
+
*/
|
|
467
|
+
export async function runCrossModelReview(args) {
|
|
468
|
+
const detection = detectCrossModelReviewer(args.detectInputs);
|
|
469
|
+
if (!detection.available) {
|
|
470
|
+
const flag = buildCrossModelFlag('unavailable', detection.reason);
|
|
471
|
+
return {
|
|
472
|
+
status: 'unavailable',
|
|
473
|
+
reason: detection.reason,
|
|
474
|
+
flag: flag.flag,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const framework = SUPPORTED_REVIEWER_FRAMEWORKS.find((f) => f.id === detection.framework);
|
|
478
|
+
if (!framework) {
|
|
479
|
+
// Defensive: detection named a framework with no registry entry.
|
|
480
|
+
const flag = buildCrossModelFlag('unavailable', 'no-supported-framework');
|
|
481
|
+
return { status: 'unavailable', reason: 'no-supported-framework', flag: flag.flag };
|
|
482
|
+
}
|
|
483
|
+
return framework.review({
|
|
484
|
+
promptText: args.assembled.promptText,
|
|
485
|
+
timeoutMs: args.timeoutMs ?? REVIEW_TIMEOUT_MS,
|
|
486
|
+
...(args.providerOverride ? { providerOverride: args.providerOverride } : {}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=crossModelReviewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossModelReviewer.js","sourceRoot":"","sources":["../../src/core/crossModelReviewer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-02T00:19:44.300Z",
|
|
5
|
+
"instarVersion": "1.3.192",
|
|
6
6
|
"entryCount": 193,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Spec-review-convergence's external **cross-model reviewer** is now real and framework-native:
|
|
9
|
+
instead of a placeholder API path, it detects the installed **codex CLI** and runs a
|
|
10
|
+
GPT-tier second opinion through the existing hardened codex provider — so any instar agent
|
|
11
|
+
with codex gets cross-model review for free, no API keys. When no supported reviewer is
|
|
12
|
+
installed (or a call fails, or convergence is abbreviated) the spec carries a loud,
|
|
13
|
+
honest `cross-model-review:` flag; the review is **signal, never a gate**. Step B of the
|
|
14
|
+
tiered-development project.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
Nothing to do — this only affects how the agent reviews its own instar specs. Specs now get
|
|
19
|
+
a real outside-model opinion when codex is present, and a clearly-marked note when they
|
|
20
|
+
don't, so a spec that never got an external review can't quietly look like it did.
|
|
21
|
+
|
|
22
|
+
## Summary of New Capabilities
|
|
23
|
+
|
|
24
|
+
- `src/core/crossModelReviewer.ts` — codex-CLI reviewer detection + a supported-reviewer
|
|
25
|
+
registry + the reviewer driver (via the existing codex provider) + the honest flag
|
|
26
|
+
machinery (`codex-cli:<model>` / `degraded` / `degraded-all-rounds` / `unavailable` /
|
|
27
|
+
`skipped-abbreviated`), with deterministic, named context truncation.
|
|
28
|
+
- `skills/spec-converge/scripts/cross-model-review.mjs` — the CLI the convergence skill
|
|
29
|
+
calls; `write-convergence-tag.mjs` gains `--cross-model-review`; `SKILL.md` + the reviewer
|
|
30
|
+
prompt template now describe the real, inlined-context, single-codex-pass mechanism.
|
|
31
|
+
|
|
32
|
+
## Evidence
|
|
33
|
+
|
|
34
|
+
- `npx tsc --noEmit` exit 0; Step-B tests 53 green (45 + 5 unit, 3 integration); `npm run
|
|
35
|
+
lint` green. Detection live-validated on this host: `{"available":true,
|
|
36
|
+
"framework":"codex-cli","model":"gpt-5.5"}`.
|
|
37
|
+
- Dev-only tooling: `/spec-converge` is not shipped to end-agent homes, so no migration.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-model review on the codex CLI (Step B)
|
|
2
|
+
|
|
3
|
+
**Slug:** `codex-crossreview-stepB`
|
|
4
|
+
**Date:** 2026-06-01
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Spec:** `docs/specs/codex-crossreview-stepB-spec.md` (approved by Justin + abbreviated convergence, 5 findings folded)
|
|
7
|
+
**Project:** Step B of the Tiered Development Process (`docs/projects/tiered-dev-process/PROJECT.md`)
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Re-platforms the external **cross-model reviewer** in spec-review-convergence from the
|
|
12
|
+
(placeholder) `/crossreview` API path onto the **installed codex CLI**, so every instar
|
|
13
|
+
agent with codex gets a GPT-tier second opinion for free, no API keys. Honors the
|
|
14
|
+
constitution's **Signal vs. Authority** — the external review is *signal*, never a
|
|
15
|
+
blocking authority; an absent/failed reviewer is disclosed loudly, never faked.
|
|
16
|
+
|
|
17
|
+
**Files changed (in-scope):**
|
|
18
|
+
- `src/core/crossModelReviewer.ts` (NEW) — detection (`detectCodexReviewer` /
|
|
19
|
+
`detectCrossModelReviewer`: codex binary present AND OAuth `tokens.access_token` AND
|
|
20
|
+
Rule-1 clean), the `SUPPORTED_REVIEWER_FRAMEWORKS` registry (codex first; one entry per
|
|
21
|
+
future framework), the reviewer driver (routes the cross-model prompt through the
|
|
22
|
+
existing `buildIntelligenceProvider({framework:'codex-cli'})` → gpt-5.5, 120s, reusing
|
|
23
|
+
the provider's env allowlist + the factory circuit breaker), `assembleReviewerPrompt`
|
|
24
|
+
(inlines the spec in full + referenced context under a 60KB budget with **deterministic**
|
|
25
|
+
priority order — constitutional docs first — and a truncation note that **names** the
|
|
26
|
+
partial + omitted docs), `parseReviewerReply` (never throws/zeroes), and the flag
|
|
27
|
+
machinery: `buildCrossModelFlag` + `aggregateRoundOutcomes` producing one of
|
|
28
|
+
`codex-cli:<model>` (ran) / `degraded` / `degraded-all-rounds` / `unavailable` /
|
|
29
|
+
`skipped-abbreviated`. Never throws, never blocks.
|
|
30
|
+
- `skills/spec-converge/scripts/cross-model-review.mjs` (NEW) — the thin CLI the skill
|
|
31
|
+
shells out to (does only repo file-I/O — codex is sandboxed with no repo access —
|
|
32
|
+
delegating logic to compiled `dist/core/crossModelReviewer.js`; always exits 0).
|
|
33
|
+
- `skills/spec-converge/scripts/write-convergence-tag.mjs` — `--cross-model-review` /
|
|
34
|
+
`--cross-model-reason` args writing the `cross-model-review:` frontmatter field
|
|
35
|
+
(idempotent, YAML-safe quoting).
|
|
36
|
+
- `skills/spec-converge/SKILL.md` — replaced the `/crossreview` placeholder with the real
|
|
37
|
+
invocation; Phase 3 documents per-round aggregation (→ `degraded-all-rounds` when a
|
|
38
|
+
framework was present but **zero** rounds got a successful pass); Phase 4 renders a loud
|
|
39
|
+
`⚠` banner for **every** non-ran state (a real pass is the only unmarked one); Phase 5
|
|
40
|
+
the valid flag values. Internal reviewers + the Standards-Conformance Gate unchanged.
|
|
41
|
+
|
|
42
|
+
**Files changed (not in-scope):** `skills/spec-converge/templates/reviewer-cross-model.md`
|
|
43
|
+
(patched: spec + context are "inlined below" — no file-read instruction codex can't
|
|
44
|
+
follow — and dropped the three-phantom-model GPT/Gemini/Grok framing → one codex pass);
|
|
45
|
+
the spec + ELI16; tests (`tests/unit/crossModelReviewer.test.ts` 45,
|
|
46
|
+
`tests/unit/write-convergence-tag-crossmodel.test.ts` 5,
|
|
47
|
+
`tests/integration/cross-model-review-flow.test.ts` 3).
|
|
48
|
+
|
|
49
|
+
## Blast radius
|
|
50
|
+
|
|
51
|
+
Additive. The new module is pure/injectable except the one provider call (reusing the
|
|
52
|
+
hardened codex provider). The convergence flow gains a real external reviewer where it had
|
|
53
|
+
a placeholder; the internal reviewers, the lessons-aware (non-skippable) reviewer, and the
|
|
54
|
+
Standards-Conformance gate are untouched. The reviewer never throws and never blocks — the
|
|
55
|
+
worst case is a loudly-disclosed `unavailable` / `degraded` / `degraded-all-rounds` flag.
|
|
56
|
+
The `cross-model-review:` flag is **disclosure, not a gate**: it does NOT touch the
|
|
57
|
+
`review-convergence` + `approved` enforcement in `instar-dev-precommit.js`.
|
|
58
|
+
|
|
59
|
+
## Risks considered
|
|
60
|
+
|
|
61
|
+
- **Could a failed/absent review read as a clean pass?** No (convergence Finding 1/2): every
|
|
62
|
+
non-ran state carries a loud `⚠` banner, and a convergence where a framework was present
|
|
63
|
+
but *no* round ever succeeded collapses to a spec-level `degraded-all-rounds` (as loud as
|
|
64
|
+
`unavailable`) — "never actually got an external opinion" surfaces at spec level.
|
|
65
|
+
- **Silent partial context?** No (Finding 4): truncation is deterministic (constitutional
|
|
66
|
+
docs first) and the note names the partial + fully-omitted docs.
|
|
67
|
+
- **API-key leak through codex?** No — reuses `buildCodexChildEnv()`'s allowlist; Rule-1
|
|
68
|
+
(`validateRule1`) forbids an API-key-shaped credential; detection requires OAuth.
|
|
69
|
+
- **Cost?** Worst case ~10 capable-tier calls per spec (the 10-round convergence cap × 1
|
|
70
|
+
call/round), bounded by the account circuit breaker. Stated in the spec.
|
|
71
|
+
|
|
72
|
+
## Migration parity
|
|
73
|
+
|
|
74
|
+
**None needed — verified.** `/spec-converge` is dev-only tooling: it is NOT in
|
|
75
|
+
`package.json` `files[]` and NOT installed by `installBuiltinSkills()`, so it never lands
|
|
76
|
+
in an end-agent home. No `PostUpdateMigrator` change. (The agents that *develop* instar get
|
|
77
|
+
it from the repo.)
|
|
78
|
+
|
|
79
|
+
## Tests / lint
|
|
80
|
+
|
|
81
|
+
`npx tsc --noEmit` exit 0; Step-B tests 53 green (45+5 unit, 3 integration); `npm run lint`
|
|
82
|
+
(tsc + destructive/LLM-http/url-log/codex-drift) green. (The worktree's ~28 pre-existing
|
|
83
|
+
local failures — node v25.6.1 + real-config + tunnel — are independent and CI-green.)
|