@yunhozz/arc 0.0.0-stage → 0.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.
- package/MANIFEST.sha256 +96 -0
- package/README.md +265 -2
- package/bin/arc.mjs +3 -0
- package/claude/agents/arc-coder.md +67 -0
- package/claude/agents/arc-planner.md +74 -0
- package/claude/agents/arc-qa.md +68 -0
- package/claude/agents/arc-reviewer.md +65 -0
- package/claude/agents/arc-scouter.md +63 -0
- package/claude/skill/SKILL.md +370 -0
- package/claude/skill/hooks/git-policy.json +208 -0
- package/claude/skill/hooks/role-git-guard.mjs +98 -0
- package/claude/skill/models.toml +64 -0
- package/claude/skill/references/commit.md +77 -0
- package/claude/skill/references/git-recovery.md +46 -0
- package/claude/skill/references/handoff-prep.md +102 -0
- package/claude/skill/references/handoff.md +100 -0
- package/claude/skill/references/limits.md +42 -0
- package/claude/skill/references/planning.md +250 -0
- package/claude/skill/references/superpowers/LICENSE +21 -0
- package/claude/skill/references/superpowers/brainstorming/SKILL.md +225 -0
- package/claude/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
- package/claude/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
- package/claude/skill/references/superpowers/provenance.json +26 -0
- package/claude/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
- package/claude/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
- package/claude/skill/references/superpowers/writing-plans/SKILL.md +204 -0
- package/claude/skill/references/verification.md +125 -0
- package/claude/skill/references/visual-companion.md +16 -0
- package/claude/skill/scripts/apply-models.py +31 -0
- package/claude/skill/scripts/model_config.py +211 -0
- package/codex/agents/arc-coder.toml +37 -0
- package/codex/agents/arc-planner.toml +38 -0
- package/codex/agents/arc-qa.toml +24 -0
- package/codex/agents/arc-reviewer.toml +25 -0
- package/codex/agents/arc-scouter.toml +37 -0
- package/codex/skill/SKILL.md +356 -0
- package/codex/skill/models.toml +64 -0
- package/codex/skill/references/commit.md +100 -0
- package/codex/skill/references/git-recovery.md +64 -0
- package/codex/skill/references/handoff-prep.md +153 -0
- package/codex/skill/references/handoff-worktree.md +93 -0
- package/codex/skill/references/handoff.md +181 -0
- package/codex/skill/references/limits.md +43 -0
- package/codex/skill/references/planning.md +287 -0
- package/codex/skill/references/superpowers/LICENSE +21 -0
- package/codex/skill/references/superpowers/brainstorming/SKILL.md +225 -0
- package/codex/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
- package/codex/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
- package/codex/skill/references/superpowers/provenance.json +26 -0
- package/codex/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
- package/codex/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
- package/codex/skill/references/superpowers/writing-plans/SKILL.md +204 -0
- package/codex/skill/references/verification.md +163 -0
- package/codex/skill/references/visual-companion.md +23 -0
- package/codex/skill/scripts/apply-models.py +207 -0
- package/codex/skill/scripts/model_config.py +362 -0
- package/install.command +5 -0
- package/lib/cli/choices.mjs +47 -0
- package/lib/cli/completion.mjs +53 -0
- package/lib/cli/index.mjs +55 -0
- package/lib/cli/preset.mjs +29 -0
- package/lib/cli/progress.mjs +10 -0
- package/lib/cli/root.mjs +20 -0
- package/lib/cli/uninstall.mjs +17 -0
- package/lib/installer/acquisition.mjs +77 -0
- package/lib/installer/global-config.mjs +43 -0
- package/lib/installer/install.mjs +28 -0
- package/lib/installer/language.mjs +13 -0
- package/lib/installer/limits.mjs +21 -0
- package/lib/installer/manifest.mjs +37 -0
- package/lib/installer/package-references.mjs +32 -0
- package/lib/installer/plan.mjs +62 -0
- package/lib/installer/preset.mjs +26 -0
- package/lib/installer/runtime-config.mjs +85 -0
- package/lib/installer/shell-completion.mjs +29 -0
- package/lib/installer/stage-cleanup.mjs +40 -0
- package/lib/installer/superpowers.mjs +113 -0
- package/lib/installer/targets.mjs +55 -0
- package/lib/installer/transaction.mjs +91 -0
- package/lib/installer/uninstall.mjs +32 -0
- package/lib/limits.mjs +13 -0
- package/lib/models/config.mjs +58 -0
- package/lib/models/index.mjs +2 -0
- package/lib/models/profiles.mjs +111 -0
- package/lib/policy/claude-hook.mjs +40 -0
- package/lib/policy/render.mjs +74 -0
- package/lib/policy/role-policy.json +577 -0
- package/lib/safe-fs/index.mjs +67 -0
- package/lib/safe-fs/native.mjs +54 -0
- package/native/binding.c +157 -0
- package/native/binding.gyp +1 -0
- package/package.json +43 -4
- package/prebuilds/darwin-arm64/arc_safe_fs.node +0 -0
- package/prebuilds/darwin-x64/arc_safe_fs.node +0 -0
- package/prebuilds/index.json +64 -0
- package/prebuilds/linux-arm64-gnu/arc_safe_fs.node +0 -0
- package/prebuilds/linux-x64-gnu/arc_safe_fs.node +0 -0
- package/scripts/build-native.mjs +36 -0
- package/scripts/check-installer-parity.mjs +59 -0
- package/scripts/update-manifest.mjs +28 -0
- package/scripts/update-role-policy.mjs +15 -0
- package/scripts/update-superpowers.mjs +4 -0
- package/scripts/vendor-toml.mjs +40 -0
- package/vendor/toml/LICENSE +24 -0
- package/vendor/toml/dist/date.js +156 -0
- package/vendor/toml/dist/error.js +67 -0
- package/vendor/toml/dist/extract.js +293 -0
- package/vendor/toml/dist/index.js +34 -0
- package/vendor/toml/dist/parse.js +164 -0
- package/vendor/toml/dist/primitive.js +151 -0
- package/vendor/toml/dist/stringify.js +219 -0
- package/vendor/toml/dist/struct.js +156 -0
- package/vendor/toml/dist/util.js +57 -0
- package/vendor/toml/package.json +6 -0
- package/vendor/toml/provenance.json +35 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {spawn as nodeSpawn} from 'node:child_process';
|
|
3
|
+
import {readCheckedFile,sameIdentity,typeOf} from './targets.mjs';
|
|
4
|
+
|
|
5
|
+
import {REQUIRED,SKILLS,snapshotSkill,createReferenceBundle} from './superpowers.mjs';
|
|
6
|
+
const ARGV=['clone','--depth','1','--single-branch','--no-tags','--','https://github.com/obra/superpowers'];
|
|
7
|
+
const defaultClock={setTimeout:(fn,ms)=>setTimeout(fn,ms),clearTimeout:id=>clearTimeout(id)};
|
|
8
|
+
export class AcquisitionError extends Error{
|
|
9
|
+
constructor(message,{code='ARC_ACQUISITION',cause,residualPaths}={}){super(message,{cause});this.name='AcquisitionError';this.code=code;this.exitCode=code==='ARC_ACQUISITION_CANCELLED'?130:1;if(residualPaths)this.residualPaths=residualPaths;}
|
|
10
|
+
}
|
|
11
|
+
const failure=message=>{throw new AcquisitionError(message);};
|
|
12
|
+
function decode(bytes,stream){try{return new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes);}catch{failure(`Upstream Git returned non-UTF-8 ${stream}`);}}
|
|
13
|
+
function environment(work){
|
|
14
|
+
const result={};for(const key of ['PATH','LANG','LC_ALL'])if(process.env[key]!==undefined)result[key]=process.env[key];
|
|
15
|
+
Object.assign(result,{CI:'1',DO_NOT_TRACK:'1',HOME:path.join(work,'home'),XDG_CONFIG_HOME:path.join(work,'config'),CODEX_HOME:path.join(work,'codex-runtime'),TMPDIR:path.join(work,'tmp'),XDG_STATE_HOME:path.join(work,'state'),npm_config_cache:path.join(work,'npm-cache'),npm_config_userconfig:path.join(work,'npm-userconfig'),npm_config_globalconfig:path.join(work,'npm-globalconfig'),npm_config_audit:'false',npm_config_fund:'false',npm_config_update_notifier:'false',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:path.join(work,'gitconfig'),GIT_TERMINAL_PROMPT:'0',GIT_CONFIG_COUNT:'3',GIT_CONFIG_KEY_0:'core.hooksPath',GIT_CONFIG_VALUE_0:'/dev/null',GIT_CONFIG_KEY_1:'protocol.file.allow',GIT_CONFIG_VALUE_1:'never',GIT_CONFIG_KEY_2:'submodule.recurse',GIT_CONFIG_VALUE_2:'false'});
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function clearHeldDirectory(directory){
|
|
19
|
+
for(const name of directory.readDir()){
|
|
20
|
+
const before=directory.lstat(name);
|
|
21
|
+
if(typeOf(before)===0o040000){const child=directory.openDir(name);try{if(!sameIdentity(before,child.stat()))failure('Work entry changed before cleanup');clearHeldDirectory(child);}finally{child.close();}directory.rmdir(name);}
|
|
22
|
+
else directory.unlink(name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function groupExists(pid){if(!pid)return false;try{process.kill(-pid,0);return true;}catch(error){if(error.code==='ESRCH')return false;if(error.code==='EPERM')return true;throw error;}}
|
|
26
|
+
function signalGroup(pid,signal){if(!pid)return;try{process.kill(-pid,signal);}catch(error){if(error.code!=='ESRCH')throw error;}}
|
|
27
|
+
function groupGone(pid,clock){
|
|
28
|
+
if(!groupExists(pid))return Promise.resolve();
|
|
29
|
+
return new Promise((resolve,reject)=>{let poll,done=false;const finish=error=>{if(done)return;done=true;clock.clearTimeout(poll);clock.clearTimeout(deadline);if(error)reject(error);else resolve();};const check=()=>{try{if(!groupExists(pid))finish();else poll=clock.setTimeout(check,20);}catch(error){finish(error);}};const deadline=clock.setTimeout(()=>{try{finish(groupExists(pid)?new AcquisitionError('External skill descendants did not terminate'):undefined);}catch(error){finish(error);}},5000);check();});
|
|
30
|
+
}
|
|
31
|
+
async function terminateGroup(child,completed,clock){
|
|
32
|
+
const pid=child?.pid;if(!groupExists(pid))return;
|
|
33
|
+
signalGroup(pid,'SIGTERM');let timer;const grace=new Promise(resolve=>{timer=clock.setTimeout(resolve,5000);});
|
|
34
|
+
try{await Promise.race([completed,grace]);if(groupExists(pid))await grace;if(groupExists(pid))signalGroup(pid,'SIGKILL');await groupGone(pid,clock);await completed;}
|
|
35
|
+
finally{clock.clearTimeout(timer);}
|
|
36
|
+
}
|
|
37
|
+
export async function acquireExternalSkills({work,safeFs,spawn=nodeSpawn,signal,clock=defaultClock}){
|
|
38
|
+
let root,owned=false,transferred=false,child,completed,timer,onAbort,termination;try{
|
|
39
|
+
if(typeof work!=='string'||!path.isAbsolute(work)||path.normalize(work)!==work)failure('Acquisition work must be an absolute normalized directory');
|
|
40
|
+
root=safeFs.openRoot(work);const identity=root.stat();if(root.readDir().length)failure('Acquisition work directory must be empty');owned=true;
|
|
41
|
+
if(signal?.aborted)throw new AcquisitionError('External skill acquisition cancelled',{code:'ARC_ACQUISITION_CANCELLED'});
|
|
42
|
+
for(const name of ['home','config','codex-runtime','npm-cache','state','tmp'])root.mkdir(name,0o700);
|
|
43
|
+
for(const name of ['npm-userconfig','npm-globalconfig','gitconfig']){const file=root.openFile(name,{createExclusive:true,mode:0o600});file.close();}
|
|
44
|
+
async function run(args){
|
|
45
|
+
termination=undefined;
|
|
46
|
+
const stdout=[],stderr=[];let startError;
|
|
47
|
+
child=spawn('git',args,{cwd:work,env:environment(work),stdio:['ignore','pipe','pipe'],detached:true,shell:false});
|
|
48
|
+
completed=new Promise(resolve=>{child.stdout.on('data',bytes=>stdout.push(Buffer.from(bytes)));child.stderr.on('data',bytes=>stderr.push(Buffer.from(bytes)));child.once('error',error=>{startError=error;});child.once('close',(code,killedBy)=>resolve({code,killedBy}));});
|
|
49
|
+
const interrupted=new Promise((resolve,reject)=>{timer=clock.setTimeout(()=>reject(new AcquisitionError('External skill acquisition timed out after 300 seconds',{code:'ARC_ACQUISITION_TIMEOUT'})),300000);onAbort=()=>reject(new AcquisitionError('External skill acquisition cancelled',{code:'ARC_ACQUISITION_CANCELLED'}));signal?.addEventListener('abort',onAbort,{once:true});if(signal?.aborted)onAbort();});
|
|
50
|
+
const result=await Promise.race([completed,interrupted]);clock.clearTimeout(timer);termination=terminateGroup(child,completed,clock);await termination;
|
|
51
|
+
if(signal?.aborted)throw new AcquisitionError('External skill acquisition cancelled',{code:'ARC_ACQUISITION_CANCELLED'});
|
|
52
|
+
if(startError)throw new AcquisitionError(`Cannot start upstream Git: ${startError.message}`,{cause:startError});
|
|
53
|
+
const errorText=decode(Buffer.concat(stderr),'stderr');if(result.code!==0)failure(`Upstream Git failed with exit code ${result.code}${errorText.trim()?': '+errorText.trim():''}`);
|
|
54
|
+
signal?.removeEventListener('abort',onAbort);onAbort=undefined;
|
|
55
|
+
return decode(Buffer.concat(stdout),'stdout');
|
|
56
|
+
}
|
|
57
|
+
await run([...ARGV,path.join(work,'upstream')]);
|
|
58
|
+
const revision=(await run(['-C',path.join(work,'upstream'),'rev-parse','HEAD'])).trim();
|
|
59
|
+
if(!/^[a-f0-9]{40,64}$/.test(revision))failure('Git returned an invalid revision');
|
|
60
|
+
const current=safeFs.openRoot(work);try{const now=current.stat();if(now.dev!==identity.dev||now.ino!==identity.ino)failure('Acquisition work path was replaced');}finally{current.close();}
|
|
61
|
+
const agents=root.openDir('upstream');let directory;try{directory=agents.openDir('skills');}finally{agents.close();}
|
|
62
|
+
const skills=new Map();try{for(const name of SKILLS){const skill=directory.openDir(name);try{skills.set(name,snapshotSkill(skill,path.join(work,'upstream/skills',name),REQUIRED[name]));}finally{skill.close();}}}finally{directory.close();}
|
|
63
|
+
const upstream=root.openDir('upstream');let license;try{license=readCheckedFile(upstream,'LICENSE').bytes;}finally{upstream.close();}
|
|
64
|
+
const references=createReferenceBundle({skills,license,revision});
|
|
65
|
+
let disposed=false;
|
|
66
|
+
const dispose=async()=>{
|
|
67
|
+
if(disposed)return;disposed=true;
|
|
68
|
+
try{clearHeldDirectory(root);}
|
|
69
|
+
catch(cause){throw new AcquisitionError('Cannot dispose private acquisition work; preserve residual work',{code:'ARC_ACQUISITION_CLEANUP',cause,residualPaths:[work]});}
|
|
70
|
+
finally{root.close();}
|
|
71
|
+
};
|
|
72
|
+
transferred=true;return{work,skills,references,dispose};
|
|
73
|
+
}catch(cause){let error=cause instanceof AcquisitionError?cause:new AcquisitionError(`External skill acquisition failed: ${cause.message}`,{cause});
|
|
74
|
+
try{if(child&&completed){termination??=terminateGroup(child,completed,clock);await termination;}if(owned)clearHeldDirectory(root);}catch(cleanup){error=new AcquisitionError(`Acquisition cleanup failed; preserve residual work: ${work}`,{code:'ARC_ACQUISITION_CLEANUP',cause:cleanup,residualPaths:[work]});error.work=work;}
|
|
75
|
+
throw error;
|
|
76
|
+
}finally{if(timer!==undefined)clock.clearTimeout(timer);if(onAbort)signal?.removeEventListener('abort',onAbort);if(!transferred)root?.close();}
|
|
77
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import{defaultRolePolicy,renderPolicySection}from'../policy/render.mjs';
|
|
2
|
+
import{languagePolicy}from'./language.mjs';
|
|
3
|
+
import{isDeepStrictEqual}from'node:util';import{parseToml}from'../models/config.mjs';
|
|
4
|
+
export class GlobalConfigurationError extends Error{constructor(message,options){super(message,options);this.name='GlobalConfigurationError';this.code='ARC_GLOBAL_CONFIGURATION';}}
|
|
5
|
+
const fail=message=>{throw new GlobalConfigurationError(message);};
|
|
6
|
+
const BEGIN='<!-- arc:begin -->',END='<!-- arc:end -->';
|
|
7
|
+
function decode(bytes){if(bytes===null)return'';if(!Buffer.isBuffer(bytes))fail('Global configuration must be read as bytes');try{return new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes);}catch{fail('Global file is not UTF-8; fix it or use --skip-global');}}
|
|
8
|
+
export function removeManagedBlock(original){return removeBlock(original,false,true);}
|
|
9
|
+
function removeBlock(original,consumeEol,consumeTerminalEol=false){
|
|
10
|
+
const text=decode(original);
|
|
11
|
+
if(/<!--\s*[A-Za-z0-9_-]+\s*:\s*begin\b[^\r\n]*managed\s+by\s+install\.py/i.test(text)||/<!--\s*(?!arc\s*:)[A-Za-z0-9_-]+\s*:\s*(?:begin|end)\b/i.test(text))fail('Unsupported prior managed markers; automatic migration is not supported');
|
|
12
|
+
const begins=text.split(BEGIN).length-1,ends=text.split(END).length-1,starts=[...text.matchAll(/<!--\s*arc\b/gi)].length;
|
|
13
|
+
if(starts!==begins+ends||begins!==ends||begins>1||(begins&&text.indexOf(BEGIN)>text.indexOf(END)))fail('Malformed ARC managed markers');
|
|
14
|
+
let rest=text;if(begins){const start=text.indexOf(BEGIN);let stop=text.indexOf(END)+END.length;if(consumeEol||consumeTerminalEol&&/^(?:\r\n|\n)*$/.test(text.slice(stop))){if(text.slice(stop,stop+2)==='\r\n')stop+=2;else if(text[stop]==='\n')stop++;}rest=text.slice(0,start)+text.slice(stop);}
|
|
15
|
+
return{content:Buffer.from(rest),changed:Boolean(begins)};
|
|
16
|
+
}
|
|
17
|
+
export function renderManagedBlock(original,body){
|
|
18
|
+
const text=decode(original),eol=/\r?\n/.exec(text)?.[0]??'\n';
|
|
19
|
+
const {content:removed}=removeBlock(original,true);
|
|
20
|
+
const begins=text.includes(BEGIN);let rest=removed.toString('utf8');
|
|
21
|
+
const outside=rest;
|
|
22
|
+
if(rest&&!rest.endsWith('\n'))rest+=eol;
|
|
23
|
+
const separator=!rest||rest.endsWith(eol+eol)?'':eol;
|
|
24
|
+
const normalizedBody=body.replace(/\r?\n/g,eol);const block=BEGIN+eol+'## Agent Role Coordination'+eol+eol+normalizedBody+(normalizedBody.endsWith(eol)?'':eol)+END+eol;
|
|
25
|
+
return{content:Buffer.from(rest+separator+block),action:original===null?'create with managed ARC block':begins?'update managed ARC block at the end':'append managed ARC block',notes:/^(#{1,6})[ \t]+(?:\d+\.[ \t]*)?(?:ARC|Agent Role Coordination)[ \t]*\r?$/im.test(outside)?['An existing ARC section outside the managed block was preserved']:[]};
|
|
26
|
+
}
|
|
27
|
+
export function renderWaitSettings(original){
|
|
28
|
+
const text=decode(original);let data;try{data=parseToml(text);}catch(cause){throw new GlobalConfigurationError(`Cannot parse Codex config.toml: ${cause.message}`,{cause});}
|
|
29
|
+
const wanted={min_wait_timeout_ms:120000n,default_wait_timeout_ms:300000n};const features=data.features,current=features?.multi_agent_v2;
|
|
30
|
+
if(current&&typeof current==='object'&&Object.entries(wanted).every(([k,v])=>current[k]===v))return{content:Buffer.from(text),action:'unchanged'};
|
|
31
|
+
const manual=reason=>({content:null,action:`skip: set [features.multi_agent_v2] wait settings manually (${reason})`});
|
|
32
|
+
const lines=text.match(/[^\n]*\n|[^\n]+$/g)??[],eol=/\r?\n/.exec(text)?.[0]??'\n';
|
|
33
|
+
const index=lines.findIndex(line=>/^[ \t]*\[[ \t]*features[ \t]*\.[ \t]*multi_agent_v2[ \t]*\][ \t]*(?:#[^\r\n]*)?\r?\n?$/.test(line));
|
|
34
|
+
if(features!==undefined&&(!features||typeof features!=='object'||Array.isArray(features))||current!==undefined&&(!current||typeof current!=='object'||Array.isArray(current)||index<0))return manual('features.multi_agent_v2 is not its own table');
|
|
35
|
+
let updated;if(index<0){let prefix=text;if(prefix&&!prefix.endsWith('\n'))prefix+=eol;if(prefix&&!prefix.endsWith(eol+eol))prefix+=eol;updated=prefix+'[features.multi_agent_v2]'+eol+Object.entries(wanted).map(([k,v])=>`${k} = ${v}${eol}`).join('');}
|
|
36
|
+
else{if(!lines[index].endsWith('\n'))lines[index]+=eol;let end=lines.findIndex((line,i)=>i>index&&/^[ \t]*\[/.test(line));if(end<0)end=lines.length;const section=lines.slice(index+1,end),remaining=new Map(Object.entries(wanted));for(let i=0;i<section.length;i++){const m=/^([ \t]*(min_wait_timeout_ms|default_wait_timeout_ms)[ \t]*=[ \t]*)([^#\r\n]*?)([ \t]*(?:#[^\r\n]*)?)(\r?\n)?$/.exec(section[i]);if(m&&remaining.has(m[2])){section[i]=m[1]+remaining.get(m[2])+m[4]+(m[5]??'');remaining.delete(m[2]);}}
|
|
37
|
+
let insertion=section.length;while(insertion&&!section[insertion-1].trim())insertion--;if(insertion&& !section[insertion-1].endsWith('\n'))section[insertion-1]+=eol;section.splice(insertion,0,...[...remaining].map(([k,v])=>`${k} = ${v}${eol}`));updated=[...lines.slice(0,index+1),...section,...lines.slice(end)].join('');}
|
|
38
|
+
const expected=parseToml(text);expected.features??=Object.create(null);expected.features.multi_agent_v2??=Object.create(null);Object.assign(expected.features.multi_agent_v2,wanted);
|
|
39
|
+
try{if(!isDeepStrictEqual(parseToml(updated),expected))return manual('automatic edit could not be verified');}catch{return manual('automatic edit could not be verified');}
|
|
40
|
+
return{content:Buffer.from(updated),action:'set ARC wait settings'};
|
|
41
|
+
}
|
|
42
|
+
export function agentsBlockBody(home,userHome,language='en',platform='codex'){const skill=home+'/skills/arc/SKILL.md',within=skill.startsWith(userHome+'/'),shown=within?'~/'+skill.slice(userHome.length+1):skill;return '- '+languagePolicy(language)+'\n'+'- For requests requiring substantive multi-step research, analysis, or implementation, configuration, or test work, and for design, spec, plan, and brainstorming requests (these are NOT standalone document writing), load and follow `'+shown+'`; keep following it in relevant follow-up turns of the same chat without re-reading it unless it changed.'+(within?" Resolve `~` to the current user's home directory.":'')+' ARC subagents (depth 2) follow their role profile and brief instead and do not load `SKILL.md`.\n'+'- Design, spec, plan, and brainstorming drafts are authored ONLY by the Planner. Orchestrator MUST NEVER author them; route drafting skills to the Planner.\n'+
|
|
43
|
+
'- For repository implementation, configuration, or test work, Orchestrator owns coordination and acceptance; assign file edits and other work to the other roles according to their defined responsibilities. Orchestrator must not edit implementation, configuration, or test-code files directly, except under the ARC Orchestrator direct path for small, low-risk tasks.\n'+'\n[ARC role policy: global begin]\n'+renderPolicySection(defaultRolePolicy,{platform,audience:'orchestrator',section:'contract',skillPath:shown})+'\n[ARC role policy: global end]\n';}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {validatePlanSet} from './plan.mjs';
|
|
2
|
+
import {assertPlanSnapshot,applyTransaction} from './transaction.mjs';
|
|
3
|
+
function pending(plan){return{platform:plan.target.platform,status:'not-started',rollback:'not-needed',residualPaths:[],error:null};}
|
|
4
|
+
export async function installTargets(plans,{safeFs,acquisition,signal,hooks={}}){
|
|
5
|
+
const ordered=[...plans].sort((a,b)=>(a.target.platform==='codex'?0:1)-(b.target.platform==='codex'?0:1));const results=ordered.map(pending);let interrupted=false;
|
|
6
|
+
let invalid=false;
|
|
7
|
+
for(let index=0;index<ordered.length;index++){
|
|
8
|
+
try{validatePlanSet([ordered[index]]);assertPlanSnapshot(ordered[index],{safeFs});}catch(error){results[index]={...results[index],status:'failed',error};invalid=true;}
|
|
9
|
+
}
|
|
10
|
+
for(let first=0;first<ordered.length;first++)for(let second=first+1;second<ordered.length;second++){
|
|
11
|
+
if(results[first].status==='failed'||results[second].status==='failed')continue;
|
|
12
|
+
try{validatePlanSet([ordered[first],ordered[second]]);}catch(error){results[first]={...results[first],status:'failed',error};results[second]={...results[second],status:'failed',error};invalid=true;}
|
|
13
|
+
}
|
|
14
|
+
if(invalid)return{results,interrupted};
|
|
15
|
+
for(let index=0;index<ordered.length;index++){
|
|
16
|
+
const plan=ordered[index];if(signal?.aborted){interrupted=true;break;}
|
|
17
|
+
let acquired,failed=false;
|
|
18
|
+
try{
|
|
19
|
+
hooks.fault?.({phase:'before-acquisition',plan});assertPlanSnapshot(plan,{safeFs});
|
|
20
|
+
acquired=await acquisition(plan,{signal});
|
|
21
|
+
hooks.fault?.({phase:'after-acquisition',plan});assertPlanSnapshot(plan,{safeFs});
|
|
22
|
+
const installed=applyTransaction(plan,{safeFs,acquired,signal,hooks});
|
|
23
|
+
results[index]={...results[index],status:'installed',error:installed.cleanupError??null,residualPaths:[...results[index].residualPaths,...(installed.residualPaths??[])]};if(installed.cleanupError)failed=true;
|
|
24
|
+
}catch(error){const cancelled=signal?.aborted||error.code==='ARC_ACQUISITION_CANCELLED'||error.code==='ARC_INSTALL_CANCELLED';interrupted=Boolean(cancelled);results[index]={...results[index],status:'failed',rollback:error.rollback??'not-needed',residualPaths:[...results[index].residualPaths,...(error.residualPaths??[])],error};failed=true;}finally{if(acquired?.dispose){try{const disposal=await acquired.dispose();if(disposal?.residualPaths)results[index].residualPaths=[...new Set([...results[index].residualPaths,...disposal.residualPaths])];}catch(cleanup){const result=results[index];result.error=result.error?new AggregateError([result.error,cleanup],'Installation and acquisition cleanup reported failures'):cleanup;result.residualPaths=[...new Set([...result.residualPaths,...(cleanup.residualPaths??(acquired.work?[acquired.work]:[]))])];failed=true;}}}
|
|
25
|
+
if(failed)break;
|
|
26
|
+
}
|
|
27
|
+
return{results,interrupted};
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {parseToml} from '../models/config.mjs';
|
|
2
|
+
const MARKER='{{ARC_OUTPUT_LANGUAGE_POLICY}}';
|
|
3
|
+
export function validateLanguage(language){if(!['en','ko'].includes(language))throw new Error(`Invalid language: ${language}`);return language;}
|
|
4
|
+
export function parseLanguage(bytes){
|
|
5
|
+
if(!Buffer.isBuffer(bytes))throw new Error('Language settings must be bytes');
|
|
6
|
+
const data=parseToml(new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes));
|
|
7
|
+
if(Object.keys(data).length!==2||!Object.hasOwn(data,'version')||!Object.hasOwn(data,'language')||data.version!==1n&&data.version!==1)throw new Error('Invalid language settings schema or version');
|
|
8
|
+
return{version:1,language:validateLanguage(data.language)};
|
|
9
|
+
}
|
|
10
|
+
export function serializeLanguage(language){return Buffer.from(`version = 1\nlanguage = "${validateLanguage(language)}"\n`);}
|
|
11
|
+
export function defaultLanguage(existing){return existing.length&&existing.every(value=>value===existing[0])&&existing[0]?validateLanguage(existing[0]):'en';}
|
|
12
|
+
export function languagePolicy(language){return `Use ${validateLanguage(language)==='ko'?'Korean':'English'} by default for user-facing responses, progress updates, reports, and document titles and prose, including designs, specs, and plans. An explicit user language request overrides this installed default. Preserve identifiers, skill/API names, code, paths, commands, and literal values exactly.`;}
|
|
13
|
+
export function renderLanguagePolicy(bytes,language){const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);if(text.split(MARKER).length!==2)throw new Error('Expected exactly one ARC output language policy marker');return Buffer.from(text.replace(MARKER,languagePolicy(language)));}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {parseToml} from '../models/config.mjs';
|
|
2
|
+
import {resolveLimits} from '../limits.mjs';
|
|
3
|
+
function validate(data) {
|
|
4
|
+
const keys=['version','max_total_subagents','max_concurrent_subagents'];
|
|
5
|
+
if(!data||Object.keys(data).length!==3||keys.some(key=>!Object.hasOwn(data,key))||data.version!==1n&&data.version!==1)throw new Error('Invalid limits schema or version');
|
|
6
|
+
const value={version:1};
|
|
7
|
+
for(const key of keys.slice(1)) {
|
|
8
|
+
const raw=data[key];
|
|
9
|
+
if(typeof raw!=='bigint'&&typeof raw!=='number'||raw<1||raw>Number.MAX_SAFE_INTEGER)throw new Error(`Invalid ${key}`);
|
|
10
|
+
value[key]=Number(raw);
|
|
11
|
+
}
|
|
12
|
+
return resolveLimits({existing:value});
|
|
13
|
+
}
|
|
14
|
+
export function parseLimits(bytes) {
|
|
15
|
+
if(!Buffer.isBuffer(bytes))throw new Error('Limits must be bytes');
|
|
16
|
+
return validate(parseToml(new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes)));
|
|
17
|
+
}
|
|
18
|
+
export function serializeLimits(limits) {
|
|
19
|
+
const value=validate(limits);
|
|
20
|
+
return Buffer.from(`version = 1\nmax_total_subagents = ${value.max_total_subagents}\nmax_concurrent_subagents = ${value.max_concurrent_subagents}\n`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {POLICY_TARGETS,buildPolicyOutputs} from '../policy/render.mjs';
|
|
2
|
+
import {isDynamicReferencePath,readReferenceBundle} from './superpowers.mjs';
|
|
3
|
+
import path from 'node:path';import crypto from 'node:crypto';import{loadModelConfig,resolveModelSettings,renderProfiles}from '../models/index.mjs';import{readCheckedFile,typeOf,sameIdentity,metadataOf}from './targets.mjs';
|
|
4
|
+
export class PayloadValidationError extends Error{constructor(message,options){super(message,options);this.name='PayloadValidationError';this.code='ARC_PAYLOAD_VALIDATION';}}
|
|
5
|
+
const MUTABLE=new Set(['codex/skill/models.toml','claude/skill/models.toml']);
|
|
6
|
+
const ROOTS=['bin','lib','scripts','vendor','prebuilds','codex/skill','codex/agents','claude/skill','claude/agents'];
|
|
7
|
+
const FIXED=['README.md','install.command','package.json','native/binding.c','native/binding.gyp'];
|
|
8
|
+
function safeName(name){return typeof name==='string'&&name.length>0&&!name.includes('\\')&&!name.includes('\0')&&!name.startsWith('/')&&name.split('/').every(p=>p&&p!=='.'&&p!=='..')&&path.posix.normalize(name)===name;}
|
|
9
|
+
function allowed(name){return FIXED.includes(name)||ROOTS.some(root=>name.startsWith(root+'/'));}
|
|
10
|
+
export function verifyPayload({packageRoot,safeFs}){
|
|
11
|
+
let root;try{
|
|
12
|
+
root=safeFs.openRoot(packageRoot);const manifest=readCheckedFile(root,'MANIFEST.sha256').bytes;let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(manifest);}catch{throw Error('Manifest is not UTF-8');}
|
|
13
|
+
const listed=new Map();for(const line of text.split(/\r?\n/)){if(!line)continue;const m=/^([a-fA-F0-9]{64}) (.+)$/.exec(line);if(!m||!safeName(m[2]))throw Error('Malformed or escaping manifest path');const name=m[2];if(MUTABLE.has(name)||isDynamicReferencePath(name)||!allowed(name)||name.split('/').includes('.DS_Store')||listed.has(name))throw Error(`Duplicate, mutable or unexpected manifest entry: ${name}`);listed.set(name,m[1].toLowerCase());}
|
|
14
|
+
const files=new Map(),snapshot=new Map(),referenceTrees={};
|
|
15
|
+
function walk(directory,prefix){const before=directory.stat();for(const name of directory.readDir()){
|
|
16
|
+
const relative=prefix?prefix+'/'+name:name;const stat=directory.lstat(name),type=typeOf(stat);
|
|
17
|
+
if(name==='.DS_Store'){if(type!==0o100000)throw Error('DS_Store must be a regular file');continue;}
|
|
18
|
+
if(type===0o040000){const sub=directory.openDir(name);try{if(!sameIdentity(stat,sub.stat()))throw Error('Payload directory changed');snapshot.set(relative,{...metadataOf(stat),type:'directory'});if(isDynamicReferencePath(relative))referenceTrees[relative.split('/')[0]]=readReferenceBundle({safeFs,root:path.join(packageRoot,relative),directory:sub});else walk(sub,relative);}finally{sub.close();}}
|
|
19
|
+
else if(type===0o100000){const item=readCheckedFile(directory,name,stat);if(!MUTABLE.has(relative)&&(!listed.has(relative)||crypto.createHash('sha256').update(item.bytes).digest('hex')!==listed.get(relative)))throw Error(`Missing entry or hash mismatch: ${relative}`);files.set(relative,item.bytes);snapshot.set(relative,item.metadata);}
|
|
20
|
+
else throw Error(`Unsafe payload type: ${relative}`);
|
|
21
|
+
}if(!sameIdentity(before,directory.stat()))throw Error('Payload directory changed while reading');}
|
|
22
|
+
function select(relative,isTree){const parts=relative.split('/');let parent=root;const opened=[];try{for(const component of parts.slice(0,-1)){let st;try{st=parent.lstat(component);}catch(e){if(e.code==='ENOENT')return;throw e;}if(typeOf(st)!==0o040000)throw Error('Unsafe payload parent');const next=parent.openDir(component);if(!sameIdentity(st,next.stat())){next.close();throw Error('Payload parent changed');}opened.push(next);parent=next;}const name=parts.at(-1);let st;try{st=parent.lstat(name);}catch(e){if(e.code==='ENOENT')return;throw e;}if(isTree){if(typeOf(st)!==0o040000)throw Error('Payload root is not a directory');const sub=parent.openDir(name);try{if(!sameIdentity(st,sub.stat()))throw Error('Payload root changed');snapshot.set(relative,{...metadataOf(st),type:'directory'});if(isDynamicReferencePath(relative))referenceTrees[relative.split('/')[0]]=readReferenceBundle({safeFs,root:path.join(packageRoot,relative),directory:sub});else walk(sub,relative);}finally{sub.close();}}else{if(typeOf(st)!==0o100000)throw Error('Payload file is not regular');const item=readCheckedFile(parent,name,st);if(!listed.has(relative)||crypto.createHash('sha256').update(item.bytes).digest('hex')!==listed.get(relative))throw Error(`Missing entry or hash mismatch: ${relative}`);files.set(relative,item.bytes);snapshot.set(relative,item.metadata);}}finally{for(const h of opened.reverse())h.close();}}
|
|
23
|
+
for(const item of FIXED)select(item,false);for(const item of ROOTS)select(item,true);
|
|
24
|
+
for(const platform of ['codex','claude'])if(!referenceTrees[platform])throw Error('Required superpowers reference tree is missing');
|
|
25
|
+
for(const name of listed.keys())if(!files.has(name))throw Error(`Manifest entry is missing: ${name}`);
|
|
26
|
+
const required=['claude/skill/hooks/role-git-guard.mjs','claude/skill/hooks/git-policy.json','lib/policy/claude-hook.mjs','lib/policy/role-policy.json','lib/policy/render.mjs','scripts/update-role-policy.mjs','lib/limits.mjs','lib/installer/limits.mjs','lib/installer/runtime-config.mjs','README.md','install.command','package.json','bin/arc.mjs','lib/cli/root.mjs','lib/cli/completion.mjs','lib/installer/shell-completion.mjs','lib/installer/manifest.mjs','lib/installer/targets.mjs','lib/installer/global-config.mjs','lib/installer/plan.mjs','lib/safe-fs/index.mjs','lib/safe-fs/native.mjs','lib/models/index.mjs','lib/models/config.mjs','lib/models/profiles.mjs','vendor/toml/LICENSE','vendor/toml/package.json','vendor/toml/provenance.json','vendor/toml/dist/index.js','prebuilds/index.json'];
|
|
27
|
+
for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
|
|
28
|
+
const modelInputs={};for(const platform of ['codex','claude']){const base=`${platform}/skill`;required.push(`${base}/SKILL.md`,`${base}/models.toml`,`${base}/references/limits.md`);const profiles=new Map();for(const role of ['scouter','planner','coder','qa','reviewer']){const name=`arc-${role}.${platform==='codex'?'toml':'md'}`,key=`${platform}/agents/${name}`;required.push(key);profiles.set(name,files.get(key));}modelInputs[platform]={models:files.get(`${base}/models.toml`),profiles};}
|
|
29
|
+
for(const name of required)if(!files.has(name))throw Error(`Required runtime asset is missing: ${name}`);
|
|
30
|
+
const policyInputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,files.get(name)]));
|
|
31
|
+
const policyOutputs=buildPolicyOutputs(policyInputs,JSON.parse(files.get('lib/policy/role-policy.json').toString('utf8')));
|
|
32
|
+
for(const [name,bytes] of policyOutputs)if(!bytes.equals(files.get(name)))throw Error(`Stale role policy output: ${name}`);
|
|
33
|
+
const vendor=JSON.parse(files.get('vendor/toml/provenance.json'));for(const name of Object.keys(vendor.files??{}))if(!safeName(name)||!files.has('vendor/toml/'+name))throw Error('Vendored runtime asset is missing');
|
|
34
|
+
for(const platform of ['codex','claude']){const input=modelInputs[platform];const config=loadModelConfig(input.models,platform);renderProfiles(input.profiles,resolveModelSettings(config));modelInputs[platform]={models:Buffer.from(input.models),profiles:new Map([...input.profiles].map(([k,v])=>[k,Buffer.from(v)]))};}
|
|
35
|
+
return{packageRoot,files,modelInputs,snapshot,referenceTrees};
|
|
36
|
+
}catch(cause){throw new PayloadValidationError(`Payload verification failed: ${cause.message}`,{cause});}finally{root?.close();}
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {openRoot} from '../safe-fs/index.mjs';
|
|
5
|
+
import {loadNative} from '../safe-fs/native.mjs';
|
|
6
|
+
import {acquireExternalSkills} from './acquisition.mjs';
|
|
7
|
+
import {DYNAMIC_ROOTS,validateReferenceBundle} from './superpowers.mjs';
|
|
8
|
+
import {inspectPath} from './targets.mjs';
|
|
9
|
+
import {applyTransaction} from './transaction.mjs';
|
|
10
|
+
|
|
11
|
+
export async function refreshPackageReferences({packageRoot,safeFs,acquisition,signal,hooks={}}){
|
|
12
|
+
if(!safeFs){const native=loadNative({root:packageRoot});safeFs={openRoot:p=>openRoot(p,{native})};}
|
|
13
|
+
const paths=DYNAMIC_ROOTS.map(root=>path.join(packageRoot,root)),snapshot=new Map();
|
|
14
|
+
for(const filename of paths)inspectPath(safeFs,filename,{kind:'directory',tree:true,allowLinks:true,snapshot});
|
|
15
|
+
let acquired;
|
|
16
|
+
try{
|
|
17
|
+
if(acquisition)acquired=await acquisition({signal});
|
|
18
|
+
else{
|
|
19
|
+
const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-package-acquire-'));
|
|
20
|
+
try{acquired=await acquireExternalSkills({work,safeFs,signal});}catch(error){if(!error.residualPaths?.length)fs.rmdirSync(work);throw error;}
|
|
21
|
+
const dispose=acquired.dispose;acquired.dispose=async()=>{await dispose();fs.rmdirSync(work);};
|
|
22
|
+
}
|
|
23
|
+
validateReferenceBundle(acquired.references);
|
|
24
|
+
const plan={target:{home:packageRoot},managedPaths:paths,externalPaths:[],snapshot};
|
|
25
|
+
const result=applyTransaction(plan,{safeFs,signal,hooks,specs:paths.map(filename=>({path:filename,tree:acquired.references.entries}))});
|
|
26
|
+
if(result.cleanupError)throw result.cleanupError;
|
|
27
|
+
return{...result,provenance:acquired.references.provenance};
|
|
28
|
+
}catch(error){
|
|
29
|
+
if(acquired?.dispose){const owned=acquired;acquired=undefined;try{await owned.dispose();}catch(cleanup){throw Object.assign(new AggregateError([error,cleanup],'Reference refresh and cleanup failed'),{residualPaths:[...new Set([...(error.residualPaths??[]),...(cleanup.residualPaths??[])])]});}}
|
|
30
|
+
throw error;
|
|
31
|
+
}finally{if(acquired?.dispose)await acquired.dispose();}
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {installClaudeHook} from '../policy/claude-hook.mjs';
|
|
2
|
+
import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
|
|
3
|
+
import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
|
|
4
|
+
import{resolveLimits}from'../limits.mjs';import{parseLimits,serializeLimits}from'./limits.mjs';import{readRuntimeConcurrency,renderCodexConcurrency,renderClaudeConcurrency}from'./runtime-config.mjs';import{assertPlanSnapshot}from'./transaction.mjs';
|
|
5
|
+
import path from'node:path';import{loadModelConfig,applyPreset,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
|
|
6
|
+
function fail(message){throw new TargetConfigurationError(message);}
|
|
7
|
+
export function previewLimits({target,safeFs}){
|
|
8
|
+
const snapshot=new Map(),filename=path.join(target.home,'skills/arc/limits.toml');
|
|
9
|
+
const current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});
|
|
10
|
+
let existing=null;try{if(current.exists)existing=parseLimits(current.bytes);}catch(cause){fail(`Invalid ARC limits: ${cause.message}`);}
|
|
11
|
+
return{existing,snapshot};
|
|
12
|
+
}
|
|
13
|
+
export function previewLanguage({target,safeFs}){
|
|
14
|
+
const snapshot=new Map(),filename=path.join(target.home,'skills/arc/settings.toml');
|
|
15
|
+
const current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});
|
|
16
|
+
let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
|
|
17
|
+
return{existing,snapshot};
|
|
18
|
+
}
|
|
19
|
+
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview}){
|
|
20
|
+
validateLanguage(language);
|
|
21
|
+
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
22
|
+
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
23
|
+
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
24
|
+
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
25
|
+
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
26
|
+
const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
|
|
27
|
+
const config=applyPreset(loadModelConfig(input.models,target.platform),preset),profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
|
|
28
|
+
if(target.platform==='claude'){try{const script=payload.files.get('claude/skill/hooks/role-git-guard.mjs'),policy=payload.files.get('claude/skill/hooks/git-policy.json');if(!Buffer.isBuffer(script)||!script.length||!Buffer.isBuffer(policy))throw Error('Missing hook assets');validateGitPolicy(JSON.parse(policy.toString('utf8')));for(const[name,bytes]of profiles)profiles.set(name,installClaudeHook(bytes,path.join(target.home,'skills/arc')));}catch(cause){fail(`Invalid Claude Git guard: ${cause.message}`);}}
|
|
29
|
+
const skill=new Map();for(const[name,bytes]of payload.files)if(name.startsWith(target.payloadPrefix+'/skill/'))skill.set(name.slice((target.payloadPrefix+'/skill/').length),Buffer.from(bytes));skill.set('SKILL.md',renderLanguagePolicy(skill.get('SKILL.md'),language));skill.set('settings.toml',serializeLanguage(language));skill.set('models.toml',modelBytes);skill.set('limits.toml',serializeLimits(limits));
|
|
30
|
+
const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
|
|
31
|
+
for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
|
|
32
|
+
inspectPath(safeFs,arc,{kind:'directory',tree:true,allowLinks:true,snapshot});
|
|
33
|
+
const profilePaths=target.profileNames.map(name=>path.join(agents,name));for(const name of profilePaths)inspectPath(safeFs,name,{kind:'file',snapshot});
|
|
34
|
+
const externalPaths=[],summary=['Fresh superpowers references will be acquired inside ARC'];
|
|
35
|
+
const globalChanges=[];summary.push(`ARC limits: total ${limits.max_total_subagents}; concurrent ${limits.max_concurrent_subagents} (policy: ${arc}/limits.toml)`);
|
|
36
|
+
if(limits.max_concurrent_subagents>limits.max_total_subagents)summary.push(`Effective concurrency is at most ${limits.max_total_subagents} for a fresh ARC session; it may be lower after cumulative use or due to native capacity.`);
|
|
37
|
+
if(skipGlobal)summary.push('Native concurrency update skipped (--skip-global).');
|
|
38
|
+
else for(const filename of target.globalPaths){
|
|
39
|
+
const current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot}),original=current.bytes??null;
|
|
40
|
+
let rendered;
|
|
41
|
+
if(filename===target.instructionPath)rendered=renderManagedBlock(original,agentsBlockBody(home,target.userHome,language,target.platform));
|
|
42
|
+
else{
|
|
43
|
+
let previous;try{previous=readRuntimeConcurrency(target.platform,original);}catch(cause){fail(`Invalid native configuration: ${cause.message}`);}
|
|
44
|
+
if(preview.existing&&previous!==null&&previous!==preview.existing.max_concurrent_subagents&&!concurrentExplicit)fail(`Native concurrency drift at ${filename}; explicitly select --max-concurrent-subagents to update it`);
|
|
45
|
+
let base=original;if(target.platform==='codex'){const wait=renderWaitSettings(original);base=wait.content??original;if(wait.content===null)summary.push(`${filename}: ${wait.action}`);}
|
|
46
|
+
try{rendered=target.platform==='codex'?renderCodexConcurrency(base,limits.max_concurrent_subagents):renderClaudeConcurrency(base,limits.max_concurrent_subagents);}catch(cause){fail(`Cannot merge native concurrency: ${cause.message}`);}
|
|
47
|
+
summary.push(`${filename}: native concurrency ${previous??'missing'} -> ${limits.max_concurrent_subagents}`);
|
|
48
|
+
}
|
|
49
|
+
if(rendered.content!==null&&(original===null||!rendered.content.equals(original)))globalChanges.push({path:filename,original,content:rendered.content,action:rendered.action});
|
|
50
|
+
for(const note of rendered.notes??[])summary.push(`${filename}: ${note}`);
|
|
51
|
+
}
|
|
52
|
+
const managedPaths=[arc,...profilePaths,...(skipGlobal?[]:target.globalPaths)];
|
|
53
|
+
for(const [filename,metadata]of preview.snapshot)snapshot.set(filename,metadata);
|
|
54
|
+
for(const [filename,metadata]of languageState.snapshot)snapshot.set(filename,metadata);
|
|
55
|
+
summary.push(`ARC output language: ${language}`);
|
|
56
|
+
const plan={target,language,preset,skipGlobal,limits,concurrentExplicit,generated:{models:modelBytes,profiles,skill},globalChanges,managedPaths,externalPaths,snapshot,summary};validatePlanSet([plan]);return plan;
|
|
57
|
+
}
|
|
58
|
+
function overlaps(a,b){return a===b||a.startsWith(b+'/')||b.startsWith(a+'/');}
|
|
59
|
+
export function validatePlanSet(plans){
|
|
60
|
+
const paths=[];for(let i=0;i<plans.length;i++){const plan=plans[i];if(!plan.target?.home||!path.isAbsolute(plan.target.home)||plan.target.home==='/')fail('Unsafe install home');for(let j=0;j<i;j++)if(overlaps(plan.target.home,plans[j].target.home))fail('Selected platform homes overlap');
|
|
61
|
+
for(const value of [...plan.managedPaths,...plan.externalPaths]){if(typeof value!=='string'||!path.isAbsolute(value)||value==='/'||path.normalize(value)!==value)fail('Unsafe planned write path');for(const previous of paths)if(overlaps(value,previous.path))fail(`Install write paths collide: ${value}`);paths.push({path:value,plan:i});}}
|
|
62
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
3
|
+
import {loadModelConfig,applyPreset,serializeModelConfig,resolveModelSettings,renderProfiles} from '../models/index.mjs';
|
|
4
|
+
import {assertPlanSnapshot,applyTransaction} from './transaction.mjs';
|
|
5
|
+
import {validatePlanSet} from './plan.mjs';
|
|
6
|
+
export function buildPresetPlan({target,preset,safeFs}){
|
|
7
|
+
const snapshot=new Map(),modelsPath=path.join(target.home,'skills/arc/models.toml'),managedPaths=[modelsPath,...target.profileNames.map(name=>path.join(target.home,'agents',name))];
|
|
8
|
+
const read=filename=>{const found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});if(!found.exists)throw new TargetConfigurationError(`Installed ARC file missing: ${filename}`);return found.bytes;};
|
|
9
|
+
const originalModels=read(modelsPath),config=loadModelConfig(originalModels,target.platform),next=applyPreset(config,preset),originalProfiles=new Map(target.profileNames.map((name,i)=>[name,read(managedPaths[i+1])]));
|
|
10
|
+
const profiles=renderProfiles(originalProfiles,resolveModelSettings(next)),specs=[];
|
|
11
|
+
if(config.preset!==preset)specs.push({path:modelsPath,bytes:Buffer.from(serializeModelConfig(next))});
|
|
12
|
+
for(const[name,bytes]of profiles)if(!bytes.equals(originalProfiles.get(name)))specs.push({path:path.join(target.home,'agents',name),bytes});
|
|
13
|
+
return{target,preset,snapshot,managedPaths,externalPaths:[],specs};
|
|
14
|
+
}
|
|
15
|
+
export async function switchPresetTargets(plans,{safeFs,signal,hooks={}}){
|
|
16
|
+
const results=plans.map(plan=>({platform:plan.target.platform,status:'not-started',rollback:'not-needed',residualPaths:[],error:null}));
|
|
17
|
+
try{validatePlanSet(plans);}catch(error){return{results:results.map(item=>({...item,status:'failed',error})),interrupted:false};}
|
|
18
|
+
let invalid=false;for(let i=0;i<plans.length;i++)try{assertPlanSnapshot(plans[i],{safeFs});}catch(error){results[i]={...results[i],status:'failed',error};invalid=true;}
|
|
19
|
+
if(invalid)return{results,interrupted:false};
|
|
20
|
+
for(let i=0;i<plans.length;i++){
|
|
21
|
+
if(signal?.aborted)return{results,interrupted:true};const plan=plans[i];
|
|
22
|
+
try{assertPlanSnapshot(plan,{safeFs});const applied=plan.specs.length?applyTransaction(plan,{safeFs,signal,hooks,specs:plan.specs}):{residualPaths:[],cleanupError:null};results[i]={...results[i],status:plan.specs.length?'updated':'unchanged',residualPaths:applied.residualPaths,error:applied.cleanupError};if(applied.cleanupError)break;}
|
|
23
|
+
catch(error){results[i]={...results[i],status:'failed',rollback:error.rollback??'not-needed',residualPaths:error.residualPaths??[],error};return{results,interrupted:Boolean(signal?.aborted)};}
|
|
24
|
+
}
|
|
25
|
+
return{results,interrupted:false};
|
|
26
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
2
|
+
import {parseToml} from '../models/config.mjs';
|
|
3
|
+
const KEY='max_concurrent_threads_per_session';
|
|
4
|
+
const ENV='CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS';
|
|
5
|
+
const fail=message=>{throw new Error(message);};
|
|
6
|
+
function decode(bytes) {
|
|
7
|
+
if(bytes===null)return '';
|
|
8
|
+
if(!Buffer.isBuffer(bytes))fail('Runtime configuration must be bytes');
|
|
9
|
+
return new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes);
|
|
10
|
+
}
|
|
11
|
+
function object(value){return value!==null&&typeof value==='object'&&!Array.isArray(value)&&!(value instanceof Date);}
|
|
12
|
+
function positive(value){if(!Number.isSafeInteger(value)||value<1)fail('Concurrency must be a positive safe integer');return value;}
|
|
13
|
+
function codex(text) {
|
|
14
|
+
const data=parseToml(text);
|
|
15
|
+
if(data.agents!==undefined&&!object(data.agents))fail('Codex agents must be a table');
|
|
16
|
+
const value=data.agents?.[KEY];
|
|
17
|
+
if(value!==undefined&&(typeof value!=='bigint'||value<1n||value>BigInt(Number.MAX_SAFE_INTEGER)))fail('Invalid Codex concurrency integer');
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
export function readRuntimeConcurrency(platform,original) {
|
|
21
|
+
const text=decode(original);
|
|
22
|
+
if(platform==='codex'){const value=codex(text).agents?.[KEY];return value===undefined?null:Number(value);}
|
|
23
|
+
if(platform==='claude'){const value=claude(text).env?.[ENV];return value===undefined?null:Number(value);}
|
|
24
|
+
fail('Unknown runtime platform');
|
|
25
|
+
}
|
|
26
|
+
export function renderCodexConcurrency(original,value) {
|
|
27
|
+
positive(value);const text=decode(original),data=codex(text);
|
|
28
|
+
if(data.agents?.[KEY]===BigInt(value))return {content:Buffer.from(text),action:'unchanged'};
|
|
29
|
+
const eol=/\r?\n/.exec(text)?.[0]??'\n',lines=text.match(/[^\n]*\n|[^\n]+$/g)??[];
|
|
30
|
+
const header=/^[ \t]*\[[ \t]*agents[ \t]*\][ \t]*(?:#[^\r\n]*)?\r?\n?$/;
|
|
31
|
+
const index=lines.findIndex(line=>header.test(line));
|
|
32
|
+
let updated;
|
|
33
|
+
if(index<0) {
|
|
34
|
+
// An explicit agents value (inline or dotted) requires manual editing.
|
|
35
|
+
if(data.agents!==undefined&&!lines.some(line=>/^\s*\[agents\./.test(line)))fail('Unsupported Codex agents representation');
|
|
36
|
+
let prefix=text;if(prefix&&!prefix.endsWith('\n'))prefix+=eol;
|
|
37
|
+
updated=prefix+`[agents]${eol}${KEY} = ${value}${eol}`;
|
|
38
|
+
} else {
|
|
39
|
+
let end=lines.findIndex((line,i)=>i>index&&/^[ \t]*\[/.test(line));if(end<0)end=lines.length;
|
|
40
|
+
let found=false;
|
|
41
|
+
for(let i=index+1;i<end;i++) {
|
|
42
|
+
const match=/^([ \t]*max_concurrent_threads_per_session[ \t]*=[ \t]*)([^#\r\n]*?)([ \t]*(?:#[^\r\n]*)?)(\r?\n)?$/.exec(lines[i]);
|
|
43
|
+
if(match){if(found)fail('Ambiguous Codex concurrency');found=true;lines[i]=match[1]+value+match[3]+(match[4]??'');}
|
|
44
|
+
}
|
|
45
|
+
if(data.agents?.[KEY]!==undefined&&!found)fail('Unsupported Codex concurrency representation');
|
|
46
|
+
if(!found){if(!lines[end-1].endsWith('\n'))lines[end-1]+=eol;lines.splice(end,0,`${KEY} = ${value}${eol}`);}
|
|
47
|
+
updated=lines.join('');
|
|
48
|
+
}
|
|
49
|
+
const expected=parseToml(text);expected.agents??=Object.create(null);expected.agents[KEY]=BigInt(value);
|
|
50
|
+
if(!isDeepStrictEqual(parseToml(updated),expected))fail('Cannot verify safe Codex concurrency edit');
|
|
51
|
+
return {content:Buffer.from(updated),action:'set Codex concurrency'};
|
|
52
|
+
}
|
|
53
|
+
function claude(text) {
|
|
54
|
+
const data=text===''?{}:JSON.parse(text);
|
|
55
|
+
// Validate duplicate names before JSON.parse can silently discard them.
|
|
56
|
+
const tokens=[...text.matchAll(/"(?:[^"\\]|\\[\s\S])*"|[{}\[\]:,]|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/g)].map(match=>match[0]);
|
|
57
|
+
const stack=[];
|
|
58
|
+
for(let i=0;i<tokens.length;i++) {
|
|
59
|
+
const token=tokens[i];
|
|
60
|
+
if(token==='{')stack.push(new Set());
|
|
61
|
+
else if(token==='[')stack.push(null);
|
|
62
|
+
else if(token==='}'||token===']')stack.pop();
|
|
63
|
+
else if(token.startsWith('"')&&tokens[i+1]===':') {
|
|
64
|
+
const key=JSON.parse(token),keys=stack.at(-1);
|
|
65
|
+
if(keys?.has(key))fail('Duplicate Claude settings key');
|
|
66
|
+
keys?.add(key);
|
|
67
|
+
} else if(/^-?\d/.test(token)) {
|
|
68
|
+
const number=Number(token);
|
|
69
|
+
if(!Number.isFinite(number)||Number.isInteger(number)&&!Number.isSafeInteger(number))fail('Unsafe Claude JSON number');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if(!object(data))fail('Claude settings root must be an object');
|
|
73
|
+
if(data.env!==undefined&&!object(data.env))fail('Claude env must be an object');
|
|
74
|
+
const value=data.env?.[ENV];
|
|
75
|
+
if(value!==undefined&&(typeof value!=='string'||! /^[0-9]+$/.test(value)||!Number.isSafeInteger(Number(value))||Number(value)<1))fail('Invalid Claude concurrency string');
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
export function renderClaudeConcurrency(original,value) {
|
|
79
|
+
positive(value);const text=decode(original),data=claude(text);
|
|
80
|
+
if(data.env?.[ENV]===String(value))return {content:Buffer.from(text),action:'unchanged'};
|
|
81
|
+
data.env??={};data.env[ENV]=String(value);
|
|
82
|
+
const content=Buffer.from(JSON.stringify(data,null,2)+'\n');
|
|
83
|
+
if(!isDeepStrictEqual(JSON.parse(content),data))fail('Cannot verify safe Claude concurrency edit');
|
|
84
|
+
return {content,action:'set Claude concurrency'};
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {applyTransaction} from './transaction.mjs';
|
|
3
|
+
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
4
|
+
const start='# >>> arc completion >>>',end='# <<< arc completion <<<';
|
|
5
|
+
const body='if command -v arc >/dev/null 2>&1; then\n eval "$(arc completion zsh)"\nfi';
|
|
6
|
+
export function renderShellCompletion(original){
|
|
7
|
+
let text;try{text=original===null?'':new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(original);}catch{throw new TargetConfigurationError('.zshrc must be valid UTF-8');}
|
|
8
|
+
const starts=[...text.matchAll(/# >>> arc completion >>>/g)],ends=[...text.matchAll(/# <<< arc completion <<</g)];
|
|
9
|
+
if(starts.length!==ends.length||starts.length>1)throw new TargetConfigurationError('Malformed ARC completion markers');
|
|
10
|
+
const newline=text.includes('\r\n')?'\r\n':'\n',block=start+newline+body.replaceAll('\n',newline)+newline+end;
|
|
11
|
+
if(starts.length){const first=starts[0].index,last=ends[0].index;if(first>=last||(first>0&&text[first-1]!=='\n')||(last>0&&text[last-1]!=='\n')||!['','\n','\r'].includes(text[last+end.length]??''))throw new TargetConfigurationError('Malformed ARC completion markers');return Buffer.from(text.slice(0,first)+block+text.slice(last+end.length));}
|
|
12
|
+
return Buffer.concat([original??Buffer.alloc(0),Buffer.from((text&&!text.endsWith('\n')?newline:'')+block+newline)]);
|
|
13
|
+
}
|
|
14
|
+
export function attachShellCompletion(plans,{safeFs}){
|
|
15
|
+
const plan=plans[0];if(!plan||plan.skipGlobal)return;
|
|
16
|
+
const filename=path.join(plan.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:plan.snapshot}),original=current.bytes??null;
|
|
17
|
+
const metadata=plan.snapshot.get(filename);if(metadata&&(metadata.mode&0o222n)===0n)throw new TargetConfigurationError('.zshrc is not writable');
|
|
18
|
+
const content=renderShellCompletion(original);plan.managedPaths.push(filename);
|
|
19
|
+
if(original===null||!content.equals(original))plan.globalChanges.push({path:filename,original,content,action:'register completion'});
|
|
20
|
+
plan.summary.push(`${filename}: zsh completion; opens automatically in a new zsh session. For this session: eval "$(arc completion zsh)"`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerShellCompletion(userHome,{safeFs,hooks={}}){
|
|
24
|
+
const plan={target:{userHome},globalChanges:[],managedPaths:[],externalPaths:[],snapshot:new Map(),summary:[]};
|
|
25
|
+
attachShellCompletion([plan],{safeFs});
|
|
26
|
+
if(!plan.globalChanges.length)return{status:'unchanged',residualPaths:[]};
|
|
27
|
+
const result=applyTransaction(plan,{safeFs,hooks,specs:plan.globalChanges.map(change=>({path:change.path,bytes:change.content}))});
|
|
28
|
+
return{...result,status:result.cleanupError?'failed':'registered'};
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {metadataOf,sameIdentity,sameObjectIdentity,typeOf,readCheckedFile} from './targets.mjs';
|
|
3
|
+
function fail(message){throw new Error(message);}
|
|
4
|
+
export function captureCleanupEntry(parent,name){
|
|
5
|
+
const stat=parent.lstat(name),type=typeOf(stat);
|
|
6
|
+
if(type===0o100000)return readCheckedFile(parent,name,stat).metadata;
|
|
7
|
+
if(type===0o120000){const link=parent.readlink(name);if(!sameIdentity(stat,parent.lstat(name)))fail('Cleanup link changed during inspection');return{...metadataOf(stat),type:'symlink',link};}
|
|
8
|
+
if(type!==0o040000)fail('Unsafe cleanup item type');
|
|
9
|
+
const directory=parent.openDir(name);try{if(!sameIdentity(stat,directory.stat()))fail('Cleanup directory changed during inspection');const children=new Map();for(const child of directory.readDir())children.set(child,captureCleanupEntry(directory,child));if(!sameIdentity(stat,directory.stat()))fail('Cleanup directory changed during inspection');return{...metadataOf(stat),type:'directory',children};}finally{directory.close();}
|
|
10
|
+
}
|
|
11
|
+
function matches(expected,actual,{moved=false}={}){
|
|
12
|
+
const fields=['dev','ino','mode','size','mtimeNs','type','link',...(expected.type==='file'?['sha256']:[]),...(moved?[]:['ctimeNs'])];
|
|
13
|
+
if(!fields.every(key=>expected[key]===actual[key]))return false;
|
|
14
|
+
if(expected.children){if(!actual.children||expected.children.size!==actual.children.size)return false;for(const[name,item]of expected.children)if(!actual.children.has(name)||!matches(item,actual.children.get(name)))return false;}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// Only a currently held, transaction-owned stage and its recorded children may
|
|
18
|
+
// be removed. No stale-prefix scanning, pathname recursion or symlink following.
|
|
19
|
+
export function cleanupStage(area,{safeFs,hooks={}}){
|
|
20
|
+
const guard=()=>{const named=safeFs.openRoot(path.dirname(area.path));try{if(!sameObjectIdentity(named.stat(),area.parent.stat()))fail('Cleanup parent namespace changed');}finally{named.close();}if(!sameObjectIdentity(area.identity,area.stage.stat())||!sameObjectIdentity(area.identity,area.parent.lstat(area.name)))fail('Cleanup stage identity changed');};
|
|
21
|
+
const namesMatch=(directory,expected)=>{const names=directory.readDir();if(names.length!==expected.size||names.some(name=>!expected.has(name)))fail('Unknown or missing cleanup entry');};
|
|
22
|
+
const verify=(parent,name,expected)=>{if(!matches(expected,captureCleanupEntry(parent,name),{moved:true}))fail('Cleanup item drift: '+name);};
|
|
23
|
+
const remove=(parent,name,expected,absolute)=>{
|
|
24
|
+
guard();verify(parent,name,expected);
|
|
25
|
+
if(expected.type==='directory'){
|
|
26
|
+
const directory=parent.openDir(name);try{if(!sameObjectIdentity(expected,directory.stat()))fail('Cleanup directory identity changed');const remaining=new Map(expected.children);for(const[child,item]of expected.children){namesMatch(directory,remaining);remove(directory,child,item,absolute+'/'+child);remaining.delete(child);}namesMatch(directory,remaining);directory.sync();}finally{directory.close();}
|
|
27
|
+
}
|
|
28
|
+
hooks.fault?.({phase:'before-cleanup-item',path:absolute});guard();
|
|
29
|
+
if(expected.type==='directory'){
|
|
30
|
+
if(!sameObjectIdentity(expected,parent.lstat(name)))fail('Cleanup directory identity changed before deletion');const directory=parent.openDir(name);try{if(!sameObjectIdentity(expected,directory.stat())||directory.readDir().length)fail('Cleanup directory changed before deletion');}finally{directory.close();}
|
|
31
|
+
if(!sameObjectIdentity(expected,parent.lstat(name)))fail('Cleanup directory identity changed before deletion');parent.rmdir(name);
|
|
32
|
+
}else{verify(parent,name,expected);parent.unlink(name);}
|
|
33
|
+
parent.sync();hooks.fault?.({phase:'after-cleanup-item',path:absolute});
|
|
34
|
+
};
|
|
35
|
+
try{
|
|
36
|
+
guard();namesMatch(area.stage,area.expected);for(const[name,expected]of area.expected)verify(area.stage,name,expected);
|
|
37
|
+
const remaining=new Map(area.expected);for(const[name,expected]of area.expected){guard();namesMatch(area.stage,remaining);remove(area.stage,name,expected,area.path+'/'+name);remaining.delete(name);}
|
|
38
|
+
guard();namesMatch(area.stage,remaining);area.stage.sync();guard();area.parent.rmdir(area.name);area.parent.sync();return{residualPaths:[],cleanupError:null};
|
|
39
|
+
}catch(cause){const cleanupError=Object.assign(new Error(`Stage cleanup failed for ${area.path}: ${cause.message}`,{cause}),{code:'ARC_INSTALL_CLEANUP',residualPaths:[area.path]});return{residualPaths:[area.path],cleanupError};}
|
|
40
|
+
}
|