@oooooooooo/ch0de 0.0.0-stage → 1.9.55

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 (96) hide show
  1. package/API-TESTING.md +76 -0
  2. package/DEBTAUR-INTEGRATION.md +71 -0
  3. package/EDITING.md +80 -0
  4. package/EXPERT-PANEL.md +16 -0
  5. package/INTEGRATIONS.md +75 -0
  6. package/LICENSE +21 -0
  7. package/MODELS-DEV-LICENSE +21 -0
  8. package/PROVIDER-CONNECTIONS.md +48 -0
  9. package/PROVIDERS.md +17 -0
  10. package/README.md +523 -2
  11. package/RELIABILITY.md +95 -0
  12. package/UX-1.3.md +83 -0
  13. package/UX-1.4.1.md +13 -0
  14. package/UX-1.4.md +48 -0
  15. package/UX-1.5.md +13 -0
  16. package/UX-1.6.md +7 -0
  17. package/UX-1.7.3.md +13 -0
  18. package/UX-1.7.4.md +13 -0
  19. package/UX-1.7.md +19 -0
  20. package/acceptance-check.js +27 -0
  21. package/agent-budget.js +4 -0
  22. package/api-testing.js +140 -0
  23. package/automatic-re.js +19 -0
  24. package/benchmarks/public-baselines.json +29 -0
  25. package/benchmarks/render-1.4.0.json +8 -0
  26. package/benchmarks/targets.js +11 -0
  27. package/browser-check.js +1062 -0
  28. package/build-progress.js +10 -0
  29. package/build-quality.js +49 -0
  30. package/chode.js +7937 -0
  31. package/debtaur-engine.js +305 -0
  32. package/debtaur-orchestration.js +456 -0
  33. package/debtaur-worker.cjs +76 -0
  34. package/doctor.js +24 -0
  35. package/edit-transactions.js +79 -0
  36. package/elapsed.js +28 -0
  37. package/evidence-ledger.js +26 -0
  38. package/expert-panel.cjs +42 -0
  39. package/helios-ledger.js +405 -0
  40. package/install.cjs +25 -0
  41. package/integrations.js +30 -0
  42. package/json-output.js +8 -0
  43. package/local-memory.js +613 -0
  44. package/local-models.js +52 -0
  45. package/mcp-client.js +50 -0
  46. package/memory.js +511 -0
  47. package/model-catalog.js +46 -0
  48. package/native-tools.js +27 -0
  49. package/nosignups.js +325 -0
  50. package/observation-progress.js +33 -0
  51. package/orchestrator.js +299 -0
  52. package/package.json +158 -6
  53. package/plan-z6.js +490 -0
  54. package/project-memory.js +27 -0
  55. package/provider-capacity.js +13 -0
  56. package/provider-catalog.js +246 -0
  57. package/provider-connect.js +66 -0
  58. package/provider-credentials.js +5 -0
  59. package/provider-directory.js +8 -0
  60. package/provider-directory.json +32300 -0
  61. package/provider-errors.js +11 -0
  62. package/provider-picker.js +188 -0
  63. package/provider-policy.js +4 -0
  64. package/provider-race.js +25 -0
  65. package/provider-request.js +45 -0
  66. package/provider-store.js +13 -0
  67. package/rea-environment.js +5 -0
  68. package/read-window.js +21 -0
  69. package/recovery-context.js +3 -0
  70. package/reliability-memory.js +38 -0
  71. package/reliability-services.js +34 -0
  72. package/reliability-store.js +16 -0
  73. package/request-deadline.js +4 -0
  74. package/serve-frontend.html +172 -0
  75. package/settings-reset.js +11 -0
  76. package/skill-catalog.js +16 -0
  77. package/task-group.js +22 -0
  78. package/terminal-graphics.js +39 -0
  79. package/terminal-logo.js +76 -0
  80. package/terminal-ui.js +79 -0
  81. package/tool-call-validation.js +26 -0
  82. package/tool-health.js +22 -0
  83. package/tool-history.js +5 -0
  84. package/toolbox.js +156 -0
  85. package/tui-provider-picker.js +134 -0
  86. package/tui-worker.js +32 -0
  87. package/web-context.js +232 -0
  88. package/wolfram-math.js +6 -0
  89. package/worker-transport.js +11 -0
  90. package/workspace-commands.js +45 -0
  91. package/workspace-log.js +56 -0
  92. package/workspace-store.js +7 -0
  93. package/workspace-tui.js +113 -0
  94. package/workspace-view.js +109 -0
  95. package/worktrees.js +18 -0
  96. package/z6-integration.js +227 -0
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+ const ui = require('./terminal-ui');
3
+ const {formatElapsed}=require('./elapsed');
4
+ const commands = require('./workspace-commands').commands;
5
+ let packageVersion='';try{packageVersion=String(require('./package.json').version||'');}catch(_){}
6
+ const segmenter = new Intl.Segmenter(undefined,{granularity:'grapheme'});
7
+ const wrappingCache=new WeakMap();
8
+ const fittingCache=new Map();
9
+ function messageLines(message,columns){const cached=wrappingCache.get(message);if(cached&&cached.columns===columns&&cached.text===message.text)return cached.lines;const lines=wrap(message.text,columns);wrappingCache.set(message,{columns,text:message.text,lines});return lines;}
10
+ function graphemes(text){return [...segmenter.segment(String(text))].map(x=>x.segment);}
11
+ function safe(text){return String(text==null?'':text).replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g,'').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g,'').replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,'');}
12
+ function cellWidth(text){return graphemes(safe(text)).reduce((n,g)=>n+width(g),0);}
13
+ function width(g){if(/^\p{Mark}+$/u.test(g))return 0;const c=g.codePointAt(0);return /\p{Extended_Pictographic}/u.test(g)||c>=0x1100&&(c<=0x115f||c>=0x2e80&&c<=0xa4cf||c>=0xac00&&c<=0xd7a3||c>=0xf900&&c<=0xfaff||c>=0xfe10&&c<=0xfe6f||c>=0xff00&&c<=0xff60||c>=0x20000)?2:1;}
14
+ function fit(text,columns){const key=columns+'\0'+text;const cached=fittingCache.get(key);if(cached!==undefined)return cached;let s='',used=0;for(const g of graphemes(safe(text).replace(/\n/g,' '))){const n=width(g);if(used+n>columns)break;s+=g;used+=n;}const result=s+' '.repeat(Math.max(0,columns-used));if(fittingCache.size>=1024)fittingCache.delete(fittingCache.keys().next().value);fittingCache.set(key,result);return result;}
15
+ function wrap(text,columns){const lines=[];for(const line of safe(text).replace(/\t/g,' ').split('\n')){let out='',n=0;for(const g of graphemes(line)){const w=width(g);if(n+w>columns){lines.push(out);out='';n=0;}if(w<=columns){out+=g;n+=w;}}lines.push(out);}return lines;}
16
+ function createState(context={}){return {workspace:context.workspace||process.cwd(),session:context.session||'default',provider:context.provider||'auto',mode:context.mode==='plan'?'plan':'execute',messages:[],activity:[],input:'',cursor:0,history:[],historyIndex:0,details:false,scroll:0,busy:false,status:'Ready',draft:'',question:null,paste:false,logView:null,providerPicker:null};}
17
+ function addMessage(state,role,text,extra){const message={role,text:safe(text).slice(0,100000)};if(extra)for(const k of ['turn','log','logComplete'])if(extra[k]!==undefined)message[k]=extra[k];state.messages.push(message);if(state.messages.length>200)state.messages.splice(0,state.messages.length-200);}
18
+ function insert(state,text){const a=graphemes(state.input);const inserted=graphemes(safe(text));a.splice(state.cursor,0,...inserted);state.input=a.join('');state.cursor+=inserted.length;}
19
+ function key(state,text,key={}){
20
+ const name=key.name||'';
21
+ if(state.logView){if(name==='escape'||key.ctrl&&name==='c'){state.logView=null;state.scroll=0;state.status=state.busy?'Working':'Ready';return;}if(name==='pageup'){state.scroll+=10;return;}if(name==='pagedown'){state.scroll=Math.max(0,state.scroll-10);return;}if(key.shift&&name==='up'){state.scroll+=3;return;}if(key.shift&&name==='down'){state.scroll=Math.max(0,state.scroll-3);return;}return;}
22
+ if(state.providerPicker){const picker=require('./tui-provider-picker');const res=picker.handle(state.providerPicker,key,text||'');if(res&&res.type==='cancel'){state.providerPicker=null;state.status=state.busy?'Working':'Ready';return;}if(res&&res.type==='select'){state.providerPicker=null;state.status=state.busy?'Working':'Ready';return {type:'provider-select',value:res};}return;}
23
+ if(key.sequence==='\x1b[200~'){state.paste=true;return;}
24
+ if(key.sequence==='\x1b[201~'){state.paste=false;return;}
25
+ if(state.paste){insert(state,name==='return'?'\n':text||'');return;}
26
+ if(key.ctrl&&name==='c'){if(state.busy)return {type:'cancel'};if(state.input){state.input='';state.cursor=0;return;}return {type:'exit'};}
27
+ if(key.ctrl&&name==='d')return {type:state.busy?'cancel':'exit'};
28
+ if(state.question&&['up','down','tab'].includes(name))return;
29
+ if(state.question&&key.ctrl&&name==='p')return;
30
+ if(key.ctrl&&name==='p'){state.input='/';state.cursor=1;state.completionBase=null;return;}
31
+ if(name!=='tab')state.completionBase=null;
32
+ if(key.ctrl&&name==='o'){state.details=!state.details;return;}
33
+ if(name==='pageup'){state.scroll+=10;return;}
34
+ if(name==='pagedown'){state.scroll=Math.max(0,state.scroll-10);return;}
35
+ if(key.shift&&name==='up'){state.scroll+=3;return;}
36
+ if(key.shift&&name==='down'){state.scroll=Math.max(0,state.scroll-3);return;}
37
+ if(name==='escape'){state.input='';state.cursor=0;if(state.question)return {type:'answer',value:null};return;}
38
+ if(name==='return'&&!(key.shift||key.meta)){const prompt=state.input.trim();if(!prompt)return state.question?{type:'hint',value:'No input received. Paste with your terminal\u2019s own paste command (e.g. right-click), then press Enter.'}:undefined;state.input='';state.cursor=0;state.scroll=0;if(state.question)return {type:'answer',value:prompt};if(state.busy&&!/^\/(quit|exit|help|details|activity|sources|status|tools|models|providers|sessions|queue|free|reset)(?:\s|$)/.test(prompt)){insert(state,prompt);return;}state.history.push(prompt);state.historyIndex=state.history.length;return {type:prompt.startsWith('/')?'command':'submit',value:prompt};}
39
+ if(name==='return'&&(key.shift||key.meta)||key.ctrl&&name==='j'){insert(state,'\n');return;}
40
+ const chars=graphemes(state.input);
41
+ if(name==='left'){state.cursor=Math.max(0,state.cursor-1);return;}
42
+ if(name==='right'){state.cursor=Math.min(chars.length,state.cursor+1);return;}
43
+ if(name==='home'||key.ctrl&&name==='a'){state.cursor=state.cursor===0?0:chars.lastIndexOf('\n',state.cursor-1)+1;return;}
44
+ if(name==='end'||key.ctrl&&name==='e'){const end=chars.indexOf('\n',state.cursor);state.cursor=end<0?chars.length:end;return;}
45
+ if(name==='backspace'){if(state.cursor){chars.splice(--state.cursor,1);state.input=chars.join('');}return;}
46
+ if(name==='delete'){chars.splice(state.cursor,1);state.input=chars.join('');return;}
47
+ if(key.ctrl&&name==='u'){state.input=chars.slice(state.cursor).join('');state.cursor=0;return;}
48
+ if((name==='up'||name==='down')&&state.input.includes('\n')){const start=state.cursor===0?0:chars.lastIndexOf('\n',state.cursor-1)+1;const col=state.cursor-start;if(name==='up'&&start){const end=start-1;const previous=end===0?0:chars.lastIndexOf('\n',end-1)+1;state.cursor=previous+Math.min(col,end-previous);}else if(name==='down'){const end=chars.indexOf('\n',state.cursor);if(end>=0){const next=end+1;const nextEnd=chars.indexOf('\n',next);state.cursor=next+Math.min(col,(nextEnd<0?chars.length:nextEnd)-next);}}return;}
49
+ if(name==='up'||name==='down'){if(name==='up'&&state.historyIndex===state.history.length)state.historyDraft=state.input;state.historyIndex=Math.max(0,Math.min(state.history.length,state.historyIndex+(name==='up'?-1:1)));state.input=state.historyIndex===state.history.length?state.historyDraft||'':state.history[state.historyIndex]||'';state.cursor=graphemes(state.input).length;return;}
50
+ if(name==='tab'){const base=state.completionBase||state.input;if(!base.startsWith('/'))return;const matches=commands.filter(s=>s.startsWith(base));if(matches.length){if(state.completionBase===base)state.completionIndex=(state.completionIndex+(key.shift?-1:1)+matches.length)%matches.length;else{state.completionBase=base;state.completionIndex=key.shift?matches.length-1:0;}state.input=matches[state.completionIndex]+' ';state.cursor=graphemes(state.input).length;}return;}
51
+ if(!key.ctrl&&!key.meta&&text){insert(state,text);}
52
+ }
53
+ function border(text,width){const label=fit(text,width-1).trimEnd();return label+'─'.repeat(Math.max(0,width-1-cellWidth(label)))+'╮';}
54
+ function bottom(text,width){const label=fit(text,width-1).trimEnd();return label+'─'.repeat(Math.max(0,width-1-cellWidth(label)))+'╯';}
55
+ function bottomRight(left,right,width){const r=String(right||'');if(!r||cellWidth(left)>width-1-cellWidth(r)-3)return bottom(left,width);const l=fit(left,width-1-cellWidth(r)).trimEnd();const fill='─'.repeat(Math.max(0,width-1-cellWidth(l)-cellWidth(r)));return l+fill+r+'╯';}
56
+ function frame(state,columns=80,rows=24){
57
+ const secret=!!state.question?.secret,masked=secret?'•'.repeat(graphemes(state.input).length):state.input;
58
+ const w=Math.max(1,Math.floor(columns)),h=Math.max(1,Math.floor(rows));
59
+ if(w<20||h<8){const lines=Array.from({length:h},()=> ' '.repeat(w));lines[0]=fit('Chode · '+state.status,w);if(h>2)lines[1]=fit('Enlarge terminal for the workspace',w);lines[h-1]=fit('› '+masked,w);return {lines,cursor:{row:h-1,column:Math.min(w-1,2+cellWidth(graphemes(state.input).slice(0,state.cursor).join('')))},hits:[]};}
60
+ if(state.providerPicker){
61
+ const picker=require('./tui-provider-picker');
62
+ const bodyH=Math.max(0,h-8);
63
+ const pr=picker.render(state.providerPicker,w,bodyH);
64
+ const out=[fit(' \u25c8 chode / '+state.workspace,w),fit(' '+state.session+' \u00b7 '+state.provider+' \u00b7 provider picker',w),'\u2500'.repeat(w)];
65
+ for(let i=0;i<bodyH;i++)out.push(fit(pr.lines[i]||'',w));
66
+ out.push(fit(' '+state.status,w),border('╭─ Connect · up/down move · enter select · esc back ',w));
67
+ out.push(fit('│› Provider picker open…',w-1)+'│',fit('│ ',w-1)+'│',bottomRight('╰─ Ctrl+P commands · /help ',packageVersion?'v'+packageVersion:'',w));
68
+ while(out.length<h)out.push(' '.repeat(w));
69
+ const marker=String(pr.lines[pr.cursorRow]||'');const mi=marker.indexOf('\u25B6');
70
+ return {lines:out.slice(0,h),cursor:{row:Math.min(h-1,3+pr.cursorRow),column:mi>=0?cellWidth(marker.slice(0,mi)):2},hits:[],headerRows:3,titleRow:0,logo:null};
71
+ }
72
+ const side=state.details&&w>=100?30:0,mainWidth=w-side-(side?1:0),contentWidth=Math.max(4,mainWidth-4);
73
+ const logoScale=w>=60&&h>=18?1:0;const headerRows=logoScale?logoScale*4+1:3;const bodyHeight=h-headerRows-5;const body=[],bodyHits=[];
74
+ if(state.logView){
75
+ body.push('','Prompt log · '+String(state.logView.title||'Prompt'),'');
76
+ if(state.logView.file)body.push('File · '+String(state.logView.file),'');
77
+ const raw=String(state.logView.text||'').replace(/\r\n?/g,'\n').split('\n');
78
+ const shown=600;
79
+ if(raw.length>shown)body.push('['+(raw.length-shown)+' earlier lines hidden; full log is saved on disk]','');
80
+ for(const line of raw.slice(-shown))body.push(...wrap(line,contentWidth));
81
+ body.push('','Esc or Ctrl+C closes this log · wheel, PgUp/PgDn scroll');
82
+ }else{
83
+ for(const m of state.messages){body.push('',m.role==='user'?'You'+(m.turn?' · log':''):m.role==='assistant'?'Chode':'Notice');const start=body.length-1;body.push(...messageLines(m,contentWidth));if(m.role==='user'&&m.turn)for(let i=start;i<body.length;i++)bodyHits[i]={turn:m.turn,title:String(m.text||'').replace(/\s+/g,' ').slice(0,120)};}
84
+ if(state.draft){body.push('','Draft · checking before completion',...wrap(state.draft,contentWidth));}
85
+ if(state.details&&!side&&state.activity.length){body.push('','Activity');for(const e of state.activity.slice(-6))body.push(...wrap((e.ok===false?'! ':e.ok===true?'✓ ':'· ')+e.label,contentWidth));}
86
+ if(!body.length){const center=text=>' '.repeat(Math.max(0,Math.floor((contentWidth-cellWidth(text))/2)))+text;body.push(...Array(Math.max(0,Math.floor((bodyHeight-9)/2))).fill(''),center('c h o d e'),'',center('What are we building?'),'',center('Build a program · Design a website · Understand a binary'),'',center('Ctrl+P commands /models providers @file context'));}
87
+ if(!state.question&&state.input.startsWith('/')&&!/\s/.test(state.input)){const matches=commands.filter(x=>x.startsWith(state.input));if(matches.length)body.push('','Commands · Tab completes',...wrap(matches.join(' '),contentWidth));}
88
+ }
89
+ const offset=Math.min(state.scroll,Math.max(0,body.length-bodyHeight));const end=body.length-offset;const start=Math.max(0,end-bodyHeight);const visible=body.slice(start,end);const hits=[];if(!state.logView)for(let i=0;i<visible.length;i++){const hit=bodyHits[start+i];if(hit)hits.push({row:headerRows+i,start:2,end:mainWidth-1,turn:hit.turn,title:hit.title});}
90
+ const events=state.activity.slice(-Math.max(0,bodyHeight-1)).map(e=>(e.ok===false?'! ':e.ok===true?'✓ ':'· ')+e.label);
91
+ const elapsed=state.elapsedMs?' · '+formatElapsed(state.elapsedMs):'';
92
+ const titleRow=logoScale===2?2:0,prefix=' '.repeat(logoScale*8+4);
93
+ const headerText=['chode / '+state.workspace,state.session+' \u00b7 '+(state.mode||'execute')+(state.yolo?' · YOLO':'')+' \u00b7 '+state.provider,state.status+elapsed,(state.freeOnly?'Free mode \u00b7 ':'')+'Ctrl+P commands'];
94
+ const out=logoScale?[...Array.from({length:headerRows-1},(_,i)=>fit(i>=titleRow&&i<titleRow+4?prefix+headerText[i-titleRow]:'',w)),'\u2500'.repeat(w)]:[fit(' \u25c8 chode / '+state.workspace,w),fit(' '+state.session+' \u00b7 '+(state.mode||'execute')+' \u00b7 '+state.provider+' \u00b7 '+state.status+elapsed,w),'\u2500'.repeat(w)];
95
+ for(let i=0;i<bodyHeight;i++){const left=fit(' '+(visible[i]||''),mainWidth);out.push(side?left+'│'+fit(i===0?' Activity':events[i-1]||'',side):fit(left,w));}
96
+ const status=state.logView?'Prompt log · wheel/PgUp/PgDn scroll · Esc closes':state.question?'Answer the question below':state.busy?'Working · Ctrl+C stops this run':state.scroll?'History · wheel/PgDn returns to latest':'Ready · Enter sends · click a You prompt for its full log';
97
+ out.push(fit(' '+status,w),border('╭─ '+(state.logView?'Log':state.question?'Reply':state.busy?'Working':'Build')+' · '+state.provider+' ',w));
98
+ const inputWidth=w-5;
99
+ const displayInput=state.logView?'Esc closes the log':masked||(!state.busy?'Ask Chode…':'Write your next prompt…');
100
+ const inputLines=wrap(displayInput,inputWidth);
101
+ const before=graphemes(masked).slice(0,state.cursor).join('');
102
+ const cursorLines=wrap(before,inputWidth);let cursorRow=cursorLines.length-1,cursorCol=cellWidth(cursorLines[cursorRow]);
103
+ if(cursorCol>=inputWidth){cursorRow++;cursorCol=0;}
104
+ const first=Math.max(0,cursorRow-1);
105
+ out.push(fit('│› '+(inputLines[first]||''),w-1)+'│',fit('│ '+(inputLines[first+1]||''),w-1)+'│',bottomRight('╰─ Ctrl+P commands · Ctrl+O activity · /help ',packageVersion?'v'+packageVersion:'',w));
106
+ while(out.length<h)out.push(' '.repeat(w));
107
+ return {lines:out.slice(0,h),cursor:{row:Math.min(h-1,h-3+cursorRow-first),column:Math.min(w-1,3+cursorCol)},hits,headerRows,titleRow,logo:logoScale?{row:0,column:1,scale:logoScale}:null};
108
+ }
109
+ module.exports={safe,graphemes,cellWidth,fit,wrap,createState,addMessage,insert,key,frame};
package/worktrees.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ const fs=require('fs'),path=require('path'),{spawnSync}=require('child_process');
3
+ function git(cwd,args){const env={...process.env};for(const key of ['GIT_DIR','GIT_WORK_TREE','GIT_INDEX_FILE','GIT_COMMON_DIR'])delete env[key];const r=spawnSync('git',['-C',cwd,...args],{encoding:'utf8',windowsHide:true,timeout:30000,maxBuffer:1024*1024,env});if(r.error||r.status!==0)throw Error(String(r.error?.message||r.stderr||'Git command failed').trim());return r.stdout.trim();}
4
+ function create(cwd=process.cwd()){
5
+ const repo=fs.realpathSync(git(cwd,['rev-parse','--show-toplevel'])),directory=path.join(repo,'.chode','worktrees');
6
+ const nameOf=name=>{if(typeof name!=='string'||!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}$/.test(name))throw Error('Worktree name must use 1–48 letters, digits, hyphens or underscores');return name;};
7
+ function target(name){nameOf(name);let at=repo;for(const part of ['.chode','worktrees',name]){at=path.join(at,part);try{if(fs.lstatSync(at).isSymbolicLink())throw Error('Worktree path contains a symbolic link');}catch(e){if(e.code!=='ENOENT')throw e;}}return at;}
8
+ function list(){const out=[];for(const field of git(repo,['worktree','list','--porcelain','-z']).split('\0')){if(field.startsWith('worktree '))out.push({path:field.slice(9)});else if(out.length){if(field.startsWith('HEAD '))out.at(-1).commit=field.slice(5);if(field.startsWith('branch '))out.at(-1).branch=field.slice(7);}}return out;}
9
+ function add(name,ref='HEAD'){
10
+ const dest=target(name);if(fs.existsSync(dest))throw Error('Worktree destination already exists');if(typeof ref!=='string'||!ref||ref.startsWith('-')||/[\x00-\x1f]/.test(ref))throw Error('Invalid starting ref');const commit=git(repo,['rev-parse','--verify','--end-of-options',ref+'^{commit}']);
11
+ const dirty=!!git(repo,['status','--porcelain','--untracked-files=no']),branch='codex/'+name;git(repo,['check-ref-format','--branch',branch]);fs.mkdirSync(directory,{recursive:true});git(repo,['worktree','add','-b',branch,'--',dest,commit]);
12
+ const relative=path.relative(repo,path.resolve(cwd)),project=path.join(dest,relative);return {ok:true,path:dest,directory:fs.existsSync(project)?project:dest,branch,commit,originalDirty:dirty,note:dirty?'Uncommitted changes remain in the original checkout.':'Original checkout is unchanged.'};
13
+ }
14
+ function remove(name){const dest=target(name),entry=list().find(r=>path.resolve(r.path)===path.resolve(dest));if(!entry||entry.branch!=='refs/heads/codex/'+name)throw Error('Not a Chode-managed worktree');git(repo,['worktree','remove','--',dest]);return {ok:true,path:dest,branchRetained:entry.branch,note:'Worktree removed; branch retained. Dirty worktrees are never forced.'};}
15
+ return {repo,list,add,remove};
16
+ }
17
+ function run(argv,cwd=process.cwd()){try{const api=create(cwd);if(!argv.length||argv[0]==='list')return {ok:true,worktrees:api.list()};if(argv[0]==='create'&&argv[1]){if(argv.length>2&&!(argv.length===4&&argv[2]==='--ref'))throw Error('Use create <name> [--ref <ref>]');return api.add(argv[1],argv[3]||'HEAD');}if(argv[0]==='remove'&&argv.length===2)return api.remove(argv[1]);throw Error('Use chode worktree list|create <name> [--ref <ref>]|remove <name>');}catch(error){return {ok:false,error:error.message};}}
18
+ module.exports={create,run};
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+ /**
3
+ * Z6P Integration Layer - Phase 3: Handoff Protocol
4
+ *
5
+ * Integrates Z6Plan with chode's existing plan parsing and provider switching.
6
+ * Provides hash-bound handoff state between provider calls.
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const path = require('path');
11
+ const { Z6Plan } = require('./plan-z6');
12
+ const localMemory = require('./local-memory');
13
+
14
+ /**
15
+ * Convert legacy plan steps array to Z6Plan
16
+ * @param {Array} legacySteps - Array of {index, text, files, check}
17
+ * @param {string} sessionId - Session ID for storage
18
+ * @returns {Z6Plan}
19
+ */
20
+ function legacyToZ6Plan(legacySteps, sessionId) {
21
+ if (!legacySteps || !Array.isArray(legacySteps)) {
22
+ return new Z6Plan([]);
23
+ }
24
+
25
+ const steps = legacySteps.map((step, i) => ({
26
+ id: i,
27
+ description: step.text || step.desc || '',
28
+ files: step.files || [],
29
+ check: step.check || '',
30
+ status: 0, // pending
31
+ dependsOn: [],
32
+ preConditions: [],
33
+ postConditions: []
34
+ }));
35
+
36
+ return new Z6Plan(steps, sessionId ? { id: sessionId } : undefined);
37
+ }
38
+
39
+ /**
40
+ * Parse plan text into Z6Plan (replaces parsePlanSteps)
41
+ * @param {string} planText - Raw plan text from model
42
+ * @param {string} sessionId - Session ID
43
+ * @returns {Z6Plan}
44
+ */
45
+ function parsePlanToZ6(planText, sessionId) {
46
+ const legacySteps = [];
47
+ String(planText || '').split('\n').forEach(function (ln) {
48
+ var m = ln.match(/^\s*(?:step\s*)?(\d+)[.):\-]\s+(.*)$/i);
49
+ if (m) {
50
+ legacySteps.push({
51
+ index: parseInt(m[1], 10),
52
+ text: m[2].slice(0, 500),
53
+ raw: ln.slice(0, 500)
54
+ });
55
+ }
56
+ });
57
+ legacySteps.sort((a, b) => a.index - b.index);
58
+ return legacyToZ6Plan(legacySteps, sessionId);
59
+ }
60
+
61
+ /**
62
+ * Serialize Z6Plan to handoff state
63
+ * @param {Z6Plan} plan - Plan to serialize
64
+ * @param {Object} context - Additional context (provider, capacity, etc.)
65
+ * @returns {Object} Hash-bound handoff state
66
+ */
67
+ function serializeHandoff(plan, context) {
68
+ const state = {
69
+ plan: plan ? plan.serialize() : null,
70
+ provider: context?.provider || null,
71
+ providerHealth: context?.providerHealth || 0,
72
+ capacityState: context?.capacityState || {},
73
+ timestamp: Date.now()
74
+ };
75
+
76
+ const encoded = JSON.stringify(state);
77
+ const stateHash = crypto.createHash('sha256').update(encoded).digest('hex');
78
+ return {
79
+ handoffId: plan ? String(plan.id) : null,
80
+ state,
81
+ hash: stateHash,
82
+ selfHash: crypto.createHash('sha256').update(encoded + stateHash).digest('hex')
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Deserialize handoff state
88
+ * @param {Object} handoff - Serialized handoff
89
+ * @param {string} sessionId - Session ID
90
+ * @returns {Object} { plan, context, valid }
91
+ */
92
+ function deserializeHandoff(handoff, sessionId) {
93
+ if (!handoff || !handoff.state) {
94
+ return { plan: null, context: {}, valid: false, reason: 'No handoff state' };
95
+ }
96
+
97
+ // Verify hash integrity (matches serializeHandoff exactly)
98
+ const encoded = JSON.stringify(handoff.state);
99
+ const expectedHash = crypto.createHash('sha256').update(encoded).digest('hex');
100
+ const expectedSelfHash = crypto.createHash('sha256').update(encoded + expectedHash).digest('hex');
101
+
102
+ if (handoff.hash !== expectedHash || handoff.selfHash !== expectedSelfHash) {
103
+ return { plan: null, context: {}, valid: false, reason: 'Hash integrity check failed' };
104
+ }
105
+
106
+ // Reconstruct Z6Plan
107
+ let plan = null;
108
+ if (handoff.state.plan && handoff.state.plan.steps) {
109
+ plan = Z6Plan.fromSerialized(handoff.state.plan, sessionId);
110
+ }
111
+
112
+ return {
113
+ plan,
114
+ context: {
115
+ provider: handoff.state.provider,
116
+ providerHealth: handoff.state.providerHealth,
117
+ capacityState: handoff.state.capacityState
118
+ },
119
+ valid: true
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Update plan step status based on tool actions
125
+ * @param {Z6Plan} plan - Plan to update
126
+ * @param {Object} actions - Tool actions from execution
127
+ * @returns {Z6Plan} Updated plan
128
+ */
129
+ function updatePlanFromActions(plan, actions, out) {
130
+ if (!plan || !plan.steps) return plan;
131
+
132
+ // Accept either {writes, edits} action records or a tool call / call list
133
+ // (chode.js feeds the single call being executed plus its result).
134
+ const modifiedFiles = new Set();
135
+ const add = (file) => { if (typeof file === 'string' && file) modifiedFiles.add(file); };
136
+ const entry = (item) => (typeof item === 'string' ? item : item && item.file);
137
+
138
+ if (Array.isArray(actions)) {
139
+ for (const call of actions) add(call && call.args ? call.args.file : null);
140
+ if (out) add(out.file);
141
+ } else if (actions && typeof actions === 'object') {
142
+ if (actions.writes) actions.writes.forEach(w => add(entry(w)));
143
+ if (actions.edits) actions.edits.forEach(e => add(entry(e))); // was a no-op: e.file was never added
144
+ if (out) add(out.file);
145
+ }
146
+
147
+ plan.steps.forEach(step => {
148
+ if (step.files.some(f => modifiedFiles.has(f))) {
149
+ if (step.status === 0) step.status = 1; // pending -> in-progress
150
+ }
151
+ });
152
+
153
+ return plan;
154
+ }
155
+
156
+ /**
157
+ * Get next executable step from plan
158
+ * @param {Z6Plan} plan - Plan to query
159
+ * @param {Object} context - Current execution context
160
+ * @returns {Z6PlanStep|null}
161
+ */
162
+ function getNextStep(plan, context) {
163
+ if (!plan) return null;
164
+ return plan.getNextExecutable();
165
+ }
166
+
167
+ /**
168
+ * Mark step as completed with verification
169
+ * @param {Z6Plan} plan - Plan to update
170
+ * @param {number} stepId - Step ID to complete
171
+ * @param {Object} verification - Verification result
172
+ * @returns {boolean} Success
173
+ */
174
+ function completeStep(plan, stepId, verification) {
175
+ if (!plan) return false;
176
+ const step = plan.steps.find(s => s.id === stepId);
177
+ if (!step) return false;
178
+
179
+ // Check post-conditions
180
+ const postConditionsOk = step.postConditions.every(c => c.check(verification));
181
+
182
+ step.status = postConditionsOk ? 3 : 5; // done or failed
183
+ return postConditionsOk;
184
+ }
185
+
186
+ /**
187
+ * Save handoff state to local memory
188
+ * @param {string} sessionId - Session ID
189
+ * @param {Object} handoff - Serialized handoff
190
+ * @returns {boolean} Success
191
+ */
192
+ async function saveHandoffToMemory(sessionId, handoff) {
193
+ try {
194
+ localMemory.getMemoryManager().setHandoff(sessionId, handoff);
195
+ return true;
196
+ } catch (e) {
197
+ console.error('Failed to save handoff:', e.message);
198
+ return false;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Load last handoff state from local memory
204
+ * @param {string} sessionId - Session ID (storage holds one handoff per session)
205
+ * @returns {Promise<Object|null>}
206
+ */
207
+ async function loadHandoffFromMemory(sessionId) {
208
+ try {
209
+ const handoff = localMemory.getMemoryManager().getHandoff(sessionId);
210
+ return handoff && handoff.state ? handoff : null;
211
+ } catch (e) {
212
+ console.error('Failed to load handoff:', e.message);
213
+ return null;
214
+ }
215
+ }
216
+
217
+ module.exports = {
218
+ legacyToZ6Plan,
219
+ parsePlanToZ6,
220
+ serializeHandoff,
221
+ deserializeHandoff,
222
+ updatePlanFromActions,
223
+ getNextStep,
224
+ completeStep,
225
+ saveHandoffToMemory,
226
+ loadHandoffFromMemory
227
+ };