@oooooooooo/ch0de 0.0.0-stage → 1.9.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API-TESTING.md +76 -0
- package/DEBTAUR-INTEGRATION.md +71 -0
- package/EDITING.md +80 -0
- package/EXPERT-PANEL.md +16 -0
- package/INTEGRATIONS.md +75 -0
- package/LICENSE +21 -0
- package/MODELS-DEV-LICENSE +21 -0
- package/PROVIDER-CONNECTIONS.md +48 -0
- package/PROVIDERS.md +17 -0
- package/README.md +523 -2
- package/RELIABILITY.md +95 -0
- package/UX-1.3.md +83 -0
- package/UX-1.4.1.md +13 -0
- package/UX-1.4.md +48 -0
- package/UX-1.5.md +13 -0
- package/UX-1.6.md +7 -0
- package/UX-1.7.3.md +13 -0
- package/UX-1.7.4.md +13 -0
- package/UX-1.7.md +19 -0
- package/acceptance-check.js +27 -0
- package/agent-budget.js +4 -0
- package/api-testing.js +140 -0
- package/automatic-re.js +19 -0
- package/benchmarks/public-baselines.json +29 -0
- package/benchmarks/render-1.4.0.json +8 -0
- package/benchmarks/targets.js +11 -0
- package/browser-check.js +1062 -0
- package/build-progress.js +10 -0
- package/build-quality.js +49 -0
- package/chode.js +7937 -0
- package/debtaur-engine.js +305 -0
- package/debtaur-orchestration.js +456 -0
- package/debtaur-worker.cjs +76 -0
- package/doctor.js +24 -0
- package/edit-transactions.js +79 -0
- package/elapsed.js +28 -0
- package/evidence-ledger.js +26 -0
- package/expert-panel.cjs +42 -0
- package/helios-ledger.js +405 -0
- package/install.cjs +25 -0
- package/integrations.js +30 -0
- package/json-output.js +8 -0
- package/local-memory.js +613 -0
- package/local-models.js +52 -0
- package/mcp-client.js +50 -0
- package/memory.js +511 -0
- package/model-catalog.js +46 -0
- package/native-tools.js +27 -0
- package/nosignups.js +325 -0
- package/observation-progress.js +33 -0
- package/orchestrator.js +299 -0
- package/package.json +160 -6
- package/plan-z6.js +490 -0
- package/project-memory.js +27 -0
- package/provider-capacity.js +13 -0
- package/provider-catalog.js +246 -0
- package/provider-connect.js +66 -0
- package/provider-credentials.js +5 -0
- package/provider-directory.js +8 -0
- package/provider-directory.json +32300 -0
- package/provider-errors.js +11 -0
- package/provider-picker.js +188 -0
- package/provider-policy.js +4 -0
- package/provider-race.js +25 -0
- package/provider-request.js +45 -0
- package/provider-store.js +13 -0
- package/rea-environment.js +5 -0
- package/read-window.js +21 -0
- package/recovery-context.js +3 -0
- package/reliability-memory.js +38 -0
- package/reliability-services.js +34 -0
- package/reliability-store.js +16 -0
- package/request-deadline.js +4 -0
- package/serve-frontend.html +172 -0
- package/settings-reset.js +11 -0
- package/skill-catalog.js +16 -0
- package/task-group.js +22 -0
- package/terminal-graphics.js +39 -0
- package/terminal-logo.js +76 -0
- package/terminal-ui.js +79 -0
- package/tool-call-validation.js +26 -0
- package/tool-health.js +22 -0
- package/tool-history.js +5 -0
- package/toolbox.js +156 -0
- package/tui-provider-picker.js +134 -0
- package/tui-worker.js +32 -0
- package/web-context.js +232 -0
- package/wolfram-math.js +6 -0
- package/worker-transport.js +11 -0
- package/workspace-commands.js +45 -0
- package/workspace-log.js +56 -0
- package/workspace-store.js +7 -0
- package/workspace-tui.js +113 -0
- package/workspace-view.js +109 -0
- package/worktrees.js +18 -0
- 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
|
+
};
|