@@ -17,147 +17,27 @@ import os from 'node:os'
1717import path from 'node:path'
1818import process from 'node:process'
1919
20- import { which } from '@socketsecurity/lib/bin/which'
20+ import {
21+ BACKENDS ,
22+ detectAvailableBackends ,
23+ isBackendName ,
24+ resolveBackendForRole ,
25+ } from '@socketsecurity/lib/ai/backends'
2126import { safeDelete } from '@socketsecurity/lib/fs/safe'
2227import { getDefaultLogger } from '@socketsecurity/lib/logger/default'
2328import { isSpawnError } from '@socketsecurity/lib/process/spawn/errors'
2429import { spawn } from '@socketsecurity/lib/process/spawn/child'
2530
31+ import type { BackendName } from '@socketsecurity/lib/ai/backends'
32+
2633const logger = getDefaultLogger ( )
2734
2835type Role = 'discovery' | 'discovery-secondary' | 'remediation' | 'verify'
2936
30- type BackendName = 'codex' | 'claude' | 'opencode' | 'kimi'
31-
32- type BackendDescriptor = {
33- readonly bin : string
34- readonly hybrid : boolean
35- readonly name : BackendName
36- // Build the CLI argv given a prompt-file path and the temp output
37- // path the runner will read after the process exits. Backends that
38- // emit to stdout instead of an output file return outMode: 'stdout'
39- // so the runner captures stdout into the output path itself.
40- readonly run : (
41- promptFile : string ,
42- outFile : string ,
43- ) => { argv : readonly string [ ] ; outMode : 'file' | 'stdout' }
44- }
45-
46- const BACKENDS : Readonly < Record < BackendName , BackendDescriptor > > = {
47- __proto__ : null ,
48- codex : {
49- bin : 'codex' ,
50- hybrid : false ,
51- name : 'codex' ,
52- run ( promptFile , outFile ) {
53- const model = process . env [ 'CODEX_MODEL' ] ?? 'gpt-5.4'
54- const reasoning = process . env [ 'CODEX_REASONING' ] ?? 'xhigh'
55- return {
56- argv : [
57- 'exec' ,
58- '--model' ,
59- model ,
60- '-c' ,
61- `model_reasoning_effort=${ reasoning } ` ,
62- '--full-auto' ,
63- '--ephemeral' ,
64- '-o' ,
65- outFile ,
66- '-' ,
67- ] ,
68- outMode : 'file' ,
69- }
70- } ,
71- } ,
72- claude : {
73- bin : 'claude' ,
74- hybrid : false ,
75- name : 'claude' ,
76- run ( _promptFile , _outFile ) {
77- const model = process . env [ 'CLAUDE_MODEL' ] ?? 'opus'
78- // Pair the model with a reasoning effort (claude `--effort`) — see
79- // _shared/multi-agent-backends.md. Review is judgment-heavy, so the
80- // default is `high`; codex's sibling knob is CODEX_REASONING.
81- const effort = process . env [ 'CLAUDE_EFFORT' ] ?? 'high'
82- // Programmatic-Claude lockdown — all four flags per CLAUDE.md
83- // (tools / allowedTools / disallowedTools / permission-mode).
84- // The official permission flow is hooks → deny → mode → allow →
85- // canUseTool; in dontAsk mode the last step is skipped, so any
86- // tool not listed in `tools` is invisible to the model and any
87- // tool in `disallowedTools` is denied even on bypass. Verify
88- // pass is read-only by design: tools is the same set as
89- // allowedTools (read + git introspection only), with Edit /
90- // Write / destructive Bash explicitly denied.
91- return {
92- argv : [
93- '--print' ,
94- '--model' ,
95- model ,
96- '--effort' ,
97- effort ,
98- '--no-session-persistence' ,
99- '--permission-mode' ,
100- 'dontAsk' ,
101- '--tools' ,
102- 'Read' ,
103- 'Glob' ,
104- 'Grep' ,
105- 'Bash(git:*)' ,
106- '--allowedTools' ,
107- 'Read' ,
108- 'Glob' ,
109- 'Grep' ,
110- 'Bash(git:*)' ,
111- '--disallowedTools' ,
112- 'Edit' ,
113- 'Write' ,
114- 'Bash(rm:*)' ,
115- 'Bash(mv:*)' ,
116- ] ,
117- outMode : 'stdout' ,
118- }
119- } ,
120- } ,
121- opencode : {
122- bin : 'opencode' ,
123- hybrid : true ,
124- name : 'opencode' ,
125- run ( _promptFile , _outFile ) {
126- // opencode reads the prompt from stdin and writes to stdout in its
127- // non-interactive `run` form. It is hybrid — it dispatches to whatever
128- // provider its own config selects — so by default model selection lives
129- // outside this runner (opencode's config / its `recent` model).
130- //
131- // `OPENCODE_MODEL` lets a caller pin a `provider/model` slug for this run
132- // — the way the Fireworks + Synthetic providers are reached (e.g.
133- // `fireworks-ai/accounts/fireworks/models/glm-5p1`,
134- // `synthetic/hf:moonshotai/Kimi-K2.5`); see
135- // _shared/multi-agent-backends.md for the provider-slug catalog. Absent
136- // the env, opencode picks per its own config.
137- const model = process . env [ 'OPENCODE_MODEL' ]
138- const argv = model ? [ 'run' , '--model' , model ] : [ 'run' ]
139- return {
140- argv,
141- outMode : 'stdout' ,
142- }
143- } ,
144- } ,
145- kimi : {
146- bin : 'kimi' ,
147- hybrid : false ,
148- name : 'kimi' ,
149- run ( _promptFile , _outFile ) {
150- const model = process . env [ 'KIMI_MODEL' ] ?? 'kimi-latest'
151- // Tentative shape: kimi reads prompt from stdin, writes to stdout.
152- // Adjust when the actual CLI surface is known.
153- return {
154- argv : [ 'chat' , '--model' , model , '--no-stream' ] ,
155- outMode : 'stdout' ,
156- }
157- } ,
158- } ,
159- } as const
160-
37+ // The CLI registry, detection, and role resolution live in
38+ // @socketsecurity /lib/ai/backends — shared across the fleet's multi-agent
39+ // skills. This skill keeps only its own role table (prompts + per-role
40+ // preference order + timeouts) below and passes the order into the lib.
16141type RoleSpec = {
16242 readonly buildPrompt : ( ctx : ReviewContext ) => string
16343 readonly headingForVerify ?: string | undefined
@@ -372,22 +252,6 @@ export function capitalize(s: string): string {
372252 return s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 )
373253}
374254
375- export async function detectAvailableBackends ( ) : Promise <
376- ReadonlySet < BackendName >
377- > {
378- // Fan out the `which` lookups instead of awaiting one at a time.
379- // Cheap parallelism — N filesystem stats run concurrently rather
380- // than serially.
381- const names = Object . keys ( BACKENDS ) as BackendName [ ]
382- const results = await Promise . all (
383- names . map ( async name => ( {
384- name,
385- available : await isCommandAvailable ( BACKENDS [ name ] . bin ) ,
386- } ) ) ,
387- )
388- return new Set ( results . filter ( r => r . available ) . map ( r => r . name ) )
389- }
390-
391255export async function git (
392256 args : readonly string [ ] ,
393257 cwd ?: string ,
@@ -400,18 +264,6 @@ export async function git(
400264 return String ( result . stdout ?? '' ) . trim ( )
401265}
402266
403- export function isBackendName ( s : string ) : s is BackendName {
404- return s in BACKENDS
405- }
406-
407- export async function isCommandAvailable ( bin : string ) : Promise < boolean > {
408- // Use `which` from @socketsecurity/lib/bin instead of spawning
409- // `command -v` with shell: true. The shell:true variant invokes
410- // cmd.exe on Windows and mangles `command -v`; `which` is
411- // cross-platform and avoids the shell entirely.
412- return ( await which ( bin ) ) !== null
413- }
414-
415267export function isRole ( s : string ) : s is Role {
416268 return s in ROLES
417269}
@@ -512,25 +364,20 @@ export function pickBackend(
512364 available : ReadonlySet < BackendName > ,
513365 override : BackendName | undefined ,
514366) : BackendName | undefined {
515- if ( override ) {
516- if ( ! available . has ( override ) ) {
517- logger . warn (
518- `${ role } : requested backend "${ override } " is not installed; falling back to preference order` ,
519- )
520- } else {
521- return override
522- }
523- }
524- for ( const candidate of ROLES [ role ] . preferenceOrder ) {
525- // opencode is hybrid — only used when explicitly selected via --pass.
526- if ( BACKENDS [ candidate ] . hybrid ) {
527- continue
528- }
529- if ( available . has ( candidate ) ) {
530- return candidate
531- }
367+ // Routing policy (override → preference → skip; hybrid only via override)
368+ // lives in the shared lib; this wrapper supplies the role's preference order
369+ // and logs the one human-facing case the pure resolver reports structurally.
370+ const resolution = resolveBackendForRole ( {
371+ available,
372+ override,
373+ preferenceOrder : ROLES [ role ] . preferenceOrder ,
374+ } )
375+ if ( resolution . reason === 'preference' && 'overrideMissing' in resolution ) {
376+ logger . warn (
377+ `${ role } : requested backend "${ resolution . overrideMissing } " is not installed; falling back to preference order` ,
378+ )
532379 }
533- return undefined
380+ return resolution . backend
534381}
535382
536383export function printHelp ( ) : void {
0 commit comments