analyzthis_design 2.0.0 → 2.1.0

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.
Files changed (69) hide show
  1. package/HOW-TO-USE.md +436 -0
  2. package/README.md +29 -13
  3. package/agents/cards/evolve-check.md +38 -0
  4. package/agents/manifests/evolve-check.json +16 -0
  5. package/dist/HOW-TO-USE.md +15 -3
  6. package/dist/README.md +29 -13
  7. package/dist/agents/cards/evolve-check.md +38 -0
  8. package/dist/agents/manifests/evolve-check.json +16 -0
  9. package/dist/bin/cli.js +1225 -1
  10. package/dist/lib/cache.js +111 -1
  11. package/dist/lib/chunk-executor.js +219 -1
  12. package/dist/lib/chunk-models.js +228 -1
  13. package/dist/lib/chunk-planner.js +328 -1
  14. package/dist/lib/chunk-router.js +66 -1
  15. package/dist/lib/chunk-run.js +199 -1
  16. package/dist/lib/chunk-synthesis.js +176 -1
  17. package/dist/lib/chunk-telemetry.js +88 -1
  18. package/dist/lib/collect.js +858 -1
  19. package/dist/lib/cost.js +119 -1
  20. package/dist/lib/dedup.js +167 -1
  21. package/dist/lib/deliberation.js +721 -1
  22. package/dist/lib/design-spec.js +236 -1
  23. package/dist/lib/evolution-metrics.js +197 -0
  24. package/dist/lib/evolve.js +361 -1
  25. package/dist/lib/export.js +77 -1
  26. package/dist/lib/feedback-submit.js +324 -1
  27. package/dist/lib/feedback.js +182 -1
  28. package/dist/lib/host-llm.js +251 -1
  29. package/dist/lib/install.js +301 -1
  30. package/dist/lib/knowledge.js +384 -1
  31. package/dist/lib/lessons.js +217 -1
  32. package/dist/lib/moodboard.js +563 -1
  33. package/dist/lib/orchestrator/run.js +935 -1
  34. package/dist/lib/outcome.js +193 -1
  35. package/dist/lib/platforms.js +166 -1
  36. package/dist/lib/provider.js +57 -1
  37. package/dist/lib/query-expander.js +83 -1
  38. package/dist/lib/ranker.js +105 -1
  39. package/dist/lib/reference-pack.js +221 -0
  40. package/dist/lib/research.js +143 -1
  41. package/dist/lib/retrieve.js +131 -1
  42. package/dist/lib/session.js +185 -1
  43. package/dist/lib/source-discovery.js +486 -1
  44. package/dist/lib/synthesis.js +155 -1
  45. package/dist/lib/token-gate.js +46 -1
  46. package/dist/skills/design-reference/google-fonts.csv +1924 -1924
  47. package/dist/skills/design-reference/products.csv +162 -162
  48. package/dist/skills/design-reference/schema.json +159 -0
  49. package/dist/skills/design-reference/stacks/angular.csv +1 -1
  50. package/dist/skills/design-reference/stacks/astro.csv +1 -1
  51. package/dist/skills/design-reference/stacks/laravel.csv +2 -2
  52. package/dist/skills/design-reference/stacks/threejs.csv +54 -54
  53. package/dist/skills/design-reference/styles.csv +85 -85
  54. package/dist/skills/design-reference/typography.csv +75 -74
  55. package/dist/skills/design-reference/ui-reasoning.csv +1 -1
  56. package/dist/skills/evolve-check/SKILL.md +106 -0
  57. package/package.json +8 -8
  58. package/scripts/validate-csvs.js +197 -0
  59. package/skills/design-reference/google-fonts.csv +1924 -1924
  60. package/skills/design-reference/products.csv +162 -162
  61. package/skills/design-reference/schema.json +159 -0
  62. package/skills/design-reference/stacks/angular.csv +1 -1
  63. package/skills/design-reference/stacks/astro.csv +1 -1
  64. package/skills/design-reference/stacks/laravel.csv +2 -2
  65. package/skills/design-reference/stacks/threejs.csv +54 -54
  66. package/skills/design-reference/styles.csv +85 -85
  67. package/skills/design-reference/typography.csv +75 -74
  68. package/skills/design-reference/ui-reasoning.csv +1 -1
  69. package/skills/evolve-check/SKILL.md +106 -0
@@ -1,2 +1,936 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';const fs=require('fs'),path=require('path'),os=require('os'),https=require('https'),session=require('../session'),retrieve=require('../retrieve'),cache=require('../cache'),deliberation=require('../deliberation'),lessons=require('../lessons'),queryExpander=require('../query-expander'),ranker=require('../ranker'),knowledge=require('../knowledge'),chunkRun=require('../chunk-run'),chunkModels=require('../chunk-models'),{callHostLlm,createRun,HostLlmPendingError,printDeviInstructions,findLatestRun,loadManifest:loadHostManifest}=require('../host-llm'),{resolveDefaultProvider,resolveTierProvider}=require('../provider'),synthesis=require('../synthesis'),{enforceOutputCap,enforceSystemCap}=require('../token-gate'),{resolvePackageRoot}=require('../platforms'),PACKAGE_ROOT=resolvePackageRoot(__dirname),AGENTS_DIR=path['join'](PACKAGE_ROOT,'agents'),CONFIG_FILE=path['join'](os['homedir'](),'.analyzthis_design','config.json');function loadJson(a){return JSON['parse'](fs['readFileSync'](a,'utf8'));}function loadConfig(){if(!fs['existsSync'](CONFIG_FILE))return{};try{return JSON['parse'](fs['readFileSync'](CONFIG_FILE,'utf8'));}catch{return{};}}function loadManifest(a){const b=path['join'](AGENTS_DIR,'manifests',a+'.json');if(!fs['existsSync'](b))throw new Error('Manifest\x20not\x20found:\x20'+a);return loadJson(b);}function loadSkill(a){const b=path['join'](PACKAGE_ROOT,a['system_skill']);if(!fs['existsSync'](b))return'(skill\x20missing:\x20'+a['system_skill']+')';return fs['readFileSync'](b,'utf8');}function loadCard(a,b){if(a['system_card']){const c=path['join'](PACKAGE_ROOT,a['system_card']);if(fs['existsSync'](c))return fs['readFileSync'](c,'utf8');}return loadSkill(a)['slice'](0x0,0xfa0);}const PRODUCT_KEYWORDS=['saas','b2b','b2c','dashboard','analytics','e-commerce','ecommerce','fintech','healthcare','crm','marketplace','admin','enterprise','consumer','mobile','productivity','social','education','finance'];function extractKeywords(a){const b=(a||'')['toLowerCase']();return PRODUCT_KEYWORDS['filter'](c=>b['includes'](c));}const REFERENCE_MAP={'arjun':{'file':'styles.csv','column':'Best\x20For','limit':0x3},'zara':{'file':'colors.csv','column':'Product\x20Type','limit':0x3},'meera':{'file':'products.csv','column':'Product\x20Type','limit':0x3},'noor':{'file':'ux-guidelines.csv','column':'Category','limit':0x3},'anuj':{'file':'ux-guidelines.csv','column':'Category','limit':0x3},'priya':{'file':'react-performance.csv','column':'Category','limit':0x3}};function detectStack(a){if(!a||!fs['existsSync'](path['join'](a,'Tech')))return null;const b=fs['readdirSync'](path['join'](a,'Tech'))['map'](d=>d['toLowerCase']()),c=['nextjs','react','shadcn','vue','angular','svelte','nuxtjs','astro'];for(const d of c){if(b['some'](e=>e['includes'](d)))return d;}return null;}function getReferenceSpec(a,b){const c=REFERENCE_MAP[a];if(!c)return null;if(a==='priya'){const d=detectStack(b['vault_path']);if(d)return{'file':'stacks/'+d+'.csv','column':'Category','limit':0x3};}return c;}async function buildReferencePack(a,b,c){const d=getReferenceSpec(a,c);if(!d)return null;let e=extractKeywords(b);try{const f=await queryExpander['expandQuery']({'task':b,'personaId':a,'callLlmFn':callLlm,'provider':'host','model':'devi'});f&&f['terms']&&f['terms']['length']&&(e=f['terms']);}catch{}if(!e['length'])return null;try{const g=retrieve['retrieve']({'file':d['file'],'filters':[{'column':d['column'],'anyOf':e}],'limit':0xa});if(!g['rows']['length'])return null;const h=[];for(const i of g['rows']){h['push']({'type':'reference','source':d['file'],'row':i['__no'],'content':i[d['column']]||''});}try{const j=knowledge['getPersonaSliceForPrompt'](c,a);for(const k of j['slice'](0x0,0x5)){h['push']({'type':'knowledge','source':k['title'],'row':0x0,'content':k['content']['slice'](0x0,0xc8)});}}catch{}try{const l=await ranker['rankCandidates']({'personaId':a,'task':b,'candidates':h,'callLlmFn':callLlm,'provider':'host','model':'devi'});return{'cacheHit':g['cacheHit'],'citations':l['ranked']['slice'](0x0,0x5)['map'](m=>'['+m['source']+(m['row']?',\x20row\x20'+m['row']:'')+':\x20\x22'+m['content']+'\x22]')};}catch{return{'cacheHit':g['cacheHit'],'citations':g['rows']['slice'](0x0,0x5)['map'](m=>retrieve['cite'](d['file'],m,d['column']))};}}catch{return null;}}function classifyProblems(a){const b=loadJson(path['join'](AGENTS_DIR,'router.json')),c=(a||'')['toLowerCase'](),d=b['rules']['filter'](e=>e['signals']['some'](f=>c['includes'](f['toLowerCase']())));if(d['length']===0x0){const e=b['rules']['find'](f=>f['problem_type']==='full_screen_review')||b['rules'][0x0];return[e];}return d;}function classifyProblem(a){return classifyProblems(a)[0x0];}function resolveExpertsForRules(a,b,{mode:mode='lite',explicitExperts:explicitExperts=null}={}){if(explicitExperts&&explicitExperts['length'])return{'experts':explicitExperts['filter'](h=>fs['existsSync'](path['join'](AGENTS_DIR,'manifests',h+'.json'))),'full_chain':![],'ideation_chain':![]};const c=a['some'](h=>h['problem_type']==='ideation_wireframe'||(h['route_to']||[])['includes']('ideation_chain'));if(c){const h=new Set(),i=[];for(const j of b['ideation_chain']||[]){!h['has'](j['persona'])&&(h['add'](j['persona']),i['push'](j['persona']));}return{'experts':i['filter'](k=>fs['existsSync'](path['join'](AGENTS_DIR,'manifests',k+'.json'))),'full_chain':![],'ideation_chain':!![]};}const d=a['some'](k=>k['problem_type']==='full_screen_review'||(k['route_to']||[])['includes']('design-critic_chain'));if(d&&mode==='full')return{'experts':b['default_chain']['map'](k=>k['persona']),'full_chain':!![],'ideation_chain':![]};const e=[],f=new Set();for(const k of a){for(const l of k['never_route_to']||[])f['add'](l);}for(const m of a){const n=d&&mode!=='full'?['arjun','meera']:m['route_to']||[];for(const o of n){if(o==='design-critic_chain')continue;if(o==='ds_gate'||o==='arjun_color_system_only'){if(!e['includes']('arjun'))e['push']('arjun');}else{if(o!=='direct_noor_without_gate'&&o!=='noor_alone'){if(!e['includes'](o))e['push'](o);}}}}const g=e['filter'](p=>!f['has'](p));return{'experts':g['filter'](p=>fs['existsSync'](path['join'](AGENTS_DIR,'manifests',p+'.json'))),'full_chain':![],'ideation_chain':![]};}function resolveExperts(a){const b=loadJson(path['join'](AGENTS_DIR,'chain.json'));return resolveExpertsForRules([a],b,{'mode':'full'})['experts'];}function classifyEffort({routing:a,digest:b,manifest:c,scopedMode:d,fullChain:e,isDeltaFollowUp:f}={}){if(d)return'trivial';if(e)return'hard';if(a&&/full_screen_review/['test'](a['problem_type']))return'hard';if(a&&/stalemate/['test'](a['problem_type']))return'hard';if(b&&Array['isArray'](b['ds_at_risk'])&&b['ds_at_risk']['length'])return'hard';if(f)return'trivial';if(c&&c['tier']==='structured')return'trivial';if(c&&c['tier']==='arbitrate')return'standard';return'standard';}function resolveModel({manifest:a,effort:b,chain:c,config:d,defaultProvider:e,defaultModel:f,gateName:g}){let h=b;const i=c['gate_override'];g&&i&&(i['applies_to']||[])['includes'](g)&&(h=i['use_effort']||'hard');const j=a['effort_overrides']&&a['effort_overrides'][h]||{},k=c['effort_matrix']&&c['effort_matrix'][h]||{},l=d['orchestrator']?.['tiers']&&d['orchestrator']?.['tiers'][a['tier']]||{},m=j['provider']||k['provider']||l['provider']||e,n=j['model']||k['model']||l['model']||f,o=resolveTierProvider(m,e),p=j['max_output_tokens']||k['max_output_tokens']||d['orchestrator']?.['max_tokens']?.[a['tier']]||a['max_output_tokens'];return{'provider':o,'model':n,'maxTokens':p,'effort':h};}function ollamaJson(a,b,c,d,e){return new Promise(function(f,g){var h=a['indexOf']('https:')===0x0?require('https'):require('http'),i=new URL('/api/chat',a),j=JSON['stringify']({'model':b,'messages':[{'role':'system','content':c},{'role':'user','content':d}],'stream':![],'options':{'num_predict':e||0x4b0}}),k=h['request'](i['toString'](),{'method':'POST','headers':{'Content-Type':'application/json','Content-Length':Buffer['byteLength'](j)},'timeout':0xea60},function(l){var m=[];l['on']('data',function(n){m['push'](n);}),l['on']('end',function(){var n=Buffer['concat'](m)['toString']('utf8');try{var o=JSON['parse'](n);l['statusCode']>=0x190?g(new Error(o['error']||'Ollama\x20HTTP\x20'+l['statusCode'])):f(o);}catch(p){g(new Error('Bad\x20JSON\x20from\x20Ollama:\x20'+n['slice'](0x0,0xc8)));}});});k['on']('error',g),k['on']('timeout',function(){k['destroy'](),g(new Error('Ollama\x20timeout'));}),k['write'](j),k['end']();});}function httpsJson(a,b,c,d){return new Promise((e,f)=>{const g=JSON['stringify'](d),h=https['request']({'hostname':a,'path':b,'method':'POST','headers':{...c,'Content-Type':'application/json','Content-Length':Buffer['byteLength'](g)}},i=>{const j=[];i['on']('data',k=>j['push'](k)),i['on']('end',()=>{const k=Buffer['concat'](j)['toString']('utf8');try{const l=JSON['parse'](k);i['statusCode']>=0x190?f(new Error(l['error']?.['message']||l['message']||'HTTP\x20'+i['statusCode']+':\x20'+k['slice'](0x0,0xc8))):e(l);}catch(m){f(new Error('Bad\x20JSON\x20from\x20'+a+':\x20'+k['slice'](0x0,0xc8)));}});});h['on']('error',f),h['write'](g),h['end']();});}async function callLlm({provider:a,model:b,system:c,user:d,maxTokens:e,personaId:f,hostContext:g}){if(a==='host'){const i=await callHostLlm({'personaId':f,'system':c,'user':d,'maxTokens':e,'runContext':g});return enforceOutputCap(i,e);}let h='';if(a==='anthropic'){const j=process.env.ANTHROPIC_API_KEY;if(!j)throw new Error('ANTHROPIC_API_KEY\x20not\x20set');const k=await httpsJson('api.anthropic.com','/v1/messages',{'x-api-key':j,'anthropic-version':'2023-06-01'},{'model':b||'claude-sonnet-4-20250514','max_tokens':e||0x1000,'system':c,'messages':[{'role':'user','content':d}]});h=k['content']?.['map'](l=>l['text'])['join']('\x0a')||'';}else{if(a==='openai'){const l=process.env.OPENAI_API_KEY;if(!l)throw new Error('OPENAI_API_KEY\x20not\x20set');const m=await httpsJson('api.openai.com','/v1/chat/completions',{'Authorization':'Bearer\x20'+l},{'model':b||'gpt-4o','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=m['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='google'){const n=process.env.GEMINI_API_KEY||process.env.GOOGLE_API_KEY;if(!n)throw new Error('GEMINI_API_KEY\x20not\x20set');const o=await httpsJson('generativelanguage.googleapis.com','/v1beta/openai/chat/completions',{'Authorization':'Bearer\x20'+n},{'model':b||'gemini-2.5-flash','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=o['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='zai'){const p=process.env.ZAI_API_KEY||process.env.ZHIPU_API_KEY;if(!p)throw new Error('ZAI_API_KEY\x20not\x20set');const q=await httpsJson('open.bigmodel.cn','/api/paas/v4/chat/completions',{'Authorization':'Bearer\x20'+p},{'model':b||'glm-4.5-flash','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=q['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='ollama'){const r=process.env.OLLAMA_BASE_URL||'http://localhost:11434',s=await ollamaJson(r,b||'llama3.1',c,d,e);h=s['message']?.['content']||s['response']||'';}else{if(a==='groq'){const t=process.env.GROQ_API_KEY;if(!t)throw new Error('GROQ_API_KEY\x20not\x20set');const u=await httpsJson('api.groq.com','/openai/v1/chat/completions',{'Authorization':'Bearer\x20'+t},{'model':b||'llama-3.1-8b-instant','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=u['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='together'){const v=process.env.TOGETHER_API_KEY;if(!v)throw new Error('TOGETHER_API_KEY\x20not\x20set');const w=await httpsJson('api.together.xyz','/v1/chat/completions',{'Authorization':'Bearer\x20'+v},{'model':b||'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=w['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='openrouter'){const x=process.env.OPENROUTER_API_KEY;if(!x)throw new Error('OPENROUTER_API_KEY\x20not\x20set');const y=await httpsJson('openrouter.ai','/api/v1/chat/completions',{'Authorization':'Bearer\x20'+x,'HTTP-Referer':'https://github.com/joshirishi/analyzthis_design','X-Title':'analyzthis_design'},{'model':b||'meta-llama/llama-3.1-8b-instruct:free','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=y['choices']?.[0x0]?.['message']?.['content']||'';}else{if(a==='deepseek'){const z=process.env.DEEPSEEK_API_KEY;if(!z)throw new Error('DEEPSEEK_API_KEY\x20not\x20set');const A=await httpsJson('api.deepseek.com','/chat/completions',{'Authorization':'Bearer\x20'+z},{'model':b||'deepseek-chat','max_tokens':e||undefined,'messages':[{'role':'system','content':c},{'role':'user','content':d}]});h=A['choices']?.[0x0]?.['message']?.['content']||'';}else throw new Error('Unknown\x20provider\x20\x22'+a+'\x22.\x20Use\x20anthropic,\x20openai,\x20google,\x20zai,\x20ollama,\x20groq,\x20together,\x20openrouter,\x20deepseek,\x20or\x20host.');}}}}}}}}return enforceOutputCap(h,e);}async function runUnchunked(a={}){const {task:b,figma:figma='',dryRun:dryRun=![],project:c,output:d,experts:e=null,noDeliberate:noDeliberate=![],deliberate:deliberate=null,maxRounds:maxRounds=null,satisfaction:satisfaction=null,continueRun:continueRun=![],hostResponder:hostResponder=null}=a;if(!b)throw new Error('--task\x20is\x20required');const f=loadConfig(),g=deliberation['loadDeliberationConfig'](f),h=resolveDefaultProvider(f,a['provider']||f['orchestrator']?.['provider']||'auto'),i=a['model']||f['orchestrator']?.['model'],j=f['orchestrator']?.['mode']||'lite',k=a['full']?'full':a['lite']?'lite':j,l=noDeliberate?![]:deliberate!=null?deliberate:g['default_mode']!=='legacy';let m=session['show']({'project':c});if(!m){const O=session['init']({'project':c});m=session['show']({'project':O['projectId']});}const n=m['project_id'];let o=null;if(h==='host'&&!dryRun){if(continueRun&&m['host_run']?.['run_dir']){const P=loadHostManifest(m['host_run']['run_dir']);o={'runDir':m['host_run']['run_dir'],'manifest':P,'hostResponder':hostResponder};}else{if(!continueRun){const Q=createRun({'projectId':n,'task':b});o={'runDir':Q['runDir'],'manifest':Q['manifest'],'hostResponder':hostResponder},session['update']({'project':n,'patch':{'host_run':{'run_id':Q['runId'],'run_dir':Q['runDir'],'task':b,'status':'in_progress','started_at':new Date()['toISOString']()}}});}}}const p=loadJson(path['join'](AGENTS_DIR,'chain.json')),q=classifyProblems(b),{experts:r,full_chain:s,ideation_chain:t}=resolveExpertsForRules(q,p,{'mode':k,'explicitExperts':e}),u=[...new Set(q['flatMap'](R=>R['never_route_to']||[]))],v={'problem_type':q['map'](R=>R['problem_type'])['join']('+'),'experts':r,'never_route_to':u,'reason':e&&e['length']?'Explicit\x20--experts\x20override:\x20'+e['join'](',\x20'):'Matched\x20'+q['length']+'\x20rule(s)\x20['+q['map'](R=>R['problem_type'])['join'](',\x20')+']\x20in\x20\x22'+k+'\x22\x20mode'},w=Object['entries'](m['ds_checklist']||{})['filter'](([,R])=>R===![])['map'](([R])=>R),x=deliberation['getDeliberationGroups'](p,{'full_chain':s,'ideation_chain':t,'experts':r,'mode':k});session['update']({'project':n,'patch':{'routing_decision':v,'figma_node':figma?{'url':figma,'confirmed':![]}:m['figma_node'],'mode':'assess_only','task_map':m['task_map']?.['length']?m['task_map']:[{'task':b,'frequency':'unknown','priority':'P0'}],'digest':{...m['digest']||{},'task_map_summary':b['slice'](0x0,0xf0),'ds_at_risk':w,'experts':r,'mode':'assess_only'}}});const y={'project_id':n,'task':b,'figma':figma,'mode':k,'routing':v,'chain':r,'full_chain':s,'ideation_chain':t,'deliberation':l?{'mode':'adversarial','groups':x,'max_rounds':maxRounds??g['max_rounds'],'satisfaction_threshold':satisfaction??g['satisfaction_threshold']}:{'mode':'legacy'},'dry_run':dryRun,'provider':dryRun?null:h,'host_mode':h==='host'};if(dryRun)return console['log']('\x0a──\x20Dry\x20run\x20(no\x20LLM\x20calls)\x20──────────────────────────'),console['log'](JSON['stringify'](y,null,0x2)),console['log'](''),y;const z=k!=='full'&&!t,A=new Set(['noor','anuj']),B=!!(m['digest']&&m['digest']['prior_scores']&&Object['keys'](m['digest']['prior_scores']||{})['length']);let C=0x0,D=0x0;async function E(R,{isProduceRound:S,isObjectionRound:T}){const U=loadManifest(R),V=t&&A['has'](R),W=T&&!S,X=W?loadCard(U,R):s||V||S?loadSkill(U)['slice'](0x0,0x2ee0):loadCard(U,R),Y=W||z&&!V,Z=T?'\x0aDELIBERATION\x20REVIEW\x20MODE:\x20Read\x20prior\x20outputs\x20critically.\x20Low\x20satisfaction\x20default.\x20'+deliberation['DELIBERATION_JSON_HINT']:'',a0=['You\x20are\x20the\x20\x22'+R+'\x22\x20persona.\x20Follow\x20the\x20card/skill\x20instructions\x20exactly.','Allowed\x20jobs:\x20'+(U['allowed_jobs']||[])['join'](';\x20'),'Forbidden\x20jobs:\x20'+(U['forbidden_jobs']||[])['join'](';\x20'),'Assess-only\x20mode\x20is\x20ON\x20—\x20do\x20not\x20propose\x20code\x20edits\x20as\x20applied;\x20output\x20critique\x20only.',Y?'Use\x20the\x20LITE\x20objection\x20schema\x20—\x20short\x20grounding,\x20objections,\x20questions,\x20JSON\x20block.':S?'Use\x20the\x20DEEP\x20output\x20schema\x20(full\x20blocks\x20as\x20in\x20your\x20SKILL.md)\x20plus\x20deliberation\x20JSON.':'REVIEW\x20MODE:\x20object\x20to\x20prior\x20claims\x20with\x20evidence;\x20include\x20deliberation\x20JSON\x20block.',Z,'',X],a1=W?enforceSystemCap(a0['join']('\x0a'),0x157c):a0['join']('\x0a'),a2=classifyEffort({'routing':v,'digest':m['digest'],'manifest':U,'scopedMode':null,'fullChain':s,'isDeltaFollowUp':B}),a3=resolveModel({'manifest':U,'effort':T?'trivial':a2,'chain':p,'config':f,'defaultProvider':h,'defaultModel':i,'gateName':null});let a4=a1;try{const a5=lessons['retrieveLessons']({'task':b,'personaId':R,'limit':0x3});a5&&a5['lessons']&&a5['lessons']['length']&&(a4=a4+'\x0a\x0a'+lessons['buildLessonsInjection'](a5['lessons']));}catch{}return{'system':a4,'manifest':U,'effort':a3['effort'],'callProvider':a3['provider'],'callModel':a3['model'],'maxTokens':a3['maxTokens']};}function F(R,S){const T={'grades':{},'score':null,'top_fixes':[]},U=R['match'](/(\d+(?:\.\d+)?)\s*\/\s*5/);if(U)T['score']=Number(U[0x1]);const V=R['match'](/(\w+)\[([A-F])\]/gi);if(V)for(const X of V){const Y=X['match'](/(\w+)\[([A-F])\]/i);if(Y)T['grades'][Y[0x1]['toLowerCase']()]=Y[0x2]['toUpperCase']();}const W=R['match'](/Top\s*2\s*fixes:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)/i);return W&&(T['top_fixes']=[W[0x1]['trim'](),W[0x2]['trim']()]),T;}async function G(R,S,T,U){console['log']('\x0a⏳\x20Running\x20persona:\x20'+R+'...');const V=await callLlm({'provider':U['provider'],'model':U['model'],'system':S,'user':T,'maxTokens':U['maxTokens'],'personaId':R,'hostContext':o}),W=Math['ceil']((S['length']+T['length'])/0x4),X=Math['ceil'](V['length']/0x4),Y=f['pricing']&&f['pricing'][U['model']];return Y&&(D+=W/0xf4240*(Y['input_per_m']||0x0)+X/0xf4240*(Y['output_per_m']||0x0)),console['log']('✅\x20'+R+'\x20complete\x20('+V['length']+'\x20chars,\x20'+(U['effort']||'standard')+')'),{'text':V,'input_tokens_est':W,'output_tokens_est':X,'structured_output':F(V,R),'full_prompt':{'system':S,'user':T},'effort_log_entry':{'persona':R,'effort':U['effort']||'standard','model':U['model'],'scoped_mode':null}};}m=session['show']({'project':n})||m;let H;try{H=await deliberation['runDeliberation']({'chain':p,'experts':r,'full_chain':s,'ideation_chain':t,'mode':k,'task':b,'routing':v,'state':m,'figma':figma,'config':f,'callPersona':G,'loadPersonaContext':E,'buildReferencePack':async(R,S)=>{const T=await buildReferencePack(R,S,m);if(T?.['cacheHit'])C+=0x1;return T;},'noDeliberate':!l,'satisfaction_threshold':satisfaction!=null?Number(satisfaction):undefined,'max_rounds':maxRounds!=null?Number(maxRounds):undefined,'resume_checkpoint':continueRun?m['host_run']?.['checkpoint']:null});}catch(R){if(R['name']==='HostLlmPendingError'){session['update']({'project':n,'patch':{'persona_outputs':R['checkpoint']?.['persona_outputs']||m['persona_outputs'],'host_run':{...m['host_run']||{},'status':'pending_devi','last_pending':{'step_id':R['stepId'],'persona':R['personaId']},'checkpoint':R['checkpoint']||null}}}),printDeviInstructions(R);throw R;}throw R;}const I=synthesis['buildSynthesis'](H['persona_outputs'],{'deliberation':H['deliberation']}),J=figma?{'primary_task':'not_run','screenshots':[],'reason':'browser\x20automation\x20unavailable\x20in\x20standalone\x20runtime'}:{'primary_task':'not_run','screenshots':[],'reason':'assess_only,\x20no\x20URL'},K=Object['keys'](H['persona_outputs']),L={},M={};for(const [S,T]of Object['entries'](H['persona_outputs']||{})){if(T['full_prompt'])L[S]=T['full_prompt'];if(T['structured_output'])M[S]=T['structured_output'];}const N=session['update']({'project':n,'patch':{'persona_outputs':H['persona_outputs'],'full_prompts':L,'structured_outputs':M,'covered_points':H['covered_points']||[],'task_type':m['task_type']||classifyProblem(b)?.['problem_type']||'','deliberation':H['deliberation'],'synthesis':I['composite'],'synthesis_markdown':I['markdown'],'verify_results':J,'host_run':h==='host'?{...m['host_run']||{},'status':'complete','completed_at':new Date()['toISOString'](),'checkpoint':null}:m['host_run'],'digest':{...m['digest']||{},...H['digest_patch'],'experts':r},'metrics':{'llm_calls':H['metrics']['llm_calls'],'experts_run':K,'input_tokens_est':H['metrics']['input_tokens_est'],'output_tokens_est':H['metrics']['output_tokens_est'],'cache_hits':C,'mode':k,'effort_log':H['metrics']['effort_log'],'cost_usd':Number(D['toFixed'](0x6)),'deliberation_rounds':H['metrics']['deliberation_rounds'],'objections_raised':H['metrics']['objections_raised'],'objections_resolved':H['metrics']['objections_resolved'],'raj_escalations':H['metrics']['raj_escalations']}}});d&&(fs['writeFileSync'](path['resolve'](d),JSON['stringify'](N,null,0x2)),console['log']('\x0a✅\x20Wrote\x20session\x20state\x20to\x20'+path['resolve'](d)));console['log']('\x0a✅\x20Orchestrator\x20run\x20complete.\x20Experts\x20('+k+'):\x20'+r['join'](',\x20')),console['log']('\x20\x20\x20Provider:\x20'+h+(h==='host'?'\x20(Devi\x20host\x20bridge)':'')),console['log']('\x20\x20\x20Deliberation:\x20'+H['deliberation']['mode']+',\x20rounds='+H['metrics']['deliberation_rounds']+',\x20consensus='+H['deliberation']['consensus_reached']);if(H['deliberation']['raj_escalated'])console['log']('\x20\x20\x20Raj\x20escalated:\x20yes');console['log']('\x20\x20\x20Verdict:\x20'+I['composite']['verdict']+'\x20('+I['composite']['total']+'/'+I['composite']['max_total']+')'),console['log']('\x20\x20\x20Est.\x20tokens:\x20'+H['metrics']['input_tokens_est']+'\x20in\x20/\x20'+H['metrics']['output_tokens_est']+'\x20out');if(D)console['log']('\x20\x20\x20Est.\x20cost:\x20\x20\x20$'+D['toFixed'](0x4));return console['log']('\x20\x20\x20Session:\x20'+session['sessionPath'](n)),console['log']('\x0a──\x20Synthesis\x20──────────────────────────────────────'),console['log'](I['markdown']),console['log'](''),N;}async function run(a={}){if(a['unchunked'])return runUnchunked(a);const b=await chunkRun['runChunked'](a);if(b['mode']==='host_pending'||b['host_pending'])return b;if(b['mode']==='chunked_dry_run'||a['dryRun'])return b;const c=b['chunk_run']||b,d=c['synthesis'];console['log']('\x0a✅\x20Chunked\x20orchestrator\x20run\x20complete.');c['planner_warning']&&console['log']('\x20\x20\x20⚠\x20Planner:\x20'+c['planner_warning']);c['planner_model']&&console['log']('\x20\x20\x20Planner:\x20'+chunkModels['formatModelName'](c['planner_model']));console['log']('\x20\x20\x20Chunks:\x20\x20'+(c['results']||[])['length']),console['log']('\x20\x20\x20Failed:\x20\x20'+(c['failed_chunks']||[])['length']);c['models_used']&&c['models_used']['length']&&console['log']('\x20\x20\x20Models:\x20\x20'+c['models_used']['join'](',\x20'));console['log']('\x20\x20\x20Cost:\x20\x20\x20\x20$'+(c['total_cost']||0x0)['toFixed'](0x4));const e=c['total_tokens']||{'input':0x0,'output':0x0};return console['log']('\x20\x20\x20Tokens:\x20\x20'+e['input']+'\x20in\x20/\x20'+e['output']+'\x20out'),d&&console['log']('\x20\x20\x20Verdict:\x20'+(d['verdict']||'n/a')+'\x20('+(d['total']||0x0)+'/'+(d['max_total']||0x0)+')'),console['log']('\x20\x20\x20Session:\x20'+session['sessionPath'](b['project_id']||session['getProjectId']())),b['session'];}module['exports']={'run':run,'runUnchunked':runUnchunked,'classifyProblem':classifyProblem,'classifyProblems':classifyProblems,'resolveExperts':resolveExperts,'resolveExpertsForRules':resolveExpertsForRules,'loadManifest':loadManifest,'classifyEffort':classifyEffort,'resolveModel':resolveModel,'callLlm':callLlm,'loadConfig':loadConfig};if(require['main']===module){const args=process['argv']['slice'](0x2),get=a=>{const b=args['find'](d=>d['startsWith']('--'+a+'='));if(b)return b['split']('=')['slice'](0x1)['join']('=');const c=args['indexOf']('--'+a);if(c!==-0x1&&args[c+0x1]&&!args[c+0x1]['startsWith']('--'))return args[c+0x1];return null;},expertsArg=get('experts');run({'task':get('task'),'figma':get('figma')||'','provider':get('provider'),'model':get('model'),'dryRun':args['includes']('--dry-run'),'lite':args['includes']('--lite'),'full':args['includes']('--full'),'noDeliberate':args['includes']('--no-deliberate'),'deliberate':args['includes']('--deliberate')?!![]:null,'maxRounds':get('max-rounds'),'satisfaction':get('satisfaction'),'continueRun':args['includes']('--continue'),'experts':expertsArg?expertsArg['split'](',')['map'](a=>a['trim']())['filter'](Boolean):null,'project':get('project'),'output':get('output')})['catch'](a=>{console['error']('\x0a\x20\x20✗\x20\x20'+a['message']+'\x0a'),process['exit'](0x1);});}
2
+ 'use strict';
3
+
4
+ /**
5
+ * Standalone persona orchestrator runtime (v2).
6
+ * Loads agents/ manifests + router + chain, optionally calls an LLM per persona step.
7
+ *
8
+ * Usage:
9
+ * npx analyzthis_design run --task "..." [--figma URL] [--provider anthropic|openai|google|zai] [--dry-run]
10
+ * npx analyzthis_design run --task "..." --lite # force MoE subset only (default)
11
+ * npx analyzthis_design run --task "..." --full # allow the full design-critic chain
12
+ * npx analyzthis_design run --task "..." --deliberate --max-rounds 3 --satisfaction 0.4
13
+ * npx analyzthis_design run --task "..." --no-deliberate # legacy sequential handoff
14
+ *
15
+ * Env / config (never committed):
16
+ * ANTHROPIC_API_KEY | OPENAI_API_KEY | GEMINI_API_KEY (or GOOGLE_API_KEY) | ZAI_API_KEY (or ZHIPU_API_KEY)
17
+ * ~/.analyzthis_design/config.json → {
18
+ * "orchestrator": {
19
+ * "provider": "anthropic",
20
+ * "model": "...",
21
+ * "mode": "lite" | "full",
22
+ * "tiers": { "extract": {...}, "structured": {...}, "critique": {...}, "arbitrate": {...} },
23
+ * "max_tokens": { "structured": 900, "critique": 1800, "arbitrate": 1200 }
24
+ * },
25
+ * "pricing": { "<model-id>": { "input_per_m": 0.30, "output_per_m": 2.50 } }
26
+ * }
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const os = require('os');
32
+ const https = require('https');
33
+ const session = require('../session');
34
+ const retrieve = require('../retrieve');
35
+ const cache = require('../cache');
36
+ const deliberation = require('../deliberation');
37
+ const lessons = require('../lessons');
38
+ const queryExpander = require('../query-expander');
39
+ const ranker = require('../ranker');
40
+ const knowledge = require('../knowledge');
41
+ const chunkRun = require('../chunk-run');
42
+ const chunkModels = require('../chunk-models');
43
+ const referencePack = require('../reference-pack');
44
+ const { callHostLlm, createRun, HostLlmPendingError, printDeviInstructions, findLatestRun, loadManifest: loadHostManifest } = require('../host-llm');
45
+ const { resolveDefaultProvider, resolveTierProvider } = require('../provider');
46
+ const synthesis = require('../synthesis');
47
+ const { enforceOutputCap, enforceSystemCap } = require('../token-gate');
48
+
49
+ // Resolve package root whether we are running from source (lib/) or published (dist/lib/)
50
+ const { resolvePackageRoot } = require('../platforms');
51
+ const PACKAGE_ROOT = resolvePackageRoot(__dirname);
52
+ const AGENTS_DIR = path.join(PACKAGE_ROOT, 'agents');
53
+ const CONFIG_FILE = path.join(os.homedir(), '.analyzthis_design', 'config.json');
54
+
55
+ function loadJson(filePath) {
56
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
57
+ }
58
+
59
+ function loadConfig() {
60
+ if (!fs.existsSync(CONFIG_FILE)) return {};
61
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
62
+ catch { return {}; }
63
+ }
64
+
65
+ function loadManifest(id) {
66
+ const p = path.join(AGENTS_DIR, 'manifests', `${id}.json`);
67
+ if (!fs.existsSync(p)) throw new Error(`Manifest not found: ${id}`);
68
+ return loadJson(p);
69
+ }
70
+
71
+ function loadSkill(manifest) {
72
+ const skillPath = path.join(PACKAGE_ROOT, manifest.system_skill);
73
+ if (!fs.existsSync(skillPath)) return `(skill missing: ${manifest.system_skill})`;
74
+ return fs.readFileSync(skillPath, 'utf8');
75
+ }
76
+
77
+ /**
78
+ * Load a persona's short card (agents/cards/<id>.md) instead of its full
79
+ * SKILL.md. Falls back to a slice of the full skill if no card exists yet.
80
+ */
81
+ function loadCard(manifest, id) {
82
+ if (manifest.system_card) {
83
+ const cardPath = path.join(PACKAGE_ROOT, manifest.system_card);
84
+ if (fs.existsSync(cardPath)) return fs.readFileSync(cardPath, 'utf8');
85
+ }
86
+ return loadSkill(manifest).slice(0, 4000);
87
+ }
88
+
89
+ // ─── Retrieve-on-demand reference rows (Phase 2.3) ──────────────────────────
90
+ // buildReferencePack, REFERENCE_MAP, extractKeywords, detectStack, getReferenceSpec
91
+ // are now in lib/reference-pack.js (shared with chunk-executor).
92
+ // Thin wrapper kept here for backward compat with deliberation.js callbacks.
93
+
94
+ async function buildReferencePack(id, task, state) {
95
+ return referencePack.buildReferencePack(id, task, state, callLlm);
96
+ }
97
+
98
+ /**
99
+ * Classify a task against every router rule whose signals hit — not just the
100
+ * first match. Multiple rules can legitimately apply to one ask (e.g. a task
101
+ * that mentions both "contrast" and "onboarding"), and only merging all of
102
+ * them avoids silently dropping an expert that a single-match router would miss.
103
+ */
104
+ function classifyProblems(task) {
105
+ const router = loadJson(path.join(AGENTS_DIR, 'router.json'));
106
+ const lower = (task || '').toLowerCase();
107
+ const matched = router.rules.filter((rule) =>
108
+ rule.signals.some((s) => lower.includes(s.toLowerCase())));
109
+ if (matched.length === 0) {
110
+ const fallback = router.rules.find((r) => r.problem_type === 'full_screen_review') || router.rules[0];
111
+ return [fallback];
112
+ }
113
+ return matched;
114
+ }
115
+
116
+ // Back-compat single-rule accessor (first match) — prefer classifyProblems for new code.
117
+ function classifyProblem(task) {
118
+ return classifyProblems(task)[0];
119
+ }
120
+
121
+ /**
122
+ * Merge experts across every matched rule, then subtract the union of every
123
+ * matched rule's never_route_to. A persona explicitly excluded by any matched
124
+ * rule stays excluded even if another matched rule would have included it —
125
+ * exclusions are a stronger signal than inclusions (e.g. "never Zara for contrast").
126
+ */
127
+ function resolveExpertsForRules(rules, chain, { mode = 'lite', explicitExperts = null } = {}) {
128
+ if (explicitExperts && explicitExperts.length) {
129
+ return {
130
+ experts: explicitExperts.filter((id) => fs.existsSync(path.join(AGENTS_DIR, 'manifests', `${id}.json`))),
131
+ full_chain: false,
132
+ ideation_chain: false,
133
+ };
134
+ }
135
+
136
+ const isIdeation = rules.some((r) =>
137
+ r.problem_type === 'ideation_wireframe' || (r.route_to || []).includes('ideation_chain'));
138
+
139
+ if (isIdeation) {
140
+ const seen = new Set();
141
+ const experts = [];
142
+ for (const step of chain.ideation_chain || []) {
143
+ if (!seen.has(step.persona)) {
144
+ seen.add(step.persona);
145
+ experts.push(step.persona);
146
+ }
147
+ }
148
+ return {
149
+ experts: experts.filter((id) => fs.existsSync(path.join(AGENTS_DIR, 'manifests', `${id}.json`))),
150
+ full_chain: false,
151
+ ideation_chain: true,
152
+ };
153
+ }
154
+
155
+ const isFullScreenReview = rules.some((r) =>
156
+ r.problem_type === 'full_screen_review' || (r.route_to || []).includes('design-critic_chain'));
157
+
158
+ if (isFullScreenReview && mode === 'full') {
159
+ return { experts: chain.default_chain.map((s) => s.persona), full_chain: true, ideation_chain: false };
160
+ }
161
+
162
+ const experts = [];
163
+ const neverSet = new Set();
164
+ for (const rule of rules) {
165
+ for (const token of rule.never_route_to || []) neverSet.add(token);
166
+ }
167
+
168
+ for (const rule of rules) {
169
+ // In lite mode (the default), a full_screen_review match still resolves to
170
+ // a bounded MoE subset (Arjun + Meera) rather than the entire default_chain.
171
+ const routeTo = isFullScreenReview && mode !== 'full' ? ['arjun', 'meera'] : (rule.route_to || []);
172
+ for (const token of routeTo) {
173
+ if (token === 'design-critic_chain') continue;
174
+ if (token === 'ds_gate' || token === 'arjun_color_system_only') {
175
+ if (!experts.includes('arjun')) experts.push('arjun');
176
+ } else if (token !== 'direct_noor_without_gate' && token !== 'noor_alone') {
177
+ if (!experts.includes(token)) experts.push(token);
178
+ }
179
+ }
180
+ }
181
+
182
+ const filtered = experts.filter((id) => !neverSet.has(id));
183
+ return {
184
+ experts: filtered.filter((id) => fs.existsSync(path.join(AGENTS_DIR, 'manifests', `${id}.json`))),
185
+ full_chain: false,
186
+ ideation_chain: false,
187
+ };
188
+ }
189
+
190
+ // Back-compat single-rule accessor.
191
+ function resolveExperts(rule) {
192
+ const chain = loadJson(path.join(AGENTS_DIR, 'chain.json'));
193
+ return resolveExpertsForRules([rule], chain, { mode: 'full' }).experts;
194
+ }
195
+
196
+ // ─── Effort-graded model selection (v1.10) ──────────────────────────────────
197
+
198
+ /**
199
+ * Classify the effort a sub-task needs, using only cheap signals already
200
+ * available in routing + session digest + the persona manifest. No LLM call —
201
+ * an LLM call to pick a model would eat the savings this whole layer exists
202
+ * to capture. First matching rule wins, so the order below is the priority.
203
+ *
204
+ * @returns {"trivial"|"standard"|"hard"}
205
+ */
206
+ function classifyEffort({ routing, digest, manifest, scopedMode, fullChain, isDeltaFollowUp } = {}) {
207
+ // Safety-first rules: anything risky escalates to hard before any savings rule.
208
+ if (scopedMode) return 'trivial'; // scoped mode = single dimension by construction
209
+ if (fullChain) return 'hard'; // explicit full audit
210
+ if (routing && /full_screen_review/.test(routing.problem_type)) return 'hard';
211
+ if (routing && /stalemate/.test(routing.problem_type)) return 'hard';
212
+ if (digest && Array.isArray(digest.ds_at_risk) && digest.ds_at_risk.length) return 'hard';
213
+ // Savings rules: deltas and structured work don't need a frontier model.
214
+ if (isDeltaFollowUp) return 'trivial'; // REVISE → re-grade only flagged sections
215
+ if (manifest && manifest.tier === 'structured') return 'trivial';
216
+ if (manifest && manifest.tier === 'arbitrate') return 'standard';
217
+ return 'standard';
218
+ }
219
+
220
+ /**
221
+ * Resolve the (provider, model, maxTokens) for one persona call from the
222
+ * effort matrix, with persona-level overrides winning over the global matrix
223
+ * and the legacy tiers map as the final fallback so existing manifests keep
224
+ * working unchanged. Gates are pinned to gate_override.use_effort.
225
+ *
226
+ * Resolution order (first hit wins):
227
+ * 1. manifest.effort_overrides[effort] (persona-specific)
228
+ * 2. chain.effort_matrix[effort] (global matrix, v1.10)
229
+ * 3. chain.tiers[manifest.tier] (legacy fallback, v1.9)
230
+ * 4. manifest.max_output_tokens (last resort for the cap)
231
+ */
232
+ function resolveModel({ manifest, effort, chain, config, defaultProvider, defaultModel, gateName }) {
233
+ let effKey = effort;
234
+ const gateOverride = chain.gate_override;
235
+ if (gateName && gateOverride && (gateOverride.applies_to || []).includes(gateName)) {
236
+ effKey = gateOverride.use_effort || 'hard';
237
+ }
238
+
239
+ const override = (manifest.effort_overrides && manifest.effort_overrides[effKey]) || {};
240
+ const matrix = (chain.effort_matrix && chain.effort_matrix[effKey]) || {};
241
+ const legacy = (config.orchestrator?.tiers && config.orchestrator?.tiers[manifest.tier]) || {};
242
+
243
+ const provider = override.provider || matrix.provider || legacy.provider || defaultProvider;
244
+ const model = override.model || matrix.model || legacy.model || defaultModel;
245
+ const resolvedProvider = resolveTierProvider(provider, defaultProvider);
246
+ const maxTokens = override.max_output_tokens || matrix.max_output_tokens
247
+ || config.orchestrator?.max_tokens?.[manifest.tier] || manifest.max_output_tokens;
248
+ return { provider: resolvedProvider, model, maxTokens, effort: effKey };
249
+ }
250
+
251
+ function ollamaJson(baseUrl, model, system, user, maxTokens) {
252
+ return new Promise(function(resolve, reject) {
253
+ var lib = baseUrl.indexOf('https:') === 0 ? require('https') : require('http');
254
+ var url = new URL('/api/chat', baseUrl);
255
+ var body = JSON.stringify({
256
+ model: model,
257
+ messages: [
258
+ { role: 'system', content: system },
259
+ { role: 'user', content: user },
260
+ ],
261
+ stream: false,
262
+ options: { num_predict: maxTokens || 1200 },
263
+ });
264
+ var req = lib.request(url.toString(), {
265
+ method: 'POST',
266
+ headers: {
267
+ 'Content-Type': 'application/json',
268
+ 'Content-Length': Buffer.byteLength(body),
269
+ },
270
+ timeout: 60000,
271
+ }, function(res) {
272
+ var chunks = [];
273
+ res.on('data', function(c) { chunks.push(c); });
274
+ res.on('end', function() {
275
+ var raw = Buffer.concat(chunks).toString('utf8');
276
+ try {
277
+ var json = JSON.parse(raw);
278
+ if (res.statusCode >= 400) {
279
+ reject(new Error(json.error || 'Ollama HTTP ' + res.statusCode));
280
+ } else {
281
+ resolve(json);
282
+ }
283
+ } catch (e) {
284
+ reject(new Error('Bad JSON from Ollama: ' + raw.slice(0, 200)));
285
+ }
286
+ });
287
+ });
288
+ req.on('error', reject);
289
+ req.on('timeout', function() { req.destroy(); reject(new Error('Ollama timeout')); });
290
+ req.write(body);
291
+ req.end();
292
+ });
293
+ }
294
+
295
+ function httpsJson(hostname, apiPath, headers, body) {
296
+ return new Promise((resolve, reject) => {
297
+ const data = JSON.stringify(body);
298
+ const req = https.request({
299
+ hostname,
300
+ path: apiPath,
301
+ method: 'POST',
302
+ headers: {
303
+ ...headers,
304
+ 'Content-Type': 'application/json',
305
+ 'Content-Length': Buffer.byteLength(data),
306
+ },
307
+ }, (res) => {
308
+ const chunks = [];
309
+ res.on('data', (c) => chunks.push(c));
310
+ res.on('end', () => {
311
+ const raw = Buffer.concat(chunks).toString('utf8');
312
+ try {
313
+ const json = JSON.parse(raw);
314
+ if (res.statusCode >= 400) {
315
+ reject(new Error(json.error?.message || json.message || `HTTP ${res.statusCode}: ${raw.slice(0, 200)}`));
316
+ } else {
317
+ resolve(json);
318
+ }
319
+ } catch (e) {
320
+ reject(new Error(`Bad JSON from ${hostname}: ${raw.slice(0, 200)}`));
321
+ }
322
+ });
323
+ });
324
+ req.on('error', reject);
325
+ req.write(data);
326
+ req.end();
327
+ });
328
+ }
329
+
330
+ async function callLlm({ provider, model, system, user, maxTokens, personaId, hostContext }) {
331
+ if (provider === 'host') {
332
+ const text = await callHostLlm({
333
+ personaId, system, user, maxTokens, runContext: hostContext,
334
+ });
335
+ return enforceOutputCap(text, maxTokens);
336
+ }
337
+
338
+ let text = '';
339
+
340
+ if (provider === 'anthropic') {
341
+ const key = process.env.ANTHROPIC_API_KEY;
342
+ if (!key) throw new Error('ANTHROPIC_API_KEY not set');
343
+ const json = await httpsJson('api.anthropic.com', '/v1/messages', {
344
+ 'x-api-key': key,
345
+ 'anthropic-version': '2023-06-01',
346
+ }, {
347
+ model: model || 'claude-sonnet-4-20250514',
348
+ max_tokens: maxTokens || 4096,
349
+ system,
350
+ messages: [{ role: 'user', content: user }],
351
+ });
352
+ text = json.content?.map((c) => c.text).join('\n') || '';
353
+ } else if (provider === 'openai') {
354
+ const key = process.env.OPENAI_API_KEY;
355
+ if (!key) throw new Error('OPENAI_API_KEY not set');
356
+ const json = await httpsJson('api.openai.com', '/v1/chat/completions', {
357
+ Authorization: `Bearer ${key}`,
358
+ }, {
359
+ model: model || 'gpt-4o',
360
+ max_tokens: maxTokens || undefined,
361
+ messages: [
362
+ { role: 'system', content: system },
363
+ { role: 'user', content: user },
364
+ ],
365
+ });
366
+ text = json.choices?.[0]?.message?.content || '';
367
+ } else if (provider === 'google') {
368
+ const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
369
+ if (!key) throw new Error('GEMINI_API_KEY not set');
370
+ const json = await httpsJson('generativelanguage.googleapis.com', '/v1beta/openai/chat/completions', {
371
+ Authorization: `Bearer ${key}`,
372
+ }, {
373
+ model: model || 'gemini-2.5-flash',
374
+ max_tokens: maxTokens || undefined,
375
+ messages: [
376
+ { role: 'system', content: system },
377
+ { role: 'user', content: user },
378
+ ],
379
+ });
380
+ text = json.choices?.[0]?.message?.content || '';
381
+ } else if (provider === 'zai') {
382
+ const key = process.env.ZAI_API_KEY || process.env.ZHIPU_API_KEY;
383
+ if (!key) throw new Error('ZAI_API_KEY not set');
384
+ const json = await httpsJson('open.bigmodel.cn', '/api/paas/v4/chat/completions', {
385
+ Authorization: `Bearer ${key}`,
386
+ }, {
387
+ model: model || 'glm-4.5-flash',
388
+ max_tokens: maxTokens || undefined,
389
+ messages: [
390
+ { role: 'system', content: system },
391
+ { role: 'user', content: user },
392
+ ],
393
+ });
394
+ text = json.choices?.[0]?.message?.content || '';
395
+ } else if (provider === 'ollama') {
396
+ const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
397
+ const json = await ollamaJson(baseUrl, model || 'llama3.1', system, user, maxTokens);
398
+ text = json.message?.content || json.response || '';
399
+ } else if (provider === 'groq') {
400
+ const key = process.env.GROQ_API_KEY;
401
+ if (!key) throw new Error('GROQ_API_KEY not set');
402
+ const json = await httpsJson('api.groq.com', '/openai/v1/chat/completions', {
403
+ Authorization: `Bearer ${key}`,
404
+ }, {
405
+ model: model || 'llama-3.1-8b-instant',
406
+ max_tokens: maxTokens || undefined,
407
+ messages: [
408
+ { role: 'system', content: system },
409
+ { role: 'user', content: user },
410
+ ],
411
+ });
412
+ text = json.choices?.[0]?.message?.content || '';
413
+ } else if (provider === 'together') {
414
+ const key = process.env.TOGETHER_API_KEY;
415
+ if (!key) throw new Error('TOGETHER_API_KEY not set');
416
+ const json = await httpsJson('api.together.xyz', '/v1/chat/completions', {
417
+ Authorization: `Bearer ${key}`,
418
+ }, {
419
+ model: model || 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
420
+ max_tokens: maxTokens || undefined,
421
+ messages: [
422
+ { role: 'system', content: system },
423
+ { role: 'user', content: user },
424
+ ],
425
+ });
426
+ text = json.choices?.[0]?.message?.content || '';
427
+ } else if (provider === 'openrouter') {
428
+ const key = process.env.OPENROUTER_API_KEY;
429
+ if (!key) throw new Error('OPENROUTER_API_KEY not set');
430
+ const json = await httpsJson('openrouter.ai', '/api/v1/chat/completions', {
431
+ Authorization: `Bearer ${key}`,
432
+ 'HTTP-Referer': 'https://github.com/joshirishi/analyzthis_design',
433
+ 'X-Title': 'analyzthis_design',
434
+ }, {
435
+ model: model || 'meta-llama/llama-3.1-8b-instruct:free',
436
+ max_tokens: maxTokens || undefined,
437
+ messages: [
438
+ { role: 'system', content: system },
439
+ { role: 'user', content: user },
440
+ ],
441
+ });
442
+ text = json.choices?.[0]?.message?.content || '';
443
+ } else if (provider === 'deepseek') {
444
+ const key = process.env.DEEPSEEK_API_KEY;
445
+ if (!key) throw new Error('DEEPSEEK_API_KEY not set');
446
+ const json = await httpsJson('api.deepseek.com', '/chat/completions', {
447
+ Authorization: `Bearer ${key}`,
448
+ }, {
449
+ model: model || 'deepseek-chat',
450
+ max_tokens: maxTokens || undefined,
451
+ messages: [
452
+ { role: 'system', content: system },
453
+ { role: 'user', content: user },
454
+ ],
455
+ });
456
+ text = json.choices?.[0]?.message?.content || '';
457
+ } else {
458
+ throw new Error(`Unknown provider "${provider}". Use anthropic, openai, google, zai, ollama, groq, together, openrouter, deepseek, or host.`);
459
+ }
460
+
461
+ return enforceOutputCap(text, maxTokens);
462
+ }
463
+
464
+ /**
465
+ * Run the orchestrator graph.
466
+ * @param {{
467
+ * task: string, figma?: string, provider?: string, model?: string, dryRun?: boolean,
468
+ * project?: string, output?: string, lite?: boolean, full?: boolean, experts?: string[],
469
+ * continueRun?: boolean, hostResponder?: function,
470
+ * }} opts
471
+ */
472
+ async function runUnchunked(opts = {}) {
473
+ const {
474
+ task, figma = '', dryRun = false, project, output,
475
+ experts: explicitExperts = null,
476
+ noDeliberate = false,
477
+ deliberate = null,
478
+ maxRounds = null,
479
+ satisfaction = null,
480
+ continueRun = false,
481
+ hostResponder = null,
482
+ } = opts;
483
+ if (!task) throw new Error('--task is required');
484
+
485
+ const config = loadConfig();
486
+ const dConfig = deliberation.loadDeliberationConfig(config);
487
+ const provider = resolveDefaultProvider(config, opts.provider || config.orchestrator?.provider || 'auto');
488
+ const model = opts.model || config.orchestrator?.model;
489
+
490
+ const configMode = config.orchestrator?.mode || 'lite';
491
+ const mode = opts.full ? 'full' : (opts.lite ? 'lite' : configMode);
492
+
493
+ const useDeliberation = noDeliberate
494
+ ? false
495
+ : (deliberate != null ? deliberate : dConfig.default_mode !== 'legacy');
496
+
497
+ let state = session.show({ project });
498
+ if (!state) {
499
+ const init = session.init({ project });
500
+ state = session.show({ project: init.projectId });
501
+ }
502
+ const projectId = state.project_id;
503
+
504
+ // Host LLM run directory (Devi bridge — no API keys)
505
+ let hostContext = null;
506
+ if (provider === 'host' && !dryRun) {
507
+ if (continueRun && state.host_run?.run_dir) {
508
+ const manifest = loadHostManifest(state.host_run.run_dir);
509
+ hostContext = {
510
+ runDir: state.host_run.run_dir,
511
+ manifest,
512
+ hostResponder,
513
+ };
514
+ } else if (!continueRun) {
515
+ const created = createRun({ projectId, task });
516
+ hostContext = { runDir: created.runDir, manifest: created.manifest, hostResponder };
517
+ session.update({
518
+ project: projectId,
519
+ patch: {
520
+ host_run: {
521
+ run_id: created.runId,
522
+ run_dir: created.runDir,
523
+ task,
524
+ status: 'in_progress',
525
+ started_at: new Date().toISOString(),
526
+ },
527
+ },
528
+ });
529
+ }
530
+ }
531
+
532
+ const chain = loadJson(path.join(AGENTS_DIR, 'chain.json'));
533
+ const rules = classifyProblems(task);
534
+ const { experts, full_chain, ideation_chain } = resolveExpertsForRules(rules, chain, { mode, explicitExperts });
535
+ const never = [...new Set(rules.flatMap((r) => r.never_route_to || []))];
536
+
537
+ const routing = {
538
+ problem_type: rules.map((r) => r.problem_type).join('+'),
539
+ experts,
540
+ never_route_to: never,
541
+ reason: explicitExperts && explicitExperts.length
542
+ ? `Explicit --experts override: ${explicitExperts.join(', ')}`
543
+ : `Matched ${rules.length} rule(s) [${rules.map((r) => r.problem_type).join(', ')}] in "${mode}" mode`,
544
+ };
545
+
546
+ const dsAtRisk = Object.entries(state.ds_checklist || {})
547
+ .filter(([, v]) => v === false)
548
+ .map(([k]) => k);
549
+
550
+ const deliberationGroups = deliberation.getDeliberationGroups(chain, {
551
+ full_chain, ideation_chain, experts, mode,
552
+ });
553
+
554
+ session.update({
555
+ project: projectId,
556
+ patch: {
557
+ routing_decision: routing,
558
+ figma_node: figma ? { url: figma, confirmed: false } : state.figma_node,
559
+ mode: 'assess_only',
560
+ task_map: state.task_map?.length
561
+ ? state.task_map
562
+ : [{ task, frequency: 'unknown', priority: 'P0' }],
563
+ digest: {
564
+ ...(state.digest || {}),
565
+ task_map_summary: task.slice(0, 240),
566
+ ds_at_risk: dsAtRisk,
567
+ experts,
568
+ mode: 'assess_only',
569
+ },
570
+ },
571
+ });
572
+
573
+ const plan = {
574
+ project_id: projectId,
575
+ task,
576
+ figma,
577
+ mode,
578
+ routing,
579
+ chain: experts,
580
+ full_chain,
581
+ ideation_chain,
582
+ deliberation: useDeliberation ? {
583
+ mode: 'adversarial',
584
+ groups: deliberationGroups,
585
+ max_rounds: maxRounds ?? dConfig.max_rounds,
586
+ satisfaction_threshold: satisfaction ?? dConfig.satisfaction_threshold,
587
+ } : { mode: 'legacy' },
588
+ dry_run: dryRun,
589
+ provider: dryRun ? null : provider,
590
+ host_mode: provider === 'host',
591
+ };
592
+
593
+ if (dryRun) {
594
+ console.log('\n── Dry run (no LLM calls) ──────────────────────────');
595
+ console.log(JSON.stringify(plan, null, 2));
596
+ console.log('');
597
+ return plan;
598
+ }
599
+
600
+ const useLiteSchema = mode !== 'full' && !ideation_chain;
601
+ const wireframePersonas = new Set(['noor', 'anuj']);
602
+ const isDeltaFollowUp = !!(state.digest && state.digest.prior_scores && Object.keys(state.digest.prior_scores || {}).length);
603
+ let cacheHits = 0;
604
+ let costUsd = 0;
605
+
606
+ async function loadPersonaContext(personaId, { isProduceRound, isObjectionRound }) {
607
+ const manifest = loadManifest(personaId);
608
+ const useDeepWireframe = ideation_chain && wireframePersonas.has(personaId);
609
+ const isReviewRound = isObjectionRound && !isProduceRound;
610
+
611
+ // Objection rounds: card only (~500 tokens) even in --full mode
612
+ const context = isReviewRound
613
+ ? loadCard(manifest, personaId)
614
+ : (full_chain || useDeepWireframe || isProduceRound)
615
+ ? loadSkill(manifest).slice(0, 12000)
616
+ : loadCard(manifest, personaId);
617
+
618
+ const personaUsesLite = isReviewRound || (useLiteSchema && !useDeepWireframe);
619
+
620
+ const protocolHint = isObjectionRound
621
+ ? `\nDELIBERATION REVIEW MODE: Read prior outputs critically. Low satisfaction default. ${deliberation.DELIBERATION_JSON_HINT}`
622
+ : '';
623
+
624
+ const systemParts = [
625
+ `You are the "${personaId}" persona. Follow the card/skill instructions exactly.`,
626
+ `Allowed jobs: ${(manifest.allowed_jobs || []).join('; ')}`,
627
+ `Forbidden jobs: ${(manifest.forbidden_jobs || []).join('; ')}`,
628
+ `Assess-only mode is ON — do not propose code edits as applied; output critique only.`,
629
+ personaUsesLite
630
+ ? 'Use the LITE objection schema — short grounding, objections, questions, JSON block.'
631
+ : isProduceRound
632
+ ? 'Use the DEEP output schema (full blocks as in your SKILL.md) plus deliberation JSON.'
633
+ : 'REVIEW MODE: object to prior claims with evidence; include deliberation JSON block.',
634
+ protocolHint,
635
+ '',
636
+ context,
637
+ ];
638
+
639
+ const system = isReviewRound
640
+ ? enforceSystemCap(systemParts.join('\n'), 5500)
641
+ : systemParts.join('\n');
642
+
643
+ const effort = classifyEffort({
644
+ routing, digest: state.digest, manifest, scopedMode: null, fullChain: full_chain, isDeltaFollowUp,
645
+ });
646
+ const resolved = resolveModel({
647
+ manifest, effort: isObjectionRound ? 'trivial' : effort, chain, config,
648
+ defaultProvider: provider, defaultModel: model, gateName: null,
649
+ });
650
+
651
+ let finalSystem = system;
652
+ try {
653
+ const lessonsResult = lessons.retrieveLessons({ task, personaId, limit: 3 });
654
+ if (lessonsResult && lessonsResult.lessons && lessonsResult.lessons.length) {
655
+ finalSystem = finalSystem + '\n\n' + lessons.buildLessonsInjection(lessonsResult.lessons);
656
+ }
657
+ } catch {
658
+ // lessons retrieval failed — continue without injection
659
+ }
660
+
661
+ return {
662
+ system: finalSystem,
663
+ manifest,
664
+ effort: resolved.effort,
665
+ callProvider: resolved.provider,
666
+ callModel: resolved.model,
667
+ maxTokens: resolved.maxTokens,
668
+ };
669
+ }
670
+
671
+ function parseStructuredOutput(text, personaId) {
672
+ const result = { grades: {}, score: null, top_fixes: [] };
673
+ const scoreMatch = text.match(/(\d+(?:\.\d+)?)\s*\/\s*5/);
674
+ if (scoreMatch) result.score = Number(scoreMatch[1]);
675
+
676
+ const gradeMatches = text.match(/(\w+)\[([A-F])\]/gi);
677
+ if (gradeMatches) {
678
+ for (const gm of gradeMatches) {
679
+ const m = gm.match(/(\w+)\[([A-F])\]/i);
680
+ if (m) result.grades[m[1].toLowerCase()] = m[2].toUpperCase();
681
+ }
682
+ }
683
+
684
+ const fixesMatch = text.match(/Top\s*2\s*fixes:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)/i);
685
+ if (fixesMatch) {
686
+ result.top_fixes = [fixesMatch[1].trim(), fixesMatch[2].trim()];
687
+ }
688
+ return result;
689
+ }
690
+
691
+ async function callPersona(personaId, system, user, callOpts) {
692
+ console.log(`\n⏳ Running persona: ${personaId}...`);
693
+ const text = await callLlm({
694
+ provider: callOpts.provider,
695
+ model: callOpts.model,
696
+ system,
697
+ user,
698
+ maxTokens: callOpts.maxTokens,
699
+ personaId,
700
+ hostContext,
701
+ });
702
+ const inTok = Math.ceil((system.length + user.length) / 4);
703
+ const outTok = Math.ceil(text.length / 4);
704
+ const price = config.pricing && config.pricing[callOpts.model];
705
+ if (price) {
706
+ costUsd += (inTok / 1e6) * (price.input_per_m || 0) + (outTok / 1e6) * (price.output_per_m || 0);
707
+ }
708
+ console.log(`✅ ${personaId} complete (${text.length} chars, ${callOpts.effort || 'standard'})`);
709
+ return {
710
+ text,
711
+ input_tokens_est: inTok,
712
+ output_tokens_est: outTok,
713
+ structured_output: parseStructuredOutput(text, personaId),
714
+ full_prompt: { system, user },
715
+ effort_log_entry: {
716
+ persona: personaId,
717
+ effort: callOpts.effort || 'standard',
718
+ model: callOpts.model,
719
+ scoped_mode: null,
720
+ },
721
+ };
722
+ }
723
+
724
+ state = session.show({ project: projectId }) || state;
725
+
726
+ let delibResult;
727
+ try {
728
+ delibResult = await deliberation.runDeliberation({
729
+ chain,
730
+ experts,
731
+ full_chain,
732
+ ideation_chain,
733
+ mode,
734
+ task,
735
+ routing,
736
+ state,
737
+ figma,
738
+ config,
739
+ callPersona,
740
+ loadPersonaContext,
741
+ buildReferencePack: async (id, t) => {
742
+ const pack = await buildReferencePack(id, t, state);
743
+ if (pack?.cacheHit) cacheHits += 1;
744
+ return pack;
745
+ },
746
+ noDeliberate: !useDeliberation,
747
+ satisfaction_threshold: satisfaction != null ? Number(satisfaction) : undefined,
748
+ max_rounds: maxRounds != null ? Number(maxRounds) : undefined,
749
+ resume_checkpoint: continueRun ? state.host_run?.checkpoint : null,
750
+ });
751
+ } catch (err) {
752
+ if (err.name === 'HostLlmPendingError') {
753
+ session.update({
754
+ project: projectId,
755
+ patch: {
756
+ persona_outputs: err.checkpoint?.persona_outputs || state.persona_outputs,
757
+ host_run: {
758
+ ...(state.host_run || {}),
759
+ status: 'pending_devi',
760
+ last_pending: { step_id: err.stepId, persona: err.personaId },
761
+ checkpoint: err.checkpoint || null,
762
+ },
763
+ },
764
+ });
765
+ printDeviInstructions(err);
766
+ throw err;
767
+ }
768
+ throw err;
769
+ }
770
+
771
+ const syn = synthesis.buildSynthesis(delibResult.persona_outputs, { deliberation: delibResult.deliberation });
772
+
773
+ const verifyResults = figma
774
+ ? { primary_task: 'not_run', screenshots: [], reason: 'browser automation unavailable in standalone runtime' }
775
+ : { primary_task: 'not_run', screenshots: [], reason: 'assess_only, no URL' };
776
+
777
+ const expertsRun = Object.keys(delibResult.persona_outputs);
778
+
779
+ const full_prompts = {};
780
+ const structured_outputs = {};
781
+ for (const [id, out] of Object.entries(delibResult.persona_outputs || {})) {
782
+ if (out.full_prompt) full_prompts[id] = out.full_prompt;
783
+ if (out.structured_output) structured_outputs[id] = out.structured_output;
784
+ }
785
+
786
+ const final = session.update({
787
+ project: projectId,
788
+ patch: {
789
+ persona_outputs: delibResult.persona_outputs,
790
+ full_prompts: full_prompts,
791
+ structured_outputs: structured_outputs,
792
+ covered_points: delibResult.covered_points || [],
793
+ task_type: state.task_type || classifyProblem(task)?.problem_type || '',
794
+ deliberation: delibResult.deliberation,
795
+ synthesis: syn.composite,
796
+ synthesis_markdown: syn.markdown,
797
+ verify_results: verifyResults,
798
+ host_run: provider === 'host' ? {
799
+ ...(state.host_run || {}),
800
+ status: 'complete',
801
+ completed_at: new Date().toISOString(),
802
+ checkpoint: null,
803
+ } : state.host_run,
804
+ digest: {
805
+ ...(state.digest || {}),
806
+ ...delibResult.digest_patch,
807
+ experts,
808
+ },
809
+ metrics: {
810
+ llm_calls: delibResult.metrics.llm_calls,
811
+ experts_run: expertsRun,
812
+ input_tokens_est: delibResult.metrics.input_tokens_est,
813
+ output_tokens_est: delibResult.metrics.output_tokens_est,
814
+ cache_hits: cacheHits,
815
+ mode,
816
+ effort_log: delibResult.metrics.effort_log,
817
+ cost_usd: Number(costUsd.toFixed(6)),
818
+ deliberation_rounds: delibResult.metrics.deliberation_rounds,
819
+ objections_raised: delibResult.metrics.objections_raised,
820
+ objections_resolved: delibResult.metrics.objections_resolved,
821
+ raj_escalations: delibResult.metrics.raj_escalations,
822
+ },
823
+ },
824
+ });
825
+
826
+ if (output) {
827
+ fs.writeFileSync(path.resolve(output), JSON.stringify(final, null, 2));
828
+ console.log(`\n✅ Wrote session state to ${path.resolve(output)}`);
829
+ }
830
+
831
+ console.log(`\n✅ Orchestrator run complete. Experts (${mode}): ${experts.join(', ')}`);
832
+ console.log(` Provider: ${provider}${provider === 'host' ? ' (Devi host bridge)' : ''}`);
833
+ console.log(` Deliberation: ${delibResult.deliberation.mode}, rounds=${delibResult.metrics.deliberation_rounds}, consensus=${delibResult.deliberation.consensus_reached}`);
834
+ if (delibResult.deliberation.raj_escalated) console.log(' Raj escalated: yes');
835
+ console.log(` Verdict: ${syn.composite.verdict} (${syn.composite.total}/${syn.composite.max_total})`);
836
+ console.log(` Est. tokens: ${delibResult.metrics.input_tokens_est} in / ${delibResult.metrics.output_tokens_est} out`);
837
+ if (costUsd) console.log(` Est. cost: $${costUsd.toFixed(4)}`);
838
+ console.log(` Session: ${session.sessionPath(projectId)}`);
839
+ console.log('\n── Synthesis ──────────────────────────────────────');
840
+ console.log(syn.markdown);
841
+ console.log('');
842
+ return final;
843
+ }
844
+
845
+ /**
846
+ * Default run — chunked execution (v2.0).
847
+ * Pass unchunked: true to use the legacy deliberation path.
848
+ */
849
+ async function run(opts = {}) {
850
+ if (opts.unchunked) {
851
+ return runUnchunked(opts);
852
+ }
853
+
854
+ const result = await chunkRun.runChunked(opts);
855
+
856
+ if (result.mode === 'host_pending' || result.host_pending) {
857
+ return result;
858
+ }
859
+
860
+ if (result.mode === 'chunked_dry_run' || opts.dryRun) {
861
+ return result;
862
+ }
863
+
864
+ const chunkRunState = result.chunk_run || result;
865
+ const syn = chunkRunState.synthesis;
866
+ console.log('\n✅ Chunked orchestrator run complete.');
867
+ if (chunkRunState.planner_warning) {
868
+ console.log(` ⚠ Planner: ${chunkRunState.planner_warning}`);
869
+ }
870
+ if (chunkRunState.planner_model) {
871
+ console.log(` Planner: ${chunkModels.formatModelName(chunkRunState.planner_model)}`);
872
+ }
873
+ console.log(` Chunks: ${(chunkRunState.results || []).length}`);
874
+ console.log(` Failed: ${(chunkRunState.failed_chunks || []).length}`);
875
+ if (chunkRunState.models_used && chunkRunState.models_used.length) {
876
+ console.log(` Models: ${chunkRunState.models_used.join(', ')}`);
877
+ }
878
+ console.log(` Cost: $${(chunkRunState.total_cost || 0).toFixed(4)}`);
879
+ const tokens = chunkRunState.total_tokens || { input: 0, output: 0 };
880
+ console.log(` Tokens: ${tokens.input} in / ${tokens.output} out`);
881
+ if (syn) {
882
+ console.log(` Verdict: ${syn.verdict || 'n/a'} (${syn.total || 0}/${syn.max_total || 0})`);
883
+ }
884
+ console.log(` Session: ${session.sessionPath(result.project_id || session.getProjectId())}`);
885
+
886
+ // Evolution readiness hint after a successful run.
887
+ try {
888
+ const em = require('../evolution-metrics');
889
+ const ready = em.checkEvolutionReady(result.project_id || session.getProjectId());
890
+ if (ready.ready) {
891
+ console.log('');
892
+ console.log(' [Evolution] Team has enough data to evolve. Run: npx analyzthis_design evolve --extract');
893
+ } else {
894
+ const agg = ready.metrics.aggregate;
895
+ console.log('');
896
+ console.log(` [Evolution] ${ready.metrics.aggregate.evolution_level} — ${agg.total_lessons} lessons, ${agg.total_outcomes} outcomes. Accept outputs + confirm outcomes to trigger evolution.`);
897
+ }
898
+ } catch (e) { /* evolution-metrics unavailable */ }
899
+
900
+ return result.session;
901
+ }
902
+
903
+ module.exports = { run, runUnchunked, classifyProblem, classifyProblems, resolveExperts, resolveExpertsForRules, loadManifest, classifyEffort, resolveModel, callLlm, loadConfig };
904
+
905
+ // Direct invocation support
906
+ if (require.main === module) {
907
+ const args = process.argv.slice(2);
908
+ const get = (name) => {
909
+ const eq = args.find((a) => a.startsWith(`--${name}=`));
910
+ if (eq) return eq.split('=').slice(1).join('=');
911
+ const i = args.indexOf(`--${name}`);
912
+ if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('--')) return args[i + 1];
913
+ return null;
914
+ };
915
+ const expertsArg = get('experts');
916
+ run({
917
+ task: get('task'),
918
+ figma: get('figma') || '',
919
+ provider: get('provider'),
920
+ model: get('model'),
921
+ dryRun: args.includes('--dry-run'),
922
+ lite: args.includes('--lite'),
923
+ full: args.includes('--full'),
924
+ noDeliberate: args.includes('--no-deliberate'),
925
+ deliberate: args.includes('--deliberate') ? true : null,
926
+ maxRounds: get('max-rounds'),
927
+ satisfaction: get('satisfaction'),
928
+ continueRun: args.includes('--continue'),
929
+ experts: expertsArg ? expertsArg.split(',').map((s) => s.trim()).filter(Boolean) : null,
930
+ project: get('project'),
931
+ output: get('output'),
932
+ }).catch((err) => {
933
+ console.error(`\n ✗ ${err.message}\n`);
934
+ process.exit(1);
935
+ });
936
+ }