@yunhozz/arc 0.2.0 → 0.2.1

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 (34) hide show
  1. package/MANIFEST.sha256 +33 -33
  2. package/README.md +14 -34
  3. package/claude/agents/arc-implementer.md +4 -2
  4. package/claude/agents/arc-planner.md +2 -2
  5. package/claude/agents/arc-qa.md +3 -3
  6. package/claude/agents/arc-reviewer.md +2 -2
  7. package/claude/agents/arc-scouter.md +2 -2
  8. package/claude/skill/SKILL.md +3 -1
  9. package/claude/skill/references/commit.md +1 -1
  10. package/claude/skill/references/coordination.md +6 -2
  11. package/claude/skill/references/handoff-prep.md +5 -1
  12. package/claude/skill/references/handoff.md +12 -1
  13. package/claude/skill/references/planning.md +5 -1
  14. package/claude/skill/references/verification.md +6 -0
  15. package/claude/skill/references/visual-companion.md +6 -0
  16. package/codex/agents/arc-implementer.toml +5 -2
  17. package/codex/agents/arc-planner.toml +2 -2
  18. package/codex/agents/arc-qa.toml +3 -3
  19. package/codex/agents/arc-reviewer.toml +2 -2
  20. package/codex/agents/arc-scouter.toml +2 -2
  21. package/codex/skill/SKILL.md +3 -1
  22. package/codex/skill/references/commit.md +1 -1
  23. package/codex/skill/references/coordination.md +6 -2
  24. package/codex/skill/references/handoff-prep.md +5 -1
  25. package/codex/skill/references/handoff.md +11 -1
  26. package/codex/skill/references/planning.md +5 -1
  27. package/codex/skill/references/verification.md +6 -0
  28. package/codex/skill/references/visual-companion.md +6 -0
  29. package/lib/policy/role-policy.json +7 -7
  30. package/lib/tui/app.mjs +75 -20
  31. package/lib/tui/render.mjs +28 -6
  32. package/lib/tui/setup.mjs +4 -3
  33. package/lib/tui/terminal.mjs +14 -4
  34. package/package.json +1 -1
@@ -4,18 +4,40 @@ export function escapeTerminalText(value=''){
4
4
  const code=character.codePointAt(0);return code<=255?'\\x'+code.toString(16).padStart(2,'0'):'\\u{'+code.toString(16)+'}';
5
5
  });
6
6
  }
7
- function width(character){return /[\p{Mark}]/u.test(character)?0:character.codePointAt(0)>=0x1100?2:1;}
7
+ function width(character){return /[\p{Mark}]/u.test(character)?0:/[\u2190-\u2193\u2500-\u257f\u2588]/u.test(character)?1:character.codePointAt(0)>=0x1100?2:1;}
8
8
  function fit(text,columns){let used=0,result='';for(const character of text){const size=width(character);if(used+size>columns)return{text:result,complete:false};result+=character;used+=size;}return{text:result,complete:true};}
9
+ export function navigationHint(mode='menu',{backLabel='back',hasNext=false}={}){
10
+ if(mode==='text')return'Type/Paste Input · ← Delete · Enter Continue · Esc/Backspace Back';
11
+ if(mode==='busy')return'Ctrl+C Interrupt';
12
+ if(mode==='page'||mode==='reviewPage')return'↑/↓: Scroll · Enter '+(mode==='page'?'Back':hasNext?'Next':'Continue')+' · Esc Back';
13
+ return'↑/↓: Select · Enter Continue · Esc '+(backLabel==='exit'?'Exit':'Back');
14
+ }
15
+ export function wrapDetailRows(rows,columns){
16
+ const available=Math.max(2,Math.floor(Number(columns)||80)-3),result=[];
17
+ for(const row of rows){const escaped=escapeTerminalText(row);let line='',used=0;for(const character of escaped){const size=width(character);if(used+size>available){result.push(line);line='';used=0;}line+=character;used+=size;}result.push(line);}
18
+ return result;
19
+ }
20
+ const wordmark=[' █████ ███████ ███████ ','██ ██ ██ ██ ██ ','█████████ ███████ ██ ','██ ██ ██ ██ ██ ','██ ██ ██ ██ ███████ '];
21
+ const logoStart=Math.min(...wordmark.map(row=>row.indexOf('█'))),logoEnd=Math.max(...wordmark.map(row=>row.lastIndexOf('█')));
22
+ const logoStops=[[183,228,199],[116,198,157],[64,145,108]];
23
+ function logoColor(column){const position=(column-logoStart)/(logoEnd-logoStart)*2,segment=Math.min(1,Math.floor(position)),fraction=position-segment;return logoStops[segment].map((value,index)=>Math.round(value+(logoStops[segment+1][index]-value)*fraction));}
24
+
9
25
  export function renderScreen(screen={}, {columns=80,rows=24}={}){
10
26
  // Leave the last column unused to avoid terminal autowrap at the right edge.
11
27
  columns=Math.max(1,Math.floor(Number(columns)||80)-1);rows=Math.max(1,Math.floor(Number(rows)||24));
12
- const entries=screen.rows??[],hint=screen.hint??'Up/Down: select | Enter: continue | Esc: back';
13
- const lines=[escapeTerminalText(screen.title),...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),escapeTerminalText(hint)];
28
+ const entries=screen.rows??[],hint=screen.hint??navigationHint('menu');
29
+ const heading=screen.wordmark?[...wordmark,'─'.repeat(Math.min(columns,34))]:[escapeTerminalText(screen.title)],position=screen.position?[escapeTerminalText(screen.position)]:[];
30
+ const lines=[...heading,...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),...position,escapeTerminalText(hint)];
14
31
  const fitted=lines.map(line=>fit(line,columns));
15
32
  const required=screen.requiredRows??entries.map((_,index)=>index);
16
- const requiredVisible=required.every(index=>Number.isInteger(index)&&index>=0&&index<entries.length&&index+1<rows-1&&fitted[index+1].complete);
17
- const applyEnabled=!screen.requiresConfirmation||(requiredVisible&&fitted[0].complete&&lines.length<=rows&&fitted.at(-1).complete);
33
+ const requiredVisible=required.every(index=>Number.isInteger(index)&&index>=0&&index<entries.length&&index+heading.length<rows-1-position.length&&fitted[index+heading.length].complete);
34
+ const applyEnabled=!screen.requiresConfirmation||(requiredVisible&&heading.every((_,index)=>fitted[index].complete)&&lines.length<=rows&&fitted.at(-1).complete&&position.every((_,index)=>fitted[lines.length-2-index].complete));
18
35
  const visible=fitted.slice(0,rows).map(line=>line.text);
19
36
  if(!applyEnabled)visible[visible.length-1]=fit('Resize terminal to review before applying.',columns).text;
20
- return{text:visible.join('\n'),applyEnabled};
37
+ const styles=[];
38
+ if(screen.wordmark){for(let index=0;index<Math.min(wordmark.length,visible.length);index++){if(visible[index]===fitted[index].text)styles.push({line:index,color:'logo',cells:[...visible[index]].flatMap((character,column)=>character==='█'?[{column,rgb:logoColor(column)}]:[])});}if(visible.length>wordmark.length&&visible[wordmark.length]===fitted[wordmark.length].text)styles.push({line:wordmark.length,color:'green'});}
39
+ const selectedLine=heading.length+screen.selected;
40
+ if(Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<visible.length&&!(screen.requiresConfirmation&&!applyEnabled&&selectedLine===visible.length-1)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
41
+
42
+ return{text:visible.join('\n'),applyEnabled,styles};
21
43
  }
package/lib/tui/setup.mjs CHANGED
@@ -3,12 +3,13 @@ export function initialSettings(statuses){return Object.fromEntries(['codex','cl
3
3
  export function setupOptions(draft,target){if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude))throw Error('Target settings differ. Explicitly select common values before applying to both.');return{...draft[target==='all'?'codex':target],target};}
4
4
  export async function runSetup({ui,statuses,draft,kind,prepare}){
5
5
  for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
6
- while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Reconfigure',['Codex','Claude','Both targets']);if(targetIndex===null)return null;const target=['codex','claude','all'][targetIndex];
6
+ let selectedTarget=0;
7
+ while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Reconfigure',['Codex','Claude','Both targets'],{selected:selectedTarget});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude','all'][targetIndex];
7
8
  if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
8
9
  const choice=await ui.menu('Target settings differ',['Back','Use Codex values for both','Use Claude values for both'],{info:['Codex: '+summary(draft.codex),'Claude: '+summary(draft.claude),'Choosing common values replaces the other target values.']});if(choice===null||choice===0)continue;const source=choice===1?'codex':'claude';draft.codex={...draft[source]};draft.claude={...draft[source]};
9
10
  }
10
- const settings=draft[target==='all'?'codex':target];let editing=true;
11
- while(editing){const labels=kind==='preset'?['Preset: '+settings.preset,'Review changes']:['Preset: '+settings.preset,'Harness language: '+settings.language,'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Review changes'];const field=await ui.menu('Settings — '+target,labels);if(field===null)break;
11
+ const settings=draft[target==='all'?'codex':target];let editing=true,selectedField=0;
12
+ while(editing){const labels=kind==='preset'?['Preset: '+settings.preset,'Review changes']:['Preset: '+settings.preset,'Harness language: '+settings.language,'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Review changes'];const field=await ui.menu('Settings — '+target,labels,{selected:selectedField});if(field===null)break;selectedField=field;
12
13
  if(field===0){const value=await ui.menu('Performance preset',['Economy','Balanced','Smart'],{selected:['economy','balanced','smart'].indexOf(settings.preset)});if(value!==null)settings.preset=['economy','balanced','smart'][value];}
13
14
  else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['English','Korean'],{selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
14
15
  else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';const raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',String(settings[key]));if(raw!==null)try{settings[key]=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);}}
@@ -10,9 +10,11 @@ import {renderScreen} from './render.mjs';
10
10
  * discards unread events; a chunk is fully decoded before resolving a reader,
11
11
  * so an Enter followed by EOF in that chunk cannot escape shutdown.
12
12
  */
13
- export function createTerminalSession({input=process.stdin,output=process.stdout,signals=process}={}){
13
+ export function createTerminalSession({input=process.stdin,output=process.stdout,signals=process,env=process.env}={}){
14
14
  if(!input.isTTY||!output.isTTY)throw Object.assign(Error('An interactive terminal is required.'),{code:'ARC_TUI_TERMINAL'});
15
15
  const wasRaw=Boolean(input.isRaw),wasFlowing=input.readableFlowing;
16
+ const colorEnabled=env.NO_COLOR===undefined&&env.TERM!=='dumb';
17
+ const truecolor=colorEnabled&&(/^(truecolor|24bit)$/i.test(env.COLORTERM??'')||/^(ghostty|cmux)$/i.test(env.TERM_PROGRAM??'')||/-direct$/.test(env.TERM??'')||(output.getColorDepth?.()??0)>=24);
16
18
  let disposed=false,busy=false,parsing=false,pending='',paste=null,sequence=0,escapeTimer,waiter,queue=[],screen,lastLines=0,applyEnabled=true;
17
19
  let decoder=new StringDecoder('utf8'),lastWasCR=false;
18
20
  const clearTimer=()=>{clearTimeout(escapeTimer);escapeTimer=undefined;};
@@ -26,7 +28,7 @@ export function createTerminalSession({input=process.stdin,output=process.stdout
26
28
  output.removeListener('resize',onResize);output.removeListener('error',onError);signals.removeListener('SIGINT',onSigint);signals.removeListener('SIGTERM',onSigterm);
27
29
  try{if(input.setRawMode&&!input.destroyed)input.setRawMode(wasRaw);}finally{
28
30
  try{if(wasFlowing!==true)input.pause();}finally{
29
- try{output.write('\x1b[?2004l\x1b[?25h\n');}finally{if(waiter){const resolve=waiter;waiter=undefined;resolve({name:'eof',sequence});}}
31
+ try{output.write('\x1b[0m\x1b[?2004l\x1b[?25h\n');}finally{if(waiter){const resolve=waiter;waiter=undefined;resolve({name:'eof',sequence});}}
30
32
  }
31
33
  }
32
34
  }
@@ -39,7 +41,15 @@ export function createTerminalSession({input=process.stdin,output=process.stdout
39
41
  const result=renderScreen(screen,{columns:output.columns,rows:output.rows});applyEnabled=result.applyEnabled;
40
42
  if(lastLines)output.write(`\x1b[${lastLines}A\r`);
41
43
  const lines=result.text.split('\n');
42
- for(let index=0;index<Math.max(lastLines,lines.length);index++)output.write('\x1b[2K'+(lines[index]??'')+'\n');
44
+ for(let index=0;index<Math.max(lastLines,lines.length);index++){
45
+ let line=lines[index]??'';const style=result.styles.find(style=>style.line===index);
46
+ if(style&&colorEnabled){
47
+ if(style.color==='logo'&&truecolor){const cells=new Map(style.cells.map(cell=>[cell.column,cell.rgb]));line=[...line].map((character,column)=>cells.has(column)?'\x1b[38;2;'+cells.get(column).join(';')+'m'+character+'\x1b[0m':character).join('');}
48
+ else{const color=style.color==='logo'?'\x1b[92m':style.color==='selected'?(truecolor?'\x1b[48;2;16;55;26m\x1b[38;2;220;245;224m':'\x1b[42;30m'):'\x1b[32m';line=color+line+'\x1b[0m';}
49
+ }
50
+ output.write('\x1b[2K'+line+'\n');
51
+ }
52
+
43
53
  if(lastLines>lines.length)output.write(`\x1b[${lastLines-lines.length}A\r`);
44
54
  lastLines=lines.length;return result;
45
55
  }
@@ -67,5 +77,5 @@ export function createTerminalSession({input=process.stdin,output=process.stdout
67
77
  function onData(chunk){if(disposed||busy)return;sequence++;clearTimer();pending+=typeof chunk==='string'?chunk:decoder.write(chunk);parsing=true;try{parse();}finally{parsing=false;if(waiter&&queue.length){const resolve=waiter;waiter=undefined;resolve(queue.shift());}}}
68
78
  input.on('data',onData);input.on('end',onEnd);input.on('close',onEnd);input.on('error',onError);output.on('resize',onResize);output.on('error',onError);signals.on('SIGINT',onSigint);signals.on('SIGTERM',onSigterm);
69
79
  try{input.setRawMode?.(true);input.resume();output.write('\x1b[?25l\x1b[?2004h');}catch(error){dispose();throw error;}
70
- return{render,discardInput,dispose,setBusy(value){busy=Boolean(value);discardInput();},nextEvent(){if(queue.length)return Promise.resolve(queue.shift());if(disposed)return Promise.resolve({name:'eof',sequence});if(waiter)return Promise.reject(Error('Only one terminal event reader is allowed.'));return new Promise(resolve=>{waiter=resolve;});}};
80
+ return{render,discardInput,dispose,viewport(){return{columns:output.columns??80,rows:output.rows??24};},setBusy(value){busy=Boolean(value);discardInput();},nextEvent(){if(queue.length)return Promise.resolve(queue.shift());if(disposed)return Promise.resolve({name:'eof',sequence});if(waiter)return Promise.reject(Error('Only one terminal event reader is allowed.'));return new Promise(resolve=>{waiter=resolve;});}};
71
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"