SLOPSHOPPER

jevshift

Experimental Jev-assisted model and effort selection for Claude Code: observe, pin, off, and opt-in auto.

newcommandstatuspromptprocesstimer
★ 1v0.1.0-alpha.4MITupdated 2026-09-23unclecode/jevshift
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · jevshift
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /jevshift ⎿ jevshift: JevShift: observe ⎿ jevshift: Native default: claude-opus-5-5 ⎿ jevshift: Native effort: default ⎿ jevshift: Effort control: auto (cap: max) ⎿ jevshift: Resolved models: not discovered ⎿ jevshift: Last Jev result: none ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ jevshift: JevShift · observe · effort auto
README

JevShift

I've been fascinated by Jev and the idea of small, focused models making decisions inside software. I've been interested in this approach for the last couple of years, and I'm already using these ideas in several parts of my workflow.

One thing I need every day is intelligent model switching. When I work in Claude Code, I don't want to use Fable, Opus or Sonnet for everything. I want the model to fit the work, with the option to take control whenever I need to. That's why I built JevShift, and why I'm sharing it.

How it works

JevShift asks Jev to choose a model and thinking effort using your request, recent conversation, plan and tool results. It can change both inside the same Claude Code conversation.

WorkPreferred model
Substantial planning and designFable
Implementing an agreed plan and debuggingOpus
Small edits, explanations and documentationSonnet

Effort is chosen separately: low for routine work, medium for bounded problems, high for substantial reasoning, xhigh for difficult interactions, and max for exceptional analysis. An agreed plan can still need high effort to implement.

These are preferences, not guaranteed choices. New sessions start in observe mode: you see recommendations while Claude keeps its current model and effort. Turn on auto when you're ready, or pin either choice yourself.

Model versions follow Claude Code's sonnet, opus and fable aliases. Keep Claude Code updated; JevShift resolves them at startup. Use /jevshift refresh to check again in an existing session. An exact model-ID pin stays on that version.

Experimental alpha. Tested on macOS with Claude Code 2.1.280. Requires curl 8.4+, your Claude login and model access. Jev recommendations use a separate OpenRouter API key. No build or npm install is needed.

Quick install

Choose either method.

Inside Claude Code

Paste this into a Claude Code terminal session:

/plugin install jevshift --marketplace unclecode/jevshift

Confirm the source and choose User to make the plugin available across your projects. Claude Code may initially select Project scope.

From your terminal

claude plugin marketplace add unclecode/jevshift
claude plugin install jevshift@jevshift --scope user
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --effort low

Connect Jev

Inside Claude Code, open the plugin settings:

/plugin configure jevshift@jevshift

Enter your OpenRouter API key in the masked field. To try automatic switching, turn on Enable experimental auto control. Save, exit Claude, then start it with:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --effort low

Use that launch command for JevShift sessions. The restart loads your settings and enables the experimental hooks this alpha needs. --effort low sets the starting native effort; auto can choose any supported level. Manual pins and off work without a Jev key.

Try it

First, discover your available models:

/jevshift setup

Send a task, then check what Jev recommended and which model answered:

/jevshift status

To let JevShift apply its recommendations:

/jevshift auto

Try planning a small feature, implementing the agreed plan, then updating its documentation. Use status after each phase to see the returned model and requested effort.

Commands

CommandWhat it does
/jevshift statusShow recommendations, timing, requested effort and requested/returned model.
/jevshift setupDiscover models and return to observe mode.
/jevshift refreshRefresh model versions while preserving your mode and pins.
/jevshift observeRecommend model and effort without applying them.
/jevshift autoChoose both automatically when auto is enabled in settings.
/jevshift pin sonnetHold the Sonnet family with native effort; its version follows discovery. No Jev calls. Also accepts opus, fable or an exact discovered model ID.
/jevshift pin opus highHold both model and effort. No Jev calls.
/jevshift effort mediumFix effort while keeping the current model policy. In observe, this stays advisory.
/jevshift effort autoLet Jev choose effort, including with a pinned model. Requires auto enabled to apply changes.
/jevshift effort nativeKeep Claude's native effort while retaining the model policy.
/jevshift offReturn control to Claude Code.

Changes apply to the next model request. Each session keeps its own controls. Changing the native model or effort turns JevShift off; /jevshift off always releases both. See independent pin examples.

Privacy and limits

Observe, auto and a pinned model with automatic effort send short conversation, plan and tool excerpts to OpenRouter / TypeSafe Jev. Known secret patterns are masked, but private content can still be included. Fully manual pins and off make no Jev calls. See what gets sent and a complete synthetic request.

This is an early preview. Evaluator calls can add delay or time out, and choices still need broader testing. A failed evaluation keeps native values for automatic choices and preserves valid manual pins. Effort is a reasoning preference, not a token budget. We have tested switching and controls, but have not established better task outcomes, cache preservation or quota savings. See validation and compatibility.

Demo

A screen-recorded walkthrough is coming soon.

More details

FolderContents
.claude-plugin/Plugin metadata and marketplace listing.
hooks/Claude Code integration.
src/Context collection, Jev calls, model selection and controls.
tests/Offline tests.
examples/Synthetic request and sample settings.
docs/Setup guides, compatibility and test results.

About me

I'm UncleCode. Follow me for updates and more tools from my workflow.

I also build Crawl4AI, an open-source web crawler for AI applications with 84K GitHub stars. If you work with web data, take a look.

MIT license. An independent project, not affiliated with Anthropic, TypeSafe or OpenRouter.

Source 16 files
hooks/register.ts 4 lines
1import type {Register} from 'claude-code';
2import {registerControlHooks} from '../src/controls-hooks.ts';
3export const register:Register=(on,options)=>registerControlHooks(on,options);
4
src/controls-hooks.ts 53 lines
1import type {Register} from 'claude-code';
2import {SessionControls} from './controls.ts';
3import {registerContextHooks} from './context-hooks.ts';
4import {discoverCatalog} from './discovery.ts';
5import type {Catalog,RoutingEvent} from './auto.ts';
6import type {Observation} from './observe.ts';
7import {isEffort} from './effort.ts';
8
9export function registerControlHooks(on:Parameters<Register>[0],options:Parameters<Register>[1],
10  record:(event:Observation|RoutingEvent)=>void=()=>{}):void {
11  let diagnostic:(event:Observation|RoutingEvent)=>void=()=>{};
12  const controls=new SessionControls(options.experimental_auto===true,event=>{record(event);diagnostic(event)},isEffort(options.max_effort)?options.max_effort:'max');
13  let discover:(()=>Promise<Catalog>)|undefined;
14  // No prompt is submitted. Safe mode prevents this helper loading this plugin again.
15  on('session.start',async($,e,next)=>{
16    discover=()=>discoverCatalog((argv,init)=>$.process.run(argv,init),
17      typeof options.claude_executable==='string'&&options.claude_executable.trim()?options.claude_executable:'claude');
18    await controls.bind(await $.session.id(),'startup',{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value),discover:()=>discover!()});
19    await controls.warmCatalog();
20    await $.command.register({name:'jevshift',description:'Control Jev model and effort selection for this session',argumentHint:'status|setup|refresh|observe|auto|off|pin <model> [effort]|effort <level|auto|native>',immediate:true});
21    return next(e);
22  });
23  on('command.run',async($,e,next)=>{
24    if(e.command==='effort'){
25      if(isEffort(e.args.trim())||['auto','ultracode'].includes(e.args.trim()))await controls.nativeEffortCommand();
26      return next(e);
27    }
28    if(e.command!=='jevshift')return next(e);
29    await controls.bind(await $.session.id(),'command',{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value),discover:()=>discover!()});
30    const text=await controls.command(e.args,await $.session.model());
31    $.ui.status(`JevShift · ${controls.mode}${controls.pin?` ${controls.pin}`:''} · effort ${controls.effort}${controls.blocked?' (blocked)':''}`);
32    return {text};
33  });
34  registerContextHooks(on,async(snapshot,id,host)=>{
35    diagnostic=event=>host.ui.log(JSON.stringify(event),{to:'debug'});
36    return controls.select(snapshot,id,{
37      now:()=>host.clock.now(),after:(ms,fn)=>host.clock.after(ms,fn),fetch:(url,init)=>host.http.fetch(url,init),
38      key:async()=>typeof options.openrouter_api_key==='string'&&options.openrouter_api_key.trim()
39        ?options.openrouter_api_key:options.use_environment_key===true?await host.environmentKey():undefined,
40      show:text=>{host.ui.status(text);host.ui.log(text)},
41    });
42  },()=>controls.invalidate(),{
43    lifecycle:async(id,source,store)=>{await controls.bind(id,source,{...store,discover:()=>discover!()})},
44    guard:id=>controls.guard(id),
45    finalize:(selection,id)=>controls.finalize(selection||undefined,id),
46  });
47  on('classic.PostModelSwitch',async($,e,next)=>{
48    await controls.nativeSwitch(e.source,e.to_model);
49    $.ui.status(`JevShift · ${controls.mode}${controls.blocked?' (blocked)':''}`);
50    return next(e);
51  });
52}
53
src/controls.ts 250 lines
1import {AutoSession,type Catalog,type RoutingEvent} from './auto.ts';
2import {ObserveSession,type ObserveHost,type Observation} from './observe.ts';
3import {compatible,resolveTarget,modelId,isTier,TIERS} from './catalog.ts';
4import type {Boundary,ModelSelection} from './context-hooks.ts';
5import type {ContextSnapshot} from './context.ts';
6import {isEffort,withinCap,type Effort,type EffortPolicy} from './effort.ts';
7export type Mode='observe'|'auto'|'off'|'pin';
8export type SavedControl={version:1;sessionId:string;mode:Mode;pin?:string;effort?:EffortPolicy;blocked?:boolean};
9export type ControlHost={get:(key:string)=>Promise<unknown>;set:(key:string,value:unknown)=>Promise<void>;discover:()=>Promise<Catalog>};
10const key=(id:string)=>`session-v1:${id}`;
11const help='Use /jevshift status, setup, refresh, observe, auto, off, pin <model> [effort], or effort <low|medium|high|xhigh|max|auto|native>.';
12export class SessionControls {
13  mode:Mode='observe';pin:string|undefined;blocked=false;catalog:Catalog={};sessionId='';
14  nativeModel='unknown';recommended:string|undefined;requested:string|undefined;actual:string|undefined;
15  effort:EffortPolicy='auto';nativeEffort:Boundary['effort'];recommendedEffort:Effort|undefined;requestedEffort:Boundary['effort'];
16  private nativeEffortSeen=false;
17  note='';private revision=0;private loaded=false;private loading:Promise<void>|undefined;
18  private catalogPending:Promise<Catalog>|undefined;private saveQueue:Promise<void>=Promise.resolve();
19  private catalogEpoch=0;
20  private host:ControlHost|undefined;private auto:AutoSession|undefined;
21  private observer:ObserveSession;
22  private lastEvaluation:Observation|undefined;
23  constructor(private allowAuto=false,private record:(event:Observation|RoutingEvent)=>void=()=>{},private maxEffort:Effort='max') {
24    this.observer=new ObserveSession(e=>{
25      if(e.event==='finished'&&e.sessionId===this.sessionId&&e.outcome!=='stale'&&e.outcome!=='aborted'){
26        this.lastEvaluation=e;if(e.recommendation){this.recommended=e.recommendation;this.recommendedEffort=e.effortRecommendation;}
27      }
28      this.record(e);
29    });
30  }
31  invalidate():void{this.revision++;this.observer.invalidate();this.auto?.invalidate();}
32  private change(mode:Mode,pin?:string,effort:EffortPolicy=mode==='pin'||mode==='off'?'native':'auto'):void {
33    this.invalidate();this.auto?.stop();this.auto=undefined;this.observer.invalidate(true);
34    this.mode=mode;this.pin=pin;this.effort=effort;this.blocked=false;this.note='';this.recommended=undefined;this.lastEvaluation=undefined;
35    this.requested=undefined;this.requestedEffort=undefined;this.recommendedEffort=undefined;
36  }
37  async bind(id:string,source:string,host:ControlHost):Promise<void>{
38    if(this.sessionId===id&&this.loaded&&source!=='clear'&&source!=='fork'){this.host=host;return;}
39    if(this.sessionId===id&&this.loading)return this.loading;
40    this.change('observe');this.sessionId=id;this.host=host;this.loaded=false;
41    this.actual=undefined;this.nativeModel='unknown';this.nativeEffort=undefined;this.nativeEffortSeen=false;this.catalog={};this.catalogPending=undefined;this.catalogEpoch++;
42    const revision=this.revision;
43    const pending=(async()=>{
44      let saved:unknown,readFailed=false;
45      try{saved=source==='clear'||source==='fork'?undefined:await host.get(key(id));}
46      catch{readFailed=true;}
47      if(this.sessionId!==id||this.revision!==revision)return;
48      if(readFailed)this.note='Saved mode could not be read; observe selected.';
49      const s=saved as Partial<SavedControl>|undefined;
50      if(s?.version===1&&s.sessionId===id&&['observe','off','auto','pin'].includes(s.mode??'')){
51        const effort=isEffort(s.effort)||s.effort==='auto'||s.effort==='native'?s.effort:undefined;
52        if((s.mode==='auto'||(s.mode==='pin'&&effort==='auto'))&&!this.allowAuto){this.change('off');this.note='Saved auto mode is unavailable until experimental auto is enabled.';}
53        else if(s.mode==='pin'&&(isTier(s.pin)||modelId(s.pin)||(s.pin===undefined&&effort!==undefined))){this.change('pin',s.pin,effort);this.blocked=s.blocked===true;}
54        else if(s.mode!=='pin')this.change(s.mode as Mode,undefined,effort);
55      }
56      this.loaded=true;
57    })();
58    this.loading=pending;
59    try{await pending}finally{if(this.loading===pending)this.loading=undefined}
60  }
61  private async discover():Promise<Catalog>{
62    if(Object.keys(this.catalog).length)return this.catalog;
63    const session=this.sessionId,epoch=this.catalogEpoch;
64    if(!this.catalogPending)this.catalogPending=this.host!.discover();
65    const pending=this.catalogPending;
66    try{const catalog=await pending;if(this.sessionId!==session)throw Error('Session changed');
67      if(epoch===this.catalogEpoch)this.catalog=this.capped(catalog);return this.catalog;}
68    finally{if(this.catalogPending===pending)this.catalogPending=undefined;}
69  }
70  private capped(catalog:Catalog):Catalog{
71    const result:Catalog={};for(const tier of TIERS){const target=catalog[tier];if(!target)continue;
72      const efforts=target.efforts.filter((e):e is Effort=>isEffort(e)&&withinCap(e,this.maxEffort));
73      const versions=target.versions?.map(v=>({model:v.model,efforts:v.efforts.filter(e=>isEffort(e)&&withinCap(e,this.maxEffort))})).filter(v=>v.efforts.length);
74      if(efforts.length)result[tier]={model:target.model,efforts,...(versions?.length?{versions}:{})};}return result;
75  }
76  async warmCatalog():Promise<void>{
77    const revision=this.revision;
78    try{await this.discover()}catch{if(this.revision===revision)this.note='Model discovery unavailable; /jevshift refresh retries. Native control is unchanged.';}
79  }
80  async save():Promise<void>{
81    if(!this.host||!this.loaded)return;
82    const host=this.host,id=this.sessionId;
83    const value:SavedControl={version:1,sessionId:id,mode:this.mode,effort:this.effort,...(this.pin?{pin:this.pin}:{}),...(this.blocked?{blocked:true}:{})};
84    this.saveQueue=this.saveQueue.catch(()=>{}).then(()=>host.set(key(id),value));
85    try{await this.saveQueue}catch{if(this.sessionId===id)this.note='Mode changed for this process, but could not be saved for resume.';}
86  }
87  async command(args:string,native:string):Promise<string>{
88    this.nativeModel=native;
89    const words=args.trim().split(/\s+/).filter(Boolean),action=words[0]??'status';
90    if(action==='status'&&words.length<=1)return this.status();
91    if(['setup','refresh'].includes(action)&&words.length===1){
92      const revision=this.revision;
93      try{
94        const discovered=await this.host!.discover();
95        if(revision!==this.revision)return 'Discovery discarded because controls changed.';
96        const blocked=this.blocked,note=this.note;
97        this.catalog=this.capped(discovered);this.catalogEpoch++;
98        this.change(action==='setup'?'observe':this.mode,action==='setup'?undefined:this.pin,action==='setup'?'auto':this.effort);
99        if(action==='refresh'){this.blocked=blocked;this.note=blocked?note:'';}
100        await this.save();
101        return `Models discovered: ${TIERS.filter(t=>this.catalog[t]).map(t=>`${t} = ${this.catalog[t]!.model} (${this.catalog[t]!.efforts.join(', ')})`).join('; ')}. ${action==='setup'?'Observe selected.':'Current controls preserved.'} Automatic effort cap: ${this.maxEffort}.`;
102      }catch{return 'Model discovery failed; existing mode unchanged. Check that the configured Claude executable is available.';}
103    }
104    if(['off','observe','auto'].includes(action)&&words.length===1){
105      if(action==='auto'){
106        if(!this.allowAuto)return 'Auto is still experimental and disabled. Selection quality and real-world evaluator reliability remain under review; enable experimental_auto only for an isolated development test.';
107        const revision=this.revision;
108        try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
109        if(this.revision!==revision)return 'Auto request discarded because controls changed.';
110      }
111      this.change(action as Mode);await this.save();return this.status();
112    }
113    if(action==='pin'&&(words.length===2||words.length===3)){
114      if(words.length===3&&(!isEffort(words[2])||!withinCap(words[2],this.maxEffort)))return 'Invalid or capped effort; existing mode unchanged. '+help;
115      if(!TIERS.includes(words[1] as any)&&!modelId(words[1]))return 'Invalid pin; existing mode unchanged. '+help;
116      const revision=this.revision;
117      try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
118      if(this.revision!==revision)return 'Pin request discarded because controls changed.';
119      const target=resolveTarget(this.catalog,words[1],native);
120      if(!target)return 'That model is not in the supported native catalog; existing mode unchanged.';
121      if(!compatible(target.target,native,words[2]))return 'That pin is incompatible with the selected context window or supported effort; existing mode unchanged.';
122      this.change('pin',words[1],words[2] as Effort|undefined);await this.save();return this.status();
123    }
124    if(action==='effort'&&words.length===2){
125      const effort=words[1];
126      if(!isEffort(effort)&&effort!=='auto'&&effort!=='native')return 'Invalid effort; existing mode unchanged. '+help;
127      if(isEffort(effort)&&!withinCap(effort,this.maxEffort))return `Effort exceeds the configured ${this.maxEffort} cap; existing mode unchanged.`;
128      if(effort==='auto'&&!this.allowAuto&&this.mode!=='observe')return 'Automatic effort is experimental; enable experimental_auto first.';
129      const revision=this.revision;
130      try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
131      if(revision!==this.revision)return 'Effort request discarded because controls changed.';
132      if(this.mode==='pin'&&isEffort(effort)){
133        const target=resolveTarget(this.catalog,this.pin??native,native);
134        if(!target||!compatible(target.target,native,effort))return 'That effort is unavailable for the pinned model; existing mode unchanged.';
135      }
136      // Observe remains advisory. Off plus a fixed effort holds the native model without calling Jev.
137      const mode=this.mode==='off'?(effort==='native'?'off':'pin'):this.mode;
138      this.change(mode,this.pin,effort);await this.save();return this.status();
139    }
140    return help+' Existing mode unchanged.';
141  }
142  status():string{
143    const evaluation=this.lastEvaluation;
144    const outcome=evaluation?({transport_timeout:'timed out',timeout:'timed out',missing_key:'missing key',
145      transport_disabled:'disabled by native network setting',recommended:'recommended'} as Record<string,string>)[evaluation.outcome]??evaluation.outcome:'none';
146    const evaluationText=outcome+(evaluation?.elapsedMs!==undefined?` (${evaluation.elapsedMs} ms)`:'');
147    return [`JevShift: ${this.mode}${this.pin?` (${this.pin})`:''}${this.blocked?' (blocked; choose another model or /jevshift off)':''}`,
148      `Native default: ${this.nativeModel}`,`Native effort: ${this.nativeEffort??'default'}`,`Effort control: ${this.effort} (cap: ${this.maxEffort})`,
149      `Resolved models: ${TIERS.filter(t=>this.catalog[t]).map(t=>`${t} = ${this.catalog[t]!.model}`).join('; ')||'not discovered'}`,
150      `Last Jev result: ${evaluationText}`,`Last Jev recommendation: ${this.recommended??'none'}`,`Last Jev effort: ${this.recommendedEffort??'none'}`,
151      `Last requested model: ${this.requested??'none'}`,`Last returned model: ${this.actual??'none'}`,
152      `Last requested effort: ${this.requested===undefined?'none':this.requestedEffort??'default'}`,
153      ...(this.note?[this.note]:[]),...(this.mode==='pin'?[this.effort==='auto'?'Model held; Jev chooses effort.':'Pin makes no Jev calls. Changes apply on the next model request.']:[])].join('\n');
154  }
155  async nativeEffortCommand():Promise<void>{this.change('off');this.note='Native effort selection took control.';await this.save();}
156  private fallback(id:Boundary):ModelSelection{
157    const pinned=this.mode==='pin',fixed=isEffort(this.effort)&&!['off','observe'].includes(this.mode);
158    const input=pinned?this.pin??id.nativeModel:id.nativeModel,effort=fixed?this.effort as Effort:id.effort;
159    let model=input;
160    if(pinned||fixed){
161      const target=resolveTarget(this.catalog,model,id.nativeModel);
162      if(!target||!compatible(target.target,id.nativeModel,effort))return {model:id.nativeModel,block:'The held model/effort pair is unavailable. Choose another pin or use /jevshift off.'};
163      model=target.target.model;
164    }
165    return {model,effort,...(pinned?{stopFallback:true}:{})};
166  }
167  async nativeSwitch(source:string,model:string):Promise<void>{
168    this.nativeModel=model;
169    if(['command','picker','sdk'].includes(source)){this.change('off');this.note='Native model selection took control.';await this.save();}
170    else if(source==='auto'){
171      if(this.mode==='pin'){this.invalidate();this.blocked=true;this.note='Native fallback attempted; pin remains blocked.';await this.save();}
172      else if(this.mode==='auto'){this.change('off');this.note='Native fallback stopped automatic selection.';await this.save();}
173    }
174  }
175  async guard(id:Boundary):Promise<ModelSelection|undefined>{
176    if(id.sessionId!==this.sessionId)return;
177    this.nativeModel=id.nativeModel;
178    if(this.nativeEffortSeen&&this.nativeEffort!==id.effort&&this.mode!=='off'){
179      this.change('off');this.note='Native effort changed; native control retained.';await this.save();
180    }
181    this.nativeEffort=id.effort;this.nativeEffortSeen=true;
182    if(this.mode==='off')return this.track({model:id.nativeModel,effort:id.effort},id);
183    if(this.mode!=='pin')return;
184    if(!this.blocked){
185      const revision=this.revision;
186      try{await this.discover()}catch{if(revision===this.revision){this.blocked=true;this.note='Pinned model could not be revalidated.';}}
187      if(revision!==this.revision)return this.guard(id);
188      const target=resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel);
189      const effort=this.effort==='auto'?undefined:this.effort==='native'?id.effort:this.effort;
190      if(!target||!compatible(target.target,id.nativeModel,effort)){this.blocked=true;this.note='Pinned model is unavailable or incompatible with this effort/context setting.';}
191    }
192    if(this.blocked){await this.save();return {model:id.nativeModel,block:this.note||'Pinned model is blocked. Choose a model or /jevshift off.'};}
193    if(this.effort==='auto')return;
194    return this.track({model:resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel)!.target.model,effort:this.effort==='native'?id.effort:this.effort,stopFallback:true},id,true);
195  }
196  async select(snapshot:ContextSnapshot,id:Boundary,host:ObserveHost):Promise<ModelSelection|undefined>{
197    if(id.sessionId!==this.sessionId)return;
198    const guarded=await this.guard(id);if(guarded)return guarded;
199    const revision=this.revision;
200    let selected:ModelSelection|undefined;
201    if(this.mode==='auto'||(this.mode==='pin'&&this.effort==='auto')){
202      try{await this.discover()}catch{if(revision===this.revision){this.change('off');this.note='Model discovery failed; native control retained.';await this.save();}return;}
203      if(revision!==this.revision)return this.guard(id);
204      const pinned=this.mode==='pin'?resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel):undefined;
205      const catalog=pinned?{[pinned.tier]:pinned.target}:this.catalog;
206      this.auto??=new AutoSession(catalog,e=>this.record(e),this.observer);
207      // A restored auto controller is rebuilt after current-process discovery.
208      selected=await this.auto.select(snapshot,id,host,{...(pinned?{pinnedModel:pinned.tier}:{}),
209        ...(this.effort!=='auto'?{fixedEffort:this.effort}:{}),nativeEffort:id.effort});
210      if(pinned&&!selected)selected=this.fallback(id);
211    }else {
212      try{await this.discover()}catch{this.note='Model discovery failed; recommendation skipped.';return this.track({model:id.nativeModel,effort:id.effort},id);}
213      if(revision!==this.revision)return this.guard(id);
214      await this.observer.consume(snapshot,id,host,{scope:{catalog:this.catalog,...(this.effort!=='auto'?{fixedEffort:this.effort}:{}),nativeEffort:id.effort}});
215    }
216    if(this.revision!==revision)return this.guard(id);
217    return this.track(selected?{...selected,...(this.mode==='pin'?{stopFallback:true}:{})}:this.fallback(id),id,this.mode==='pin',this.mode==='auto');
218  }
219  async finalize(selected:ModelSelection|undefined,id:Boundary):Promise<ModelSelection|undefined>{
220    if(id.sessionId!==this.sessionId)return;
221    // An immediate pin/off command can supersede a decision while Jev is awaited.
222    if((this.mode==='pin'&&this.effort!=='auto')||this.mode==='off')return this.guard(id);
223    if((this.mode==='pin'||this.mode==='auto')&&!selected)return this.track(this.fallback(id),id,this.mode==='pin',this.mode==='auto');
224    if(selected?.valid&&!selected.valid())return this.track(this.fallback(id),id,this.mode==='pin',this.mode==='auto');
225    return selected;
226  }
227  private track(selection:ModelSelection,id:Boundary,pinned=false,automatic=false):ModelSelection{
228    const revision=this.revision,session=this.sessionId;
229    this.requested=selection.model;this.requestedEffort=selection.effort;
230    return {...selection,valid:()=>revision===this.revision&&!id.signal?.aborted&&(!selection.valid||selection.valid()),
231      completed:async actual=>{
232        if(session!==this.sessionId)return;
233        await selection.completed?.(actual);this.actual=actual??undefined;
234        if(revision!==this.revision)return;
235        if(actual&&actual!==selection.model.replace('[1m]','')){
236          if(pinned){this.blocked=true;this.note='Returned model differed from the pin; further requests blocked.';}
237          else if(automatic){this.change('off');this.note='Returned model differed; native control retained.';}
238          await this.save();
239        }
240      },
241      failed:async error=>{
242        await selection.failed?.(error);
243        if(revision!==this.revision)return;
244        if(pinned){this.blocked=true;this.note='Pinned model request failed. Choose another model, re-pin to retry, or use /jevshift off.';}
245        else if(automatic){this.change('off');this.note='Native request failed; automatic selection stopped.';}
246        await this.save();
247      }};
248  }
249}
250
src/context-hooks.ts 105 lines
1import type { Register, EngineInterface, TurnStepInput } from 'claude-code';
2import type {Reply} from './jev.ts';
3import { boundedFetch,TransportError } from './transport.ts';
4import { ContextSession } from './context-session.ts';
5import type { ContextSnapshot } from './context.ts';
6import { routingEvidence, type ToolOutcome } from './routing-evidence.ts';
7
8/** Collect at native request boundaries; only an explicit validated selection rewrites a model. */
9export type Boundary = {sessionId: string; turnId: string; step: number; nativeModel: string; effort?: TurnStepInput['effort']; signal?: AbortSignal; outcomes?: ToolOutcome[]};
10export type Invalidation = 'prompt' | 'turn' | 'session' | 'compact';
11export type ModelSelection = {model:string; effort?: TurnStepInput['effort']; valid?:()=>boolean; block?:string; stopFallback?:boolean; completed?:(actual:string|null)=>void|Promise<void>; failed?:(error?:unknown)=>void|Promise<void>};
12export type BoundaryHost = {
13  clock: Pick<EngineInterface['clock'],'now'|'after'>;
14  http: {fetch:(url:string,init:{method:string;headers:Record<string,string>;body:string})=>Promise<Reply>};
15  ui: Pick<EngineInterface['ui'],'log'|'status'>;
16  environmentKey: () => Promise<string | undefined>;
17};
18export type ContextControls = {
19  lifecycle?:(id:string,source:string,host:{get:(key:string)=>Promise<unknown>;set:(key:string,value:unknown)=>Promise<void>})=>Promise<void>;
20  guard?:(id:Boundary)=>Promise<ModelSelection|undefined>;
21  finalize?:(selection:void|ModelSelection,id:Boundary)=>Promise<ModelSelection|undefined>;
22};
23export function registerContextHooks(on: Parameters<Register>[0], consume: (snapshot: ContextSnapshot,
24  identity: Boundary, host: BoundaryHost) => Promise<void | ModelSelection>, invalidate: (reason:Invalidation) => void = () => {}, controls:ContextControls = {}): void {
25  const context = new ContextSession();
26  on('prompt.submit', async ($, e, next) => {
27    let ticket: number | null = null;
28    try {
29      ticket = context.stage(await $.session.id(), e, await $.session.messages());
30      if (ticket !== null) invalidate('prompt');
31    } catch { /* Native prompt still proceeds. */ }
32    try {
33      const result = await next(e);
34      if ('drop' in result) context.reject(ticket);
35      return result;
36    } catch (error) { context.reject(ticket); throw error; }
37  });
38  on('turn.start', async ($, e, next) => {
39    invalidate('turn');
40    try { context.beginTurn(await $.session.id(), e.turnId, e.text); } catch { context.reset(); }
41    return next(e);
42  });
43  on('classic.SessionStart', async ($, e, next) => {
44    if (['clear','fork','resume'].includes(e.source)) { context.reset(); invalidate('session'); }
45    await controls.lifecycle?.(await $.session.id(),e.source,{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value)});
46    return next(e);
47  });
48  on('session.compact', async ($, e, next) => {
49    const result = await next(e);
50    if (!e.agentId && e.trigger !== 'precompute' && !result.skip) { context.compacted(); invalidate('compact'); }
51    return result;
52  });
53  on('turn.step', async function* ($, e, next) {
54    let selection: void | ModelSelection;
55    let identity:Boundary|undefined;
56    if (!e.agentId) {
57      // A collection/evaluator failure preserves native behavior and does not retry.
58      try {
59        const id = await $.session.id();
60        identity={sessionId:id,turnId:e.turnId,step:e.index,nativeModel:e.model,effort:e.effort,signal:next.signal};
61        selection=await controls.guard?.(identity);
62        if(!selection){
63        const messages = await $.session.messages();
64        const snapshot = context.snapshot(id, e.turnId, messages);
65        selection = await consume(snapshot, {sessionId:id, turnId:e.turnId, step:e.index, nativeModel:e.model, effort:e.effort,
66          outcomes:routingEvidence(messages),signal:next.signal}, {
67          clock:{now:()=>$.clock.now(),after:(ms,fn)=>$.clock.after(ms,fn)},
68          http:{fetch:async(url,init)=>{
69            const disabled=await $.env.get('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC');
70            if(disabled==='1'||disabled==='true')throw new TransportError('transport_disabled');
71            return boundedFetch((argv,options)=>$.process.run(argv,options),url,init);
72          }},
73          ui:{log:(text,options)=>$.ui.log(text,options),status:text=>$.ui.status(text)},
74          environmentKey:()=>$.env.get('OPENROUTER_API_KEY'),
75        });
76        }
77      } catch { /* A collector failure leaves native behavior intact. */ }
78    }
79    if(identity&&controls.finalize)selection=await controls.finalize(selection,identity);
80    if(selection?.block){
81      $.ui.log(`JevShift · ${selection.block}`);$.ui.status('JevShift · Pin blocked');
82      await $.turn.abort({turnId:e.turnId});
83      return {turnId:e.turnId,index:e.index,answer:'',toolUses:[],stopReason:null,usage:null};
84    }
85    // Only model and effort are rewritable. Result and stream are returned verbatim.
86    if (selection?.valid && !selection.valid()) selection=undefined;
87    try {
88      const result = yield* next(selection ? {...e,model:selection.model,effort:selection.effort} : e);
89      try {
90        if (result?.stopReason === null && !result.usage) await selection?.failed?.();
91        else await selection?.completed?.(result?.usage?.model ?? null);
92      } catch { /* Diagnostic only. */ }
93      return result;
94    } catch (error) {
95      try { await selection?.failed?.(error); } catch { /* Preserve the original native error. */ }
96      if(selection?.stopFallback&&String(error).includes('Model fallback triggered:')){
97        $.ui.log('JevShift · Pinned model unavailable; automatic fallback stopped. Choose another model or /jevshift off.');
98        await $.turn.abort({turnId:e.turnId});
99        return {turnId:e.turnId,index:e.index,answer:'',toolUses:[],stopReason:null,usage:null};
100      }
101      throw error;
102    }
103  });
104}
105
src/discovery.ts 20 lines
1import {CATALOG_ARGS,CATALOG_INPUT,parseCatalog,TIERS} from './catalog.ts';
2import type {Catalog} from './auto.ts';
3
4type Runner=(argv:string[],init:{stdin:string;timeoutMs:number;env:Record<string,string>})=>Promise<{exitCode:number;stdout:string}>;
5/** Isolated no-inference helpers inherit the user's provider configuration and model overrides. */
6export async function discoverCatalog(run:Runner,executable:string):Promise<Catalog>{
7  const results=await Promise.allSettled(TIERS.map(async tier=>{
8    const result=await run([executable,...CATALOG_ARGS,'--model',tier],{
9      stdin:CATALOG_INPUT,timeoutMs:8000,
10      env:{CLAUDECODE:'',CLAUDE_CODE_ENABLE_FUNCTION_HOOKS:'0',CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:'1'},
11    });
12    if(result.exitCode!==0)throw Error('Catalog process failed');
13    return parseCatalog(result.stdout,tier);
14  }));
15  const catalog:Catalog={};
16  results.forEach((r,i)=>{if(r.status==='fulfilled')catalog[TIERS[i]]=r.value});
17  if(!Object.keys(catalog).length)throw Error('No supported model aliases');
18  return catalog;
19}
20
src/auto.ts 105 lines
1import type { Boundary, ModelSelection } from './context-hooks.ts';
2import type { ContextSnapshot } from './context.ts';
3import { ObserveSession, type ObserveHost, type Observation } from './observe.ts';
4import type { Tier,DecisionScope } from './jev.ts';
5import {contextTarget} from './catalog.ts';
6
7export type Target = {model:string; efforts:readonly string[]; versions?:readonly Target[]};
8/** Supplied by the host after native capability discovery; no inferred account entitlement. */
9export type Catalog = Partial<Record<Tier,Target>>;
10export type RoutingEvent = {event:'routing';sessionId:string;turnId:string;step:number;generation:number;
11  outcome:string;reason?:string;incomingModel:string;selectedModel?:string;selectedEffort?:Boundary['effort'];actualModel?:string|null;choice?:Tier;evaluations:number};
12export const AUTO_LIMITS = {evaluations:3, progressSteps:2, progressResults:2, repeatedErrors:2} as const;
13type Picked = {tier:Tier;model:string;effort:Boundary['effort']};
14export class AutoSession {
15  private evaluator: ObserveSession;
16  private session=''; private instruction=-1; private generation=-1;
17  private enabled=true; private epoch=0; private evaluations=0;
18  private selected: Picked|undefined; private visited=new Set<string>();
19  private seenOutcomes=new Set<string>(); private boundary=''; private boundaryCount=0; private evaluatedAt=0;
20  private notice='';
21  constructor(private readonly catalog: Catalog, private readonly record:(e:Observation|RoutingEvent)=>void=()=>{}, evaluator?:ObserveSession) {
22    this.evaluator=evaluator??new ObserveSession(e=>this.emit(e));
23  }
24  private emit(e:Observation|RoutingEvent):void { try {this.record(e)} catch { /* Diagnostics never select models. */ } }
25  invalidate():void {this.epoch++;this.evaluator.invalidate();this.selected=undefined;}
26  stop():void {this.enabled=false;this.invalidate();}
27  private show(host:ObserveHost,text:string):void {if(this.notice===text)return;this.notice=text;try{host.show(text)}catch{}}
28  private supported(tier:Tier,id:Boundary,effort:Boundary['effort']):Picked|undefined {
29    const offered=this.catalog[tier],target=offered&&contextTarget(offered,id.nativeModel);
30    if (!target || !/^claude-[a-z0-9-]+(?:\[1m\])?$/.test(target.model)) return;
31    // A catalog entry is capability evidence, never a quota/entitlement bypass.
32    if (effort !== undefined && (typeof effort!=='string' || !target.efforts.includes(effort))) return;
33    if (id.nativeModel.includes('[1m]') && !target.model.includes('[1m]')) return;
34    return {tier,model:target.model,effort};
35  }
36  async select(snapshot:ContextSnapshot,id:Boundary,host:ObserveHost,scope:DecisionScope={}):Promise<ModelSelection|undefined> {
37    if (!this.enabled || id.signal?.aborted || !snapshot.state || !snapshot.meta.ready) return;
38    if (this.session && this.session!==id.sessionId) {this.stop();return;}
39    this.session=id.sessionId;
40    const event=(outcome:string,extra:Partial<RoutingEvent>={})=>this.emit({event:'routing',sessionId:id.sessionId,
41      turnId:id.turnId,step:id.step,generation:snapshot.meta.generation,incomingModel:id.nativeModel,
42      evaluations:this.evaluations,outcome,...extra});
43    const instruction=snapshot.meta.instructionId;
44    const fresh=instruction!==this.instruction;
45    const revision=snapshot.meta.generation!==this.generation;
46    if (fresh) {
47      this.invalidate();this.instruction=instruction;this.evaluations=0;this.visited.clear();this.seenOutcomes.clear();
48      this.boundaryCount=0;this.evaluatedAt=0;this.boundary='';
49    } else if (revision) this.invalidate();
50    this.generation=snapshot.meta.generation;
51    const boundary=`${id.turnId}/${id.step}/${snapshot.meta.generation}`;
52    if (boundary!==this.boundary) {this.boundary=boundary;this.boundaryCount++;}
53    const outcomes=id.outcomes ?? [], newOutcomes=outcomes.filter(t=>!this.seenOutcomes.has(t.id));
54    let reason:string|undefined;
55    if (fresh || revision) reason=fresh?'new_instruction':'context_revision';
56    else if (newOutcomes.some(t=>t.tool==='ExitPlanMode'&&!t.error)) reason='plan_approved';
57    else if (newOutcomes.filter(t=>t.error).length>=AUTO_LIMITS.repeatedErrors) reason='repeated_tool_errors';
58    else if (this.boundaryCount-this.evaluatedAt>=AUTO_LIMITS.progressSteps && newOutcomes.length>=AUTO_LIMITS.progressResults) reason='new_tool_progress';
59    if (reason && this.evaluations<AUTO_LIMITS.evaluations) {
60      const previous=this.selected;this.selected=undefined;
61      const epoch=this.epoch;
62      this.evaluations++;this.evaluatedAt=this.boundaryCount;
63      this.seenOutcomes=new Set(outcomes.map(t=>t.id));
64      const result=await this.evaluator.consume(snapshot,id,host,{checkpoint:`${instruction}/${snapshot.meta.generation}/${this.evaluations}`,quiet:true,scope:{...scope,catalog:this.catalog,nativeEffort:id.effort}});
65      if (!this.enabled || this.epoch!==epoch || id.signal?.aborted) {event('stale');return;}
66      if (result?.outcome!=='recommended' || !result.recommendation) {
67        event('native_passthrough',{reason:result?.outcome ?? 'evaluator_skipped'});
68        this.show(host,'JevShift · Auto: no fresh Jev choice; keeping the incoming model and effort.');return;
69      }
70      const choice=result.recommendation,effort=scope.fixedEffort==='native'?id.effort:result.effortRecommendation,
71        target=this.supported(choice,id,effort);
72      if (!target) {
73        event('target_not_validated',{reason,choice});
74        this.show(host,'JevShift · Auto: target model or effort not validated; keeping the incoming model.');return;
75      }
76      // Reversals need fresh repeated failures, not another routine progress sample.
77      const pair=`${choice}/${effort??'default'}`;
78      if (previous && (choice!==previous.tier||effort!==previous.effort) && this.visited.has(pair) && !['repeated_tool_errors','plan_approved'].includes(reason)) {
79        this.selected=previous;event('reversal_held',{reason,choice,selectedModel:previous.model});
80      } else {
81        this.selected=target;this.visited.add(pair);event('choice_accepted',{reason,choice,selectedModel:target.model,selectedEffort:target.effort});
82      }
83    } else if (reason && this.evaluations>=AUTO_LIMITS.evaluations) event('evaluation_cap',{reason});
84    const selected=this.selected;
85    if (!selected) return;
86    // A reused choice must still fit this incoming request's effort/context mode.
87    const compatible=this.supported(selected.tier,id,selected.effort);
88    if (!compatible || compatible.model!==selected.model) {this.selected=undefined;event('incompatible_request');return;}
89    const epoch=this.epoch;
90    event('request_selected',{choice:selected.tier,selectedModel:selected.model,selectedEffort:selected.effort});
91    this.show(host,`JevShift · Auto: requesting ${selected.model} / ${selected.effort??'default'}.`);
92    return {model:selected.model,effort:selected.effort,
93      valid:()=>this.enabled && this.epoch===epoch && this.selected===selected && !id.signal?.aborted,
94      completed:actual=>{
95        event('response_observed',{selectedModel:selected.model,actualModel:actual});
96        if (epoch===this.epoch && actual && actual!==selected.model.replace('[1m]','')) {
97          this.stop();this.show(host,'JevShift · Auto stopped: Claude returned a different model. Native control retained.');
98        }
99      },
100      failed:()=>{if(epoch===this.epoch)this.stop();event('native_request_failed',{selectedModel:selected.model});
101        this.show(host,'JevShift · Auto stopped after a native request error. The original error remains visible.');},
102    };
103  }
104}
105
src/observe.ts 122 lines
1import {EVALUATOR_LIMITS} from './evaluator-limits.ts';
2import {TransportError,type TransportOutcome} from './transport.ts';
3import type { ContextSnapshot } from './context.ts';
4import type { Boundary } from './context-hooks.ts';
5import { ENDPOINT, parseReply, requestBody, type Reply, type Parsed, type Tier, type Usage,type DecisionScope } from './jev.ts';
6import type {Effort} from './effort.ts';
7
8export type ObserveHost = {
9  now: () => Promise<number>;
10  after: (ms: number, fn: () => void) => {cancel: () => void};
11  fetch: (url: string, init: {method:string;headers:Record<string,string>;body:string}) => Promise<Reply>;
12  key: () => Promise<string | undefined>;
13  show: (text: string) => void;
14};
15export type Observation = {
16  event: 'started' | 'finished' | 'late_settlement' | 'skipped';
17  sessionId: string; turnId: string; generation: number; step: number; attempt?: number;
18  outcome: string; nativeModel: string; recommendation?: Tier;effortRecommendation?:Effort;
19  timestampMs: number; elapsedMs?: number; billing?:'reported'|'unknown'|'not_requested'; usage?: Usage; httpStatus?: number; evaluatorModel?: string;
20};
21const safeModel = (x: string) => typeof x === 'string' && /^[\w./:-]{1,128}$/.test(x) ? x : 'selected model';
22const title = (s: string) => s.charAt(0).toUpperCase()+s.slice(1);
23
24/** One instance per registration. No model/effort mutation, raw-context log, or retries. */
25export class ObserveSession {
26  private session = '';
27  private seen = '';
28  private epoch = 0;
29  private serial = 0;
30  private pending = false;
31  private cancelWait: (() => void) | undefined;
32  private lastNotice = '';
33  constructor(private readonly record: (event: Observation) => void = () => {}) {}
34  invalidate(clearSeen = false): void { this.epoch++; this.cancelWait?.(); if (clearSeen) this.seen=""; }
35  private emit(e: Observation): void { try { this.record(e); } catch { /* Diagnostics cannot break native work. */ } }
36  private notice(host: ObserveHost, text: string): void {
37    if (this.lastNotice === text) return;
38    this.lastNotice = text;
39    try { host.show(text); } catch { /* UI is advisory. */ }
40  }
41  async consume(snapshot: ContextSnapshot, id: Boundary, host: ObserveHost,
42    options: {checkpoint?:string; quiet?:boolean;scope?:DecisionScope} = {}): Promise<Observation | undefined> {
43    if (this.session !== id.sessionId) { this.invalidate(); this.session=id.sessionId; this.seen=''; this.lastNotice=''; }
44    const key = options.checkpoint ?? String(snapshot.meta.generation);
45    if (key === this.seen) return;
46    this.seen = key;
47    this.invalidate();
48    const epoch = this.epoch;
49    const nativeModel = safeModel(id.nativeModel);
50    const started = await host.now();
51    const base = {sessionId:id.sessionId,turnId:id.turnId,generation:snapshot.meta.generation,step:id.step,nativeModel,timestampMs:started};
52    if (!snapshot.state || !snapshot.meta.ready || id.signal?.aborted) {
53      this.emit({...base,event:'skipped',outcome:'no_current_context'}); return;
54    }
55    if (this.pending) {
56      this.emit({...base,event:'skipped',outcome:'request_still_pending'});
57      if (!options.quiet) this.notice(host,'JevShift · Observe: previous Jev request still pending; selected model unchanged.'); return;
58    }
59    const body = requestBody(snapshot.state,options.scope);
60    if (!body) { this.emit({...base,event:'skipped',outcome:'payload_too_large'}); return; }
61    // Start the deadline before reading credentials. A local deadline does not cancel HTTP.
62    let wake: (value: 'timeout' | 'stale' | 'aborted') => void = () => {};
63    const stopped = new Promise<'timeout' | 'stale' | 'aborted'>(resolve => { wake=resolve; });
64    const cancel = () => wake('stale');
65    const abort = () => wake('aborted');
66    this.cancelWait = cancel;
67    const timer = host.after(EVALUATOR_LIMITS.selectionWaitMs,() => wake('timeout'));
68    id.signal?.addEventListener('abort',abort,{once:true});
69    this.pending = true;
70    let attempt: number | undefined;
71    let abandoned = false;
72    type Result = Parsed | {outcome:'missing_key' | 'network_error' | 'stale' | TransportOutcome};
73    const operation = (async (): Promise<Result> => {
74      try {
75        const credential = (await host.key())?.trim();
76        if (abandoned || epoch !== this.epoch || id.signal?.aborted) return {outcome:'stale'};
77        if (!credential || credential.length > 4096 || /[\r\n]/.test(credential)) return {outcome:'missing_key'};
78        attempt = ++this.serial;
79        this.emit({...base,event:'started',outcome:'requested',attempt});
80        const reply = await host.fetch(ENDPOINT,{method:'POST',headers:{Authorization:`Bearer ${credential}`,'Content-Type':'application/json'},body});
81        return parseReply(reply,options.scope);
82      } catch (error) { return {outcome:error instanceof TransportError?error.outcome:'network_error'}; }
83    })();
84    const settlement = operation.then(async result => {
85      this.pending = false;
86      if (abandoned) {
87        let now = started;
88        try { now = await host.now(); } catch { /* Preserve settlement accounting. */ }
89        this.emit({...base,event:'late_settlement',outcome:result.outcome,attempt,elapsedMs:now-started,
90          billing:('usage' in result && result.usage)?'reported':attempt&&result.outcome!=='transport_disabled'?'unknown':'not_requested',
91          ...('usage' in result ? {usage:result.usage,httpStatus:result.httpStatus} : {})});
92      }
93      return result;
94    });
95    let result: Result | 'timeout' | 'stale' | 'aborted';
96    try { result = await Promise.race([settlement,stopped]); }
97    finally {
98      timer.cancel(); id.signal?.removeEventListener('abort',abort);
99      if (this.cancelWait === cancel) this.cancelWait=undefined;
100    }
101    if (typeof result === 'string') abandoned = true;
102    const elapsedMs = Math.max(0,(await host.now())-started);
103    const stale = epoch !== this.epoch || id.signal?.aborted;
104    const outcome = stale ? 'stale' : typeof result === 'string' ? result : result.outcome;
105    const decision = typeof result === 'object' && 'decision' in result ? result.decision : undefined;
106    const observation: Observation = {...base,event:'finished',attempt,outcome,elapsedMs,
107      billing:typeof result==='object'&&'usage' in result&&result.usage?'reported':attempt&&!(typeof result==='object'&&result.outcome==='transport_disabled')?'unknown':'not_requested',
108      ...(!stale && outcome==='recommended' && decision ? {recommendation:decision.choice,effortRecommendation:decision.effort,evaluatorModel:decision.model} : {}),
109      ...(typeof result==='object' && 'httpStatus' in result ? {usage:result.usage,httpStatus:result.httpStatus} : {})};
110    this.emit(observation);
111    if (stale || outcome==='aborted' || outcome==='stale' || options.quiet) return observation;
112    if (outcome==='recommended' && decision) {
113      this.notice(host,`JevShift · Observe: recommend ${title(decision.choice)}${decision.effort?` / ${decision.effort}`:''}; keeping ${nativeModel} / ${id.effort??'default'}.`);
114    } else if (outcome==='missing_key') {
115      this.notice(host,'JevShift · Observe needs an OpenRouter key. Set the sensitive openrouter_api_key plugin option; selected model unchanged.');
116    } else {
117      this.notice(host,`JevShift · Observe: ${outcome==='timeout'||outcome==='transport_timeout' ? 'Jev request timed out' : 'Jev unavailable or reply invalid'}; selected model unchanged.`);
118    }
119    return observation;
120  }
121}
122
src/effort.ts 6 lines
1export const EFFORTS = ['low','medium','high','xhigh','max'] as const;
2export type Effort = typeof EFFORTS[number];
3export type EffortPolicy = Effort | 'auto' | 'native';
4export const isEffort = (value:unknown):value is Effort => typeof value==='string' && EFFORTS.includes(value as Effort);
5export const withinCap = (value:Effort, cap:Effort):boolean => EFFORTS.indexOf(value)<=EFFORTS.indexOf(cap);
6
src/catalog.ts 61 lines
1import type {Catalog,Target} from './auto.ts';
2import type {Tier} from './jev.ts';
3import {EFFORTS} from './effort.ts';
4export const TIERS:readonly Tier[]=['sonnet','opus','fable'];
5export const isTier=(x:unknown):x is Tier=>TIERS.includes(x as Tier);
6export const modelId=(x:unknown):x is string=>typeof x==='string'&&/^claude-[a-z0-9-]{1,100}(?:\[1m\])?$/.test(x);
7const base=(model:string)=>model.replace('[1m]','');
8/** Resolve a family through Claude's own --model selection, never menu order or version sorting. */
9export function parseCatalog(stdout:string,tier:Tier):Target {
10  if(stdout.length>1_000_000)throw Error('Catalog response too large');
11  const responses=stdout.split('\n').filter(Boolean).map(line=>{try{return JSON.parse(line)}catch{return null}});
12  const response=(id:string)=>{
13    const matches=responses.filter(x=>x?.type==='control_response'&&x.response?.request_id===id);
14    if(matches.length!==1||matches[0].response.subtype!=='success')throw Error('Catalog discovery failed');
15    return matches[0].response.response;
16  };
17  const models=response('jevshift-catalog')?.models;
18  const resolved=response('jevshift-resolved')?.applied?.model;
19  const family=new RegExp(`^claude-${tier}-`);
20  if(!modelId(resolved)||!family.test(resolved))throw Error('Alias did not resolve to the requested family');
21  if(!Array.isArray(models)||models.length>100)throw Error('Invalid model list');
22  const entries=models.filter(x=>modelId(x?.resolvedModel)&&family.test(x.resolvedModel));
23  const versions:Target[]=[];
24  for(const id of [...new Set<string>(entries.map(x=>x.resolvedModel))].sort()){
25    const rows=entries.filter(x=>x.resolvedModel===id);
26    // Conflicting/disabled rows are not evidence of permission or supported effort.
27    if(rows.some(x=>x.disabled===true||x.supportsEffort===false||!Array.isArray(x.supportedEffortLevels)))continue;
28    const efforts=EFFORTS.filter(e=>rows.every(x=>x.supportedEffortLevels.includes(e)));
29    if(efforts.length)versions.push({model:id,efforts});
30  }
31  if(entries.some(x=>x.resolvedModel===resolved)&&!versions.some(x=>x.model===resolved))
32    throw Error('Resolved alias entry is disabled or unsupported');
33  const matches=versions.filter(x=>base(x.model)===base(resolved));
34  const chosen=matches.find(x=>x.model===resolved)??matches.find(x=>!x.model.includes('[1m]'))??matches[0];
35  if(!chosen)throw Error('Resolved alias has no supported catalog entry');
36  // The resolver also verifies the plain ID when the menu exposes only its 1M variant.
37  if(!versions.some(x=>x.model===resolved)&&!entries.some(x=>x.resolvedModel===resolved))
38    versions.push({model:resolved,efforts:chosen.efforts});
39  return {...chosen,...(versions.length>1?{versions:versions.filter(x=>x.model!==chosen.model)}:{})};
40}
41export function contextTarget(target:Target,native:string):Target|undefined {
42  if(!native.includes('[1m]')||target.model.includes('[1m]'))return target;
43  return target.versions?.find(x=>x.model===target.model+'[1m]');
44}
45export function resolveTarget(catalog:Catalog,input:string,native=''):{tier:Tier;target:Target}|undefined {
46  for(const tier of TIERS){
47    const target=catalog[tier];if(!target)continue;
48    if(input===tier){const contextual=contextTarget(target,native);return contextual?{tier,target:contextual}:undefined;}
49    if(input===target.model)return{tier,target};
50    const exact=target.versions?.find(x=>x.model===input);if(exact)return{tier,target:exact};
51  }
52}
53export function compatible(target:Target,native:string,effort:unknown):boolean {
54  return modelId(target.model) && (effort===undefined||(typeof effort==='string'&&target.efforts.includes(effort)))
55    &&(!native.includes('[1m]')||target.model.includes('[1m]'));
56}
57export const CATALOG_ARGS=['--safe-mode','--strict-mcp-config','--mcp-config','{"mcpServers":{}}',
58  '--no-session-persistence','--input-format','stream-json','--output-format','stream-json','--verbose','-p'];
59export const CATALOG_INPUT=[['jevshift-catalog','initialize'],['jevshift-resolved','get_settings']]
60  .map(([request_id,subtype])=>JSON.stringify({type:'control_request',request_id,request:{subtype}})).join('\n')+'\n';
61
src/context.ts 118 lines
1/** Pure, bounded context construction. No filesystem, network, or transcript mutation. */
2export type Message = {
3  role: 'user' | 'assistant'; text: string;
4  toolUses?: readonly { tool_use_id: string; tool: string; text?: string; isError?: boolean }[];
5  toolResults?: readonly { tool_use_id: string; text: string; isError: boolean }[];
6};
7export type RequestEvidence = { text: string; origin: 'composer' | 'bridge' | 'sdk'; generation: number; instructionId?: number };
8export type ContextSnapshot = {
9  state: string | null;
10  meta: { version: 1; ready: boolean; reason: string; bytes: number; serializedBytes: number; generation: number; instructionId: number;
11    messagesIncluded: number; toolsIncluded: number; redactions: number; truncated: boolean };
12};
13export const LIMITS = Object.freeze({ bytes: 7000, serializedBytes: 7000, scanToolsPerMessage: 32, inputChars: 65536, scanMessages: 64,
14  recentMessages: 6, requestBytes: 1600, messageBytes: 350, planBytes: 1100,
15  tools: 3, toolBytes: 650 });
16
17export function bytes(s: string): number {
18  let n = 0;
19  for (const c of s) { const p = c.codePointAt(0)!; n += p <= 0x7f ? 1 : p <= 0x7ff ? 2 : p <= 0xffff ? 3 : 4; }
20  return n;
21}
22function prefix(s: string, budget: number): string {
23  let result = '', n = 0;
24  for (const c of s) { const size = bytes(c); if (n + size > budget) break; result += c; n += size; }
25  return result;
26}
27function excerpt(s: string, budget: number): string {
28  if (bytes(s) <= budget) return s;
29  const mark = '\n[excerpt clipped]\n', head = Math.floor((budget - bytes(mark)) * .72);
30  // Input is sanitized before cutting, so clipping cannot expose a secret's suffix.
31  const tail = Array.from(s).reverse().join('');
32  return prefix(s, head) + mark + Array.from(prefix(tail, budget - bytes(mark) - head)).reverse().join('');
33}
34export function redact(raw: string): { text: string; count: number; oversized: boolean } {
35  if (raw.length > LIMITS.inputChars) return { text: '[oversized content omitted]', count: 0, oversized: true };
36  let text = raw.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '');
37  let count = 0;
38  const replace = (pattern: RegExp, value: string) => { text = text.replace(pattern, () => { count++; return value; }); };
39  replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-\n]*PRIVATE KEY-----|$)/g, '[REDACTED:private_key]');
40  replace(/\b(?:sk-(?:ant-|or-)?[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{16,}|AIza[A-Za-z0-9_-]{25,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/g, '[REDACTED:credential]');
41  replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED:jwt]');
42  replace(/^(?:cookie|set-cookie)\s*:[^\n]*/gim, '[REDACTED:cookie]');
43  replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '[REDACTED:authorization]');
44  replace(/(?:["']?)(?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)|authorization|cookie)(?:["']?)\s*[:=]\s*(?:"[^"\n]*"|'[^'\n]*'|[^\s,;\n}]+)/gi, '[REDACTED:secret_assignment]');
45  replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@[^\s<>'"]+/gi, '[REDACTED:credential_url]');
46  replace(/[?&](?:key|token|secret|password|code|api_key|access_token)=[^\s&#<>"']+/gi, '[REDACTED:query_secret]');
47  replace(/[A-Za-z0-9.+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '[REDACTED:email]');
48  replace(/\/(?:Users|home)\/[^/\s<>"']+/g, '[HOME]');
49  return { text, count, oversized: false };
50}
51function xml(s: string): string { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
52const planPattern = /(?:^|\n)\s*(?:#{1,4}\s*)?(?:plan|next steps|implementation|approach|design|proposed|agreed|decision)\b|(?:^|\n)\s*(?:\d+[.)]|- \[[ x]\])\s/im;
53
54export function buildContext(messages: readonly Message[], request: RequestEvidence | null): ContextSnapshot {
55  const meta: ContextSnapshot['meta'] = { version: 1, ready: false, reason: 'missing_verified_request', bytes: 0, serializedBytes: 0,
56    generation: request?.generation ?? 0, instructionId: request?.instructionId ?? request?.generation ?? 0,
57    messagesIncluded: 0, toolsIncluded: 0, redactions: 0, truncated: false };
58  if (!request || !request.text.trim()) return { state: null, meta };
59  if (request.text.length > LIMITS.inputChars) { meta.reason = 'request_too_large'; return { state: null, meta }; }
60  const clean = (raw: string, budget: number) => {
61    const safe = redact(raw); meta.redactions += safe.count;
62    if (safe.oversized || bytes(xml(safe.text)) > budget) meta.truncated = true;
63    // Clip escaped text using a raw prefix/tail first, then reduce without breaking entities.
64    let rawBudget = budget;
65    let cut = excerpt(safe.text, rawBudget);
66    while (bytes(xml(cut)) > budget) {
67      rawBudget = Math.max(1, Math.floor(rawBudget * .65));
68      cut = rawBudget < 24 ? prefix(safe.text, rawBudget) : excerpt(safe.text, rawBudget);
69    }
70    return xml(cut);
71  };
72  const current = clean(request.text.trim(), LIMITS.requestBytes);
73  const tail = messages.slice(-LIMITS.scanMessages);
74  meta.truncated ||= messages.length > tail.length;
75  // Exact current request is already represented separately. Tool-only rows are never dialogue.
76  const currentIndex = tail.findLastIndex(m => m.role === 'user' && !m.toolResults?.length && m.text.trim() === request.text.trim());
77  const dialogue = tail.map((m, i) => ({ m, i })).filter(({ m, i }) => i !== currentIndex && m.text?.trim() && !(m.toolResults?.length));
78  const recent = dialogue.slice(-LIMITS.recentMessages);
79  meta.truncated ||= dialogue.length > recent.length;
80  const history = recent.map(({m}) => `### ${m.role === 'assistant' ? 'Assistant' : 'User-role transcript (origin not retained)'}\n\n${clean(m.text, LIMITS.messageBytes)}`);
81  meta.messagesIncluded = history.length;
82  // Quote a nearby visible plan, not an invented summary or an arbitrarily old design.
83  const candidates = dialogue.slice(-12).filter(({m}) => m.role === 'assistant');
84  const plan = candidates.slice(-3).filter(({m}) => planPattern.test(m.text)).at(-1) ?? candidates.at(-1);
85  const planText = plan ? clean(plan.m.text, LIMITS.planBytes) : '';
86  const calls = new Map<string, {tool: string; text?: string; isError?: boolean; order: number}>();
87  const results = new Map<string, {tool: string; text: string; isError: boolean; order: number}>();
88  for (let i = 0; i < tail.length; i++) {
89    const uses = tail[i].toolUses ?? [], outcomes = tail[i].toolResults ?? [];
90    meta.truncated ||= uses.length > LIMITS.scanToolsPerMessage || outcomes.length > LIMITS.scanToolsPerMessage;
91    for (const t of uses.slice(-LIMITS.scanToolsPerMessage)) {
92      calls.set(t.tool_use_id, {tool: t.tool, text: t.text, isError: t.isError, order: i});
93      if (t.text !== undefined) results.set(t.tool_use_id, {tool: t.tool, text: t.text, isError: !!t.isError, order: i});
94    }
95    for (const t of outcomes.slice(-LIMITS.scanToolsPerMessage)) results.set(t.tool_use_id,
96      {tool: calls.get(t.tool_use_id)?.tool ?? 'Tool', text: t.text, isError: t.isError, order: i});
97  }
98  const selected = [...results.values()].sort((a,b) => a.order-b.order).slice(-LIMITS.tools);
99  meta.truncated ||= results.size > selected.length;
100  const tools = selected.map(t => `### ${clean(t.tool, 80)} — ${t.isError ? 'error' : 'completed'}\n\n${clean(t.text, LIMITS.toolBytes)}`);
101  meta.toolsIncluded = tools.length;
102  const section = (name: string, text: string) => `<${name}>\n${text || '(none available)'}\n</${name}>`;
103  let state = ['# Claude Code session context', section('current_request', current),
104    section('recent_messages', history.join('\n\n')),
105    section('work_summary', planText ? 'Visible assistant plan or latest work excerpt; not independently verified.\n\n' + planText : ''),
106    section('recent_tool_results', tools.join('\n\n'))].join('\n\n');
107  // Whole-section fallback keeps XML valid and gives the current request priority.
108  if (bytes(state) > LIMITS.bytes || bytes(JSON.stringify(state)) > LIMITS.serializedBytes) {
109    meta.truncated = true; meta.messagesIncluded = 0;
110    state = ['# Claude Code session context', section('current_request', current), section('recent_messages','[omitted to fit size limit]'),
111      section('work_summary', planText), section('recent_tool_results', tools.join('\n\n'))].join('\n\n');
112  }
113  if (bytes(state) > LIMITS.bytes || bytes(JSON.stringify(state)) > LIMITS.serializedBytes) { meta.reason = 'size_limit'; return { state: null, meta }; }
114  if (bytes(JSON.stringify(state)) > LIMITS.serializedBytes) { meta.reason = 'serialized_size_limit'; return { state: null, meta }; }
115  meta.ready = true; meta.reason = 'ready'; meta.bytes = bytes(state); meta.serializedBytes = bytes(JSON.stringify(state));
116  return { state, meta };
117}
118
src/jev.ts 56 lines
1import { bytes } from './context.ts';
2import { JEV_PROMPT } from './jev-prompt.ts';
3import {isEffort,type Effort} from './effort.ts';
4import type {Catalog} from './auto.ts';
5
6export const ENDPOINT = 'https://openrouter.ai/api/alpha/decisions';
7export type Tier = 'sonnet' | 'opus' | 'fable';
8export type Usage = {inputTokens: number; outputTokens: number; costUsd: number};
9export type Reply = {status: number; ok: boolean; text: string};
10export type Decision = {choice: Tier; effort?:Effort; confidence?: number; probabilities?: Record<Tier,number>; model: string};
11export type DecisionScope = {catalog?:Catalog; pinnedModel?:Tier; fixedEffort?:Effort|'native'; nativeEffort?:unknown};
12export type Parsed = {outcome: 'recommended' | 'http_error' | 'invalid_response'; decision?: Decision; usage?: Usage; httpStatus: number};
13const object = (x: unknown): x is Record<string,any> => !!x && typeof x === 'object' && !Array.isArray(x);
14const probability = (x: unknown): x is number => typeof x === 'number' && Number.isFinite(x) && x >= 0 && x <= 1;
15const count = (x: unknown): x is number => Number.isSafeInteger(x) && (x as number) >= 0;
16export function requestBody(state: string,scope:DecisionScope={}): string | null {
17  if(scope.pinnedModel&&scope.fixedEffort)return null;
18  const questions={...(!scope.pinnedModel?{model_choice:JEV_PROMPT.questions.model_choice}:{}),
19    ...(!scope.fixedEffort?{effort_choice:JEV_PROMPT.questions.effort_choice}:{})};
20  const available=scope.catalog?Object.entries(scope.catalog).map(([tier,target])=>
21    `${tier}: ${target!.model}; allowed effort: ${target!.efforts.join(', ')}`).join('\n'):'';
22  const constraints=[available,scope.pinnedModel?`Model pinned to ${scope.pinnedModel}; choose only its effort.`:'',
23    scope.fixedEffort?`Effort fixed to ${scope.fixedEffort==='native'?String(scope.nativeEffort??'model default'):scope.fixedEffort}; choose only a compatible model.`:''].filter(Boolean).join('\n');
24  const make=()=>JSON.stringify({model:JEV_PROMPT.model,questions,state:constraints?`<available_models>\n${constraints}\n</available_models>\n\n${state}`:state});
25  let body=make();
26  // Retain the verified current request. Remove older evidence before exceeding the total cap.
27  for(const section of ['recent_messages','work_summary','recent_tool_results']){
28    if(bytes(body)<=9000)break;
29    state=state.replace(new RegExp(`<${section}>[\\s\\S]*?</${section}>`),`<${section}>\n[omitted to fit request limit]\n</${section}>`);
30    body=make();
31  }
32  return bytes(body)<=9000?body:null;
33}
34/** Never return raw response bodies or server error text to diagnostics. */
35export function parseReply(reply: Reply,scope:DecisionScope={}): Parsed {
36  const base: Parsed = {outcome: reply.ok ? 'invalid_response' : 'http_error', httpStatus:reply.status};
37  if (reply.text.length > 65536) return base;
38  let x: any;
39  try { x = JSON.parse(reply.text); } catch { return base; }
40  if (!object(x)) return base;
41  const u = x.usage;
42  if (object(u) && count(u.input_tokens) && count(u.output_tokens) && typeof u.cost === 'number' && Number.isFinite(u.cost) && u.cost >= 0)
43    base.usage = {inputTokens:u.input_tokens, outputTokens:u.output_tokens, costUsd:u.cost};
44  if (!reply.ok) return base;
45  const validChoice=(a:any,labels:string[])=>object(a)&&a.type==='choice'&&labels.includes(a.choice)&&probability(a.confidence)&&
46    object(a.probabilities)&&Object.keys(a.probabilities).sort().join(',')===labels.slice().sort().join(',')&&
47    Object.values(a.probabilities).every(probability)&&Math.abs(Object.values(a.probabilities).reduce((sum:number,p:any)=>sum+p,0)-1)<.02;
48  const a=x.answers?.model_choice,e=x.answers?.effort_choice;
49  if((!scope.pinnedModel&&!validChoice(a,['sonnet','opus','fable'])) ||
50    (!scope.fixedEffort&&!validChoice(e,['low','medium','high','xhigh','max'])) ||
51    typeof x.model!=='string'||!/^typesafe\/jev-1\.13(?:-\d{8})?$/.test(x.model))return base;
52  const effort=scope.fixedEffort==='native'?(isEffort(scope.nativeEffort)?scope.nativeEffort:undefined):scope.fixedEffort??e.choice;
53  return {...base,outcome:'recommended',decision:{choice:scope.pinnedModel??a.choice,effort,
54    ...(!scope.pinnedModel?{confidence:a.confidence,probabilities:a.probabilities}:{}),model:x.model}};
55}
56
src/transport.ts 36 lines
1import type {ProcessRunInit,ProcessRunResult} from 'claude-code';
2import {ENDPOINT,type Reply} from './jev.ts';
3import {bytes} from './context.ts';
4import {EVALUATOR_LIMITS} from './evaluator-limits.ts';
5export const TRANSPORT_LIMITS=EVALUATOR_LIMITS;
6export type TransportOutcome='transport_timeout'|'transport_unavailable'|'response_too_large'|'invalid_transport'|'transport_disabled';
7export class TransportError extends Error {
8  constructor(readonly outcome:TransportOutcome){super(outcome)}
9}
10export type RunProcess=(argv:readonly string[],init:ProcessRunInit)=>Promise<ProcessRunResult>;
11type Init={method?:string;headers?:Record<string,string>;body?:string};
12// Curl config quoting, NOT shell quoting. Nothing passes through a shell.
13const quote=(s:string)=>'"'+s.replace(/\\/g,'\\\\').replace(/"/g,'\\"').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\t/g,'\\t').replace(/\v/g,'\\v')+'"';
14export async function boundedFetch(run:RunProcess,url:string,init:Init):Promise<Reply>{
15  const auth=init.headers?.Authorization;
16  if(url!==ENDPOINT||init.method!=='POST'||typeof init.body!=='string'||bytes(init.body)>9000||
17    typeof auth!=='string'||!auth.startsWith('Bearer ')||auth.length>4103||/[\x00-\x1f\x7f]/.test(auth)||init.body.includes('\0'))throw new TransportError('invalid_transport');
18  const config=[`url = ${quote(ENDPOINT)}`,'request = "POST"',`header = ${quote(auth?'Authorization: '+auth:'')}`,
19    'header = "Content-Type: application/json"',`data-raw = ${quote(init.body)}`].join('\n')+'\n';
20  let result:ProcessRunResult;
21  try{
22    result=await run(['curl','--disable','--silent','--show-error','--globoff','--proto','=https','--proto-redir','=https',
23      '--max-redirs','0','--retry','0','--connect-timeout',String(TRANSPORT_LIMITS.connectSeconds),'--max-time',String(TRANSPORT_LIMITS.transferSeconds),
24      '--max-filesize',String(TRANSPORT_LIMITS.responseBytes),'--write-out','\n%{http_code}','--config','-'],
25      {stdin:config,timeoutMs:TRANSPORT_LIMITS.processMs,env:{OPENROUTER_API_KEY:''}});
26  }catch{throw new TransportError('transport_unavailable')}
27  // Never pass stderr or partial response bodies into diagnostics.
28  if(result.exitCode===28)throw new TransportError('transport_timeout');
29  if(result.exitCode===63||bytes(result.stdout)>TRANSPORT_LIMITS.responseBytes+4)throw new TransportError('response_too_large');
30  if(result.exitCode!==0)throw new TransportError('transport_unavailable');
31  const match=/\n([1-5]\d\d)$/.exec(result.stdout);
32  if(!match)throw new TransportError('invalid_transport');
33  const status=Number(match[1]);
34  return {status,ok:status>=200&&status<300,text:result.stdout.slice(0,-4)};
35}
36