@yunhozz/arc 0.1.9 → 0.2.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 +109 -75
- package/README.md +19 -31
- package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
- package/claude/agents/arc-planner.md +4 -4
- package/claude/agents/arc-qa.md +4 -4
- package/claude/agents/arc-reviewer.md +5 -5
- package/claude/agents/arc-scouter.md +3 -3
- package/claude/skill/SKILL.md +7 -7
- package/claude/skill/models.toml +3 -3
- package/claude/skill/references/commit.md +22 -7
- package/claude/skill/references/coordination.md +25 -18
- package/claude/skill/references/git-recovery.md +1 -1
- package/claude/skill/references/handoff-prep.md +25 -2
- package/claude/skill/references/handoff.md +116 -24
- package/claude/skill/references/planning.md +78 -22
- package/claude/skill/references/verification.md +155 -17
- package/claude/skill/runtime/acceptance.mjs +68 -0
- package/claude/skill/runtime/assignment.mjs +42 -0
- package/claude/skill/runtime/bootstrap.mjs +35 -0
- package/claude/skill/runtime/cli.mjs +8 -0
- package/claude/skill/runtime/command.mjs +21 -0
- package/claude/skill/runtime/cooperative-schema.mjs +55 -0
- package/claude/skill/runtime/cooperative.mjs +136 -0
- package/claude/skill/runtime/evaluate.mjs +40 -4
- package/claude/skill/runtime/external.mjs +51 -0
- package/claude/skill/runtime/gate.mjs +7 -7
- package/claude/skill/runtime/git-evidence.mjs +46 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +64 -19
- package/claude/skill/runtime/schema.mjs +1 -0
- package/claude/skill/runtime/scope.mjs +36 -0
- package/claude/skill/runtime/store.mjs +25 -4
- package/claude/skill/runtime/verification.mjs +123 -0
- package/claude/skill/scripts/model_config.py +2 -2
- package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
- package/codex/agents/arc-planner.toml +6 -6
- package/codex/agents/arc-qa.toml +6 -6
- package/codex/agents/arc-reviewer.toml +6 -6
- package/codex/agents/arc-scouter.toml +5 -5
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +3 -3
- package/codex/skill/references/commit.md +23 -8
- package/codex/skill/references/coordination.md +25 -18
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +25 -2
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +126 -34
- package/codex/skill/references/planning.md +81 -23
- package/codex/skill/references/verification.md +160 -22
- package/codex/skill/runtime/acceptance.mjs +68 -0
- package/codex/skill/runtime/assignment.mjs +42 -0
- package/codex/skill/runtime/bootstrap.mjs +35 -0
- package/codex/skill/runtime/cli.mjs +8 -0
- package/codex/skill/runtime/command.mjs +21 -0
- package/codex/skill/runtime/cooperative-schema.mjs +55 -0
- package/codex/skill/runtime/cooperative.mjs +136 -0
- package/codex/skill/runtime/evaluate.mjs +40 -4
- package/codex/skill/runtime/external.mjs +51 -0
- package/codex/skill/runtime/gate.mjs +7 -7
- package/codex/skill/runtime/git-evidence.mjs +46 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +64 -19
- package/codex/skill/runtime/schema.mjs +1 -0
- package/codex/skill/runtime/scope.mjs +36 -0
- package/codex/skill/runtime/store.mjs +25 -4
- package/codex/skill/runtime/verification.mjs +123 -0
- package/codex/skill/scripts/model_config.py +3 -3
- package/lib/cli/postinstall.mjs +3 -16
- package/lib/cli/root.mjs +8 -18
- package/lib/cli/upgrade.mjs +1 -57
- package/lib/installer/manifest.mjs +7 -2
- package/lib/installer/plan.mjs +27 -13
- package/lib/installer/runtime-assets.mjs +25 -0
- package/lib/installer/shell-completion.mjs +15 -20
- package/lib/installer/targets.mjs +1 -1
- package/lib/installer/uninstall.mjs +5 -1
- package/lib/installer/upgrade-state.mjs +2 -2
- package/lib/installer/upgrade.mjs +2 -5
- package/lib/management/operations.mjs +39 -0
- package/lib/management/status.mjs +18 -0
- package/lib/management/update.mjs +58 -0
- package/lib/management/workflow.mjs +51 -0
- package/lib/models/config.mjs +1 -1
- package/lib/models/profiles.mjs +1 -1
- package/lib/policy/placement.mjs +6 -6
- package/lib/policy/render.mjs +3 -3
- package/lib/policy/role-policy.json +64 -19
- package/lib/release/artifact.mjs +1 -1
- package/lib/runtime/acceptance.mjs +68 -0
- package/lib/runtime/assignment.mjs +42 -0
- package/lib/runtime/bootstrap.mjs +35 -0
- package/lib/runtime/cli.mjs +8 -0
- package/lib/runtime/command.mjs +21 -0
- package/lib/runtime/cooperative-schema.mjs +55 -0
- package/lib/runtime/cooperative.mjs +136 -0
- package/lib/runtime/evaluate.mjs +40 -4
- package/lib/runtime/external.mjs +51 -0
- package/lib/runtime/gate.mjs +7 -7
- package/lib/runtime/git-evidence.mjs +46 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/schema.mjs +1 -0
- package/lib/runtime/scope.mjs +36 -0
- package/lib/runtime/store.mjs +25 -4
- package/lib/runtime/verification.mjs +123 -0
- package/lib/tui/app.mjs +41 -0
- package/lib/tui/render.mjs +21 -0
- package/lib/tui/setup.mjs +20 -0
- package/lib/tui/terminal.mjs +71 -0
- package/lib/tui/workflow.mjs +12 -0
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -3
- package/scripts/update-runtime-assets.mjs +6 -1
- package/lib/cli/choices.mjs +0 -47
- package/lib/cli/completion.mjs +0 -55
- package/lib/cli/index.mjs +0 -55
- package/lib/cli/preset.mjs +0 -29
- package/lib/cli/terminal.mjs +0 -19
- package/lib/cli/uninstall.mjs +0 -17
|
@@ -36,6 +36,7 @@ const unique = (items,key) => items.every(x=>object(x)&&text(x[key])) && new Set
|
|
|
36
36
|
function approval(a){return object(a)&&text(a.id)&&['user-message','human-confirmation'].includes(a.source)&&text(a.messageRef)&&hash(a.contentHash)&&object(a.scope)&&text(a.scope.taskId)&&absolute(a.scope.target)&&strings(a.scope.operations)&&a.scope.operations.every(op=>OPERATIONS.includes(op))&&strings(a.scope.paths)&&a.scope.paths.every(absolute);}
|
|
37
37
|
/** Validate stored workflow data. Observed provenance must still be checked by evaluate. */
|
|
38
38
|
export function validateWorkflowState(s){
|
|
39
|
+
if(object(s)&&s.workflowKind!==undefined)fail('provider state cannot use cooperative contract');
|
|
39
40
|
if(!object(s)||s.schemaVersion!==1||!text(s.workflowId)||!Number.isSafeInteger(s.revision)||s.revision<0)fail('version or identity');
|
|
40
41
|
if(!object(s.versions)||['policy','runtime','adapter','policyHash','runtimeHash','adapterHash'].some(k=>!text(s.versions[k])))fail('versions');
|
|
41
42
|
if(['policyHash','runtimeHash','adapterHash'].some(k=>!hash(s.versions[k])))fail('hash algorithm');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
export const absolute=value=>typeof value==='string'&&path.isAbsolute(value)&&path.normalize(value)===value;
|
|
5
|
+
export const within=(base,file)=>absolute(file)&&!path.relative(base,file).split(path.sep).includes('..')&&!path.isAbsolute(path.relative(base,file));
|
|
6
|
+
/** Inspect every component, including ancestors outside the ARC namespace. */
|
|
7
|
+
export async function inspectPath(filename,{missing=false,directory=false}={}){
|
|
8
|
+
if(!absolute(filename))throw Error('Unsafe absolute path');
|
|
9
|
+
let cursor=path.parse(filename).root;
|
|
10
|
+
const parts=filename.slice(cursor.length).split(path.sep).filter(Boolean);
|
|
11
|
+
for(let i=0;i<parts.length;i++){
|
|
12
|
+
cursor=path.join(cursor,parts[i]);let stat;
|
|
13
|
+
try{stat=await fs.lstat(cursor);}catch(error){if(missing&&error.code==='ENOENT')return;throw error;}
|
|
14
|
+
if(stat.isSymbolicLink())throw Error('Unsafe symlink path');
|
|
15
|
+
if(i<parts.length-1||directory){if(!stat.isDirectory())throw Error('Unsafe directory type');}
|
|
16
|
+
else if(!stat.isFile())throw Error('State must be a regular file');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function validateWorkflowId(id){if(typeof id!=='string'||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(id))throw Error('Invalid workflow ID');return id;}
|
|
20
|
+
/** Shell-free, read-only observation; inherited Git target/config overrides are removed. */
|
|
21
|
+
export async function observeCooperativeTarget(cwd){
|
|
22
|
+
if(!absolute(cwd))throw Error('Absolute workspace required');await inspectPath(cwd,{directory:true});
|
|
23
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
|
|
24
|
+
Object.assign(env,{GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
25
|
+
const query=(args,detached=false)=>{const r=spawnSync('git',['--no-pager','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});if(detached&&r.status===1&&!r.error)return null;if(r.status!==0||r.error||!r.stdout.endsWith('\n')||r.stdout.slice(0,-1).includes('\n'))throw Error('Git target observation failed');return r.stdout.slice(0,-1);};
|
|
26
|
+
const target={realpath:query(['rev-parse','--show-toplevel']),gitCommonDir:query(['rev-parse','--path-format=absolute','--git-common-dir']),gitDir:query(['rev-parse','--absolute-git-dir']),head:query(['rev-parse','HEAD']),branch:query(['symbolic-ref','--quiet','--short','HEAD'],true)};
|
|
27
|
+
await validateObservedTarget(cwd,target);return target;
|
|
28
|
+
}
|
|
29
|
+
export async function validateObservedTarget(cwd,target){
|
|
30
|
+
if(!absolute(cwd)||!target||target.realpath!==cwd||!absolute(target.gitCommonDir)||!absolute(target.gitDir)||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(target.head)||!(target.branch===null||typeof target.branch==='string'&&target.branch))throw Error('Invalid observed target');
|
|
31
|
+
await inspectPath(cwd,{directory:true});await inspectPath(target.gitCommonDir,{directory:true});
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
export async function workflowStatePath(target,workflowId){
|
|
35
|
+
validateWorkflowId(workflowId);const statePath=path.join(target.gitCommonDir,'arc','workflows',workflowId,'state.json');await inspectPath(statePath,{missing:true});return statePath;
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {verifiedCooperativeReconciliation} from './acceptance.mjs';
|
|
1
2
|
import {verifiedReconciliation} from './gate.mjs';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -51,13 +52,33 @@ export async function transactWorkflow({statePath,expectedRevision,requestId,req
|
|
|
51
52
|
next.revision=state.revision+1;next.requests=prior?state.requests.map(r=>r.requestId===requestId?{requestId,requestFingerprint,decision}:r):[...state.requests,{requestId,requestFingerprint,decision}];
|
|
52
53
|
next.audit=[...state.audit,{requestId,revision:next.revision,decision:decision.decision,ruleIds:decision.ruleIds??[],result:'pending'}];
|
|
53
54
|
validateWorkflowState(next);
|
|
54
|
-
|
|
55
|
-
const handle=await fs.open(temporary,'wx',0o600);try{await handle.writeFile(JSON.stringify(next,null,2)+'\n');await handle.sync();}finally{await handle.close();}
|
|
56
|
-
await fs.rename(temporary,statePath);temporary=undefined;
|
|
57
|
-
const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
|
|
55
|
+
await saveDurableState({statePath,state:next});
|
|
58
56
|
return {state:next,decision,replayed:false};
|
|
59
57
|
}finally{
|
|
60
58
|
if(temporary)await fs.unlink(temporary).catch(()=>{});
|
|
61
59
|
if(lock){await lock.close();await fs.unlink(lockPath);}
|
|
62
60
|
}
|
|
63
61
|
}
|
|
62
|
+
|
|
63
|
+
/** Shared durability primitive. A post-rename sync error is reported as failure;
|
|
64
|
+
* the caller must re-read rather than assume rollback or repeat an effect. */
|
|
65
|
+
export async function saveDurableState({statePath,state,create=false,storage={}}){
|
|
66
|
+
const parent=path.dirname(statePath),temporary=path.join(parent,`.${path.basename(statePath)}.${crypto.randomUUID()}.tmp`);
|
|
67
|
+
let exists=false;
|
|
68
|
+
try{
|
|
69
|
+
const handle=await fs.open(temporary,'wx',0o600);exists=true;
|
|
70
|
+
try{await handle.writeFile(JSON.stringify(state,null,2)+'\n');await handle.sync();}finally{await handle.close();}
|
|
71
|
+
if(create){await (storage.link??fs.link)(temporary,statePath);await fs.unlink(temporary);}else await (storage.rename??fs.rename)(temporary,statePath);
|
|
72
|
+
exists=false;
|
|
73
|
+
const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
|
|
74
|
+
}finally{if(exists)await fs.unlink(temporary).catch(()=>{});}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Only a locally verified cooperative commit may advance the stored target. */
|
|
78
|
+
export function applyCooperativeCommitTransition({state,decision}){
|
|
79
|
+
if(decision?.decision!=='allow'||!verifiedCooperativeReconciliation(decision,state)||!decision.proposedTransition?.commit)throw Error('Verified cooperative reconciliation required');
|
|
80
|
+
const next=structuredClone(state),transition=decision.proposedTransition;
|
|
81
|
+
next.targetHistory.push({target:structuredClone(state.target),revision:state.revision+1,source:'local-observation',kind:'verified-commit-parent'});
|
|
82
|
+
next.target=structuredClone(transition.target);next.commits.push(structuredClone(transition.commit));next.tasks.find(t=>t.id===transition.commit.taskId).status='committed';
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {spawnSync} from 'node:child_process';
|
|
5
|
+
import {fingerprintRequest} from './store.mjs';
|
|
6
|
+
import {absolute,within,inspectPath} from './scope.mjs';
|
|
7
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
8
|
+
const text=v=>typeof v==='string'&&v.length>0;
|
|
9
|
+
const hash=v=>typeof v==='string'&&/^sha256:[a-f0-9]{64}$/.test(v);
|
|
10
|
+
const list=v=>Array.isArray(v)&&v.every(text)&&new Set(v).size===v.length;
|
|
11
|
+
const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).every(k=>keys.includes(k));
|
|
12
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
13
|
+
const hold=(state,id,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.'+id],reason});
|
|
14
|
+
const allow=(state,transition)=>({...base(state),decision:'allow',ruleIds:['cooperative.verification'],proposedTransition:transition});
|
|
15
|
+
const current=(state,scope,taskId,changes)=>scope==='final'?changes?.final:changes?.tasks?.[taskId];
|
|
16
|
+
const approval=(state,taskId)=>state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(taskId))).at(-1);
|
|
17
|
+
function authorized(state,task){
|
|
18
|
+
const a=approval(state,task?.id);
|
|
19
|
+
return task&&!task.invalidated&&a&&a.source==='operator-attested'&&a.targetVersion===state.targetVersion&&task.scopeVersion===a.scopeVersion&&task.targetVersion===state.targetVersion&&task.approvalRef===a.messageRef&&task.approvalVersion===a.data.approvalVersion&&task.documentVersion===a.data.documentVersion;
|
|
20
|
+
}
|
|
21
|
+
function descriptorValid(state,d,operation){
|
|
22
|
+
if(operation==='review')return fields(d,['kind','paths'])&&d.kind==='source-review'&&list(d.paths)&&d.paths.length>0&&d.paths.every(p=>absolute(p)&&within(state.target.realpath,p));
|
|
23
|
+
return fields(d,['executable','args','cwd','env','outputs'])&&absolute(d.executable)&&Array.isArray(d.args)&&d.args.every(v=>typeof v==='string')&&absolute(d.cwd)&&within(state.target.realpath,d.cwd)&&fields(d.env,Object.keys(d.env??{}))&&Object.values(d.env).every(v=>typeof v==='string')&&Array.isArray(d.outputs)&&d.outputs.every(p=>absolute(p)&&within(state.target.realpath,p));
|
|
24
|
+
}
|
|
25
|
+
const assignedImplementer=(state,id,taskId)=>state.assignments.some(a=>a.agentId===id&&a.taskId===taskId&&a.status==='active');
|
|
26
|
+
function independent(state,actor,taskId){
|
|
27
|
+
if(!fields(actor,['id','role'])||!text(actor.id))return false;
|
|
28
|
+
if(state.assignments.some(a=>a.agentId===actor.id))return false;
|
|
29
|
+
return !(state.intents??[]).some(i=>i.verificationActor?.id===actor.id&&(i.verificationActor.role!==actor.role||actor.role==='qa'&&i.taskId!==taskId))&&!(state.checks??[]).some(c=>c.verificationActor?.id===actor.id&&(c.verificationActor.role!==actor.role||actor.role==='qa'&&c.taskId!==taskId));
|
|
30
|
+
}
|
|
31
|
+
/** Invalidate only the affected dependency closure; preserve all historical evidence. */
|
|
32
|
+
export function invalidateAffectedEvidence({state,changedVersions={},affectedTaskIds}={}){
|
|
33
|
+
const next=structuredClone(state),ids=new Set(affectedTaskIds??next.tasks.map(t=>t.id));
|
|
34
|
+
let changed=true;while(changed){changed=false;for(const t of next.tasks)if(!ids.has(t.id)&&t.dependencies?.some(id=>ids.has(id))){ids.add(t.id);changed=true;}}
|
|
35
|
+
for(const t of next.tasks)if(ids.has(t.id)){t.invalidated=true;t.status='held';t.invalidatedBy=structuredClone(changedVersions);t.invalidatedAt=state.revision+1;}
|
|
36
|
+
for(const key of ['checks','acceptances'])for(const entry of next[key]??[])if(entry.scope==='final'||ids.has(entry.taskId)){entry.stale=true;entry.invalidatedBy=structuredClone(changedVersions);}
|
|
37
|
+
for(const intent of next.intents??[])if(['check','review'].includes(intent.operation)&&(intent.scope==='final'||ids.has(intent.taskId)))intent.stale=true;
|
|
38
|
+
// Old approval statements remain immutable; no change creates renewed authority.
|
|
39
|
+
if(Number.isSafeInteger(changedVersions.scopeVersion))next.scopeVersion=changedVersions.scopeVersion;
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
function record({state,request,evidence}){
|
|
43
|
+
const entry=request.evidence,d=entry?.data;
|
|
44
|
+
if(!entry||!text(entry.messageRef)||!d)return hold(state,'verification-record','Structured evidence required');
|
|
45
|
+
if(entry.kind==='acceptance-evidence'&&d.kind==='task'){
|
|
46
|
+
const t=d.task,previous=state.tasks.find(task=>task.id===t?.id),a=approval(state,t?.id);
|
|
47
|
+
if(previous?.invalidated&&(!a||!Number.isSafeInteger(a.revision)||a.revision<previous.invalidatedAt||a.scopeVersion!==state.scopeVersion))return hold(state,'task-approval-stale','Invalidated task requires a renewed approval statement');
|
|
48
|
+
if(!fields(t,['id','paths','criteria','requiredChecks','dependencies','approvalRef','approvalVersion','documentVersion','scopeVersion','targetVersion','status'])||!text(t.id)||!list(t.paths)||!t.paths.length||!t.paths.every(p=>absolute(p)&&within(state.target.realpath,p))||!list(t.criteria)||!t.criteria.length||!list(t.dependencies)||t.dependencies.includes(t.id)||t.dependencies.some(id=>!state.tasks.some(task=>task.id===id))||!Array.isArray(t.requiredChecks)||!t.requiredChecks.length||!t.requiredChecks.every(c=>descriptorValid(state,c,'check'))||!['pending',undefined].includes(t.status)||state.tasks.some(task=>task.id===t.id&&['accepted','committed'].includes(task.status))||!authorized(state,t))return hold(state,'task-contract','Current approval and explicit pending task contract required');
|
|
49
|
+
return allow(state,{task:{...structuredClone(t),status:'pending',source:'operator-attested',messageRef:entry.messageRef}});
|
|
50
|
+
}
|
|
51
|
+
if(entry.kind!=='finding')return hold(state,'unsupported','Check and review outcomes require durable intent/result');
|
|
52
|
+
if(d.action==='open'){
|
|
53
|
+
const check=state.checks.find(c=>c.id===d.checkId&&!c.stale);
|
|
54
|
+
if(!fields(d,['action','id','checkId','message'])||!text(d.id)||!text(d.message)||state.findings.some(f=>f.id===d.id)||!check)return hold(state,'finding','Finding must link an existing check or review');
|
|
55
|
+
return allow(state,{finding:{id:d.id,checkId:check.id,scope:check.scope,taskId:check.taskId,origin:check.scope==='final'&&check.operation==='review'?'final-review':'check',status:'open',message:d.message,messageRef:entry.messageRef,source:'operator-attested'}});
|
|
56
|
+
}
|
|
57
|
+
const finding=state.findings.find(f=>f.id===d.id);
|
|
58
|
+
const affected=finding?.scope==='final'?state.tasks.map(t=>t.id):[finding?.taskId];
|
|
59
|
+
if(!fields(d,['action','id','actor','changeFingerprint'])||d.action!=='resolve'||!finding||finding.status==='closed'||!fields(d.actor,['id','role'])||d.actor.role!=='implementer'||!affected.some(id=>assignedImplementer(state,d.actor.id,id))||!hash(d.changeFingerprint)||d.changeFingerprint!==current(state,finding.scope,finding.taskId,evidence?.changes))return hold(state,'finding-resolution','Assigned Implementer correction at the current fingerprint required');
|
|
60
|
+
return allow(state,{finding:{...finding,status:'resolved',resolutionRef:entry.messageRef,resolutionActor:structuredClone(d.actor),changeFingerprint:d.changeFingerprint}});
|
|
61
|
+
}
|
|
62
|
+
/** Pure cooperative gate: verifies recorded consistency, never identity or semantic sufficiency. */
|
|
63
|
+
export function evaluateCooperativeVerification({state,request,evidence}={}){
|
|
64
|
+
if(state?.mode!=='plan')return hold(state,'unsupported','Only the complete spec and plan path is supported');
|
|
65
|
+
if(!request||request.expectedRevision!==state.revision)return hold(state,'revision','Stale verification request');
|
|
66
|
+
if(request.actor?.role!=='orchestrator')return hold(state,'verification-actor','Orchestrator statement required');
|
|
67
|
+
if(request.operation==='verification-record')return record({state,request,evidence});
|
|
68
|
+
if(request.operation==='verification-result')return outcome({state,request,evidence});
|
|
69
|
+
if(!['check','review'].includes(request.operation))return hold(state,'unsupported','Unsupported verification operation');
|
|
70
|
+
const a=request.arguments,t=state.tasks.find(t=>t.id===request.taskId);
|
|
71
|
+
if(!fields(a,['scope','actor','descriptor','changeFingerprint','findingIds'])||!['task','final'].includes(a.scope)||!hash(a.changeFingerprint)||a.changeFingerprint!==current(state,a.scope,request.taskId,evidence?.changes)||!descriptorValid(state,a.descriptor,request.operation))return hold(state,'verification-fingerprint','Exact scope descriptor and current fingerprint required');
|
|
72
|
+
if(a.scope==='task'&&(!authorized(state,t)||t.status==='held'||request.operation==='review'||!t.requiredChecks.some(d=>same(d,a.descriptor))))return hold(state,'verification-task','Current task approval and exact assigned check required; Reviewer is final only');
|
|
73
|
+
if(a.scope==='final'&&(request.taskId!==undefined||request.operation!=='review'||!state.tasks.length||state.tasks.some(t=>!authorized(state,t)||!['accepted','committed'].includes(t.status))))return hold(state,'verification-final','Final review follows accepted tasks and required task checks');
|
|
74
|
+
const finalCorrection=a.scope==='final'&&state.findings.some(f=>f.origin==='final-review'&&f.status==='resolved'&&a.findingIds?.includes(f.id));
|
|
75
|
+
if(a.scope==='final'&&!finalCorrection&&state.tasks.some(task=>task.requiredChecks.some(descriptor=>!state.checks.some(c=>c.scope==='task'&&c.taskId===task.id&&c.operation==='check'&&!c.stale&&c.status==='passed'&&c.verdict==='proven'&&text(c.resultRef)&&c.verificationActor?.role==='qa'&&same(c.descriptor,descriptor)&&(c.changeFingerprint===evidence.changes?.tasks?.[task.id]||task.status==='committed'&&state.commits?.some(commit=>commit.taskId===task.id&&commit.observationSource==='local-git'&&commit.fingerprint===c.changeFingerprint&&commit.postCommitFingerprint===evidence.changes?.tasks?.[task.id]))))))return hold(state,'verification-final-checks','Initial final review requires current task QA evidence');
|
|
76
|
+
if(a.actor?.role!==(request.operation==='review'?'reviewer':'qa')||a.actor.id===request.actor.id||!independent(state,a.actor,request.taskId))return hold(state,'verification-independence','Independent actor statement and fresh task QA required');
|
|
77
|
+
const findings=state.findings.filter(f=>f.status!=='closed'&&(a.scope==='final'||f.scope==='task'&&f.taskId===request.taskId));
|
|
78
|
+
if(a.findingIds!==undefined&&(!list(a.findingIds)||a.findingIds.some(id=>!findings.some(f=>f.id===id))))return hold(state,'finding-recheck','Unknown finding recheck');
|
|
79
|
+
if(findings.some(f=>f.status!=='resolved'||!text(f.resolutionRef)||f.resolutionActor?.role!=='implementer'||f.changeFingerprint!==a.changeFingerprint||!a.findingIds?.includes(f.id)||f.origin==='final-review'&&request.operation!=='review'||f.origin!=='final-review'&&request.operation!=='check'))return hold(state,'finding-recheck','Implementer resolution and the required QA or final Reviewer recheck required');
|
|
80
|
+
if(state.intents.some(i=>['check','review'].includes(i.operation)&&['prepared','unknown','mismatch'].includes(i.status)&&i.scope===a.scope&&i.taskId===request.taskId&&same(i.descriptor,a.descriptor)&&!i.stale))return hold(state,'verification-active','Reconcile the existing verification intent');
|
|
81
|
+
const intent={requestId:request.requestId,operation:request.operation,status:'prepared',scope:a.scope,taskId:request.taskId,verificationActor:structuredClone(a.actor),descriptor:structuredClone(a.descriptor),changeFingerprint:a.changeFingerprint,findingIds:[...(a.findingIds??[])],scopeVersion:state.scopeVersion,targetVersion:state.targetVersion,versions:structuredClone(state.versions),target:structuredClone(state.target),approval:structuredClone(approval(state,request.taskId)),taskFingerprint:fingerprintRequest(a.scope==='final'?state.tasks:t),fingerprint:fingerprintRequest(request),source:'operator-attested'};
|
|
82
|
+
return allow(state,{intent});
|
|
83
|
+
}
|
|
84
|
+
function outcome({state,request,evidence}){
|
|
85
|
+
const intent=state.intents.find(i=>i.requestId===request.intentId),r=request.result,d=r?.data;
|
|
86
|
+
if(!intent||!['check','review'].includes(intent.operation)||!['prepared','unknown','mismatch'].includes(intent.status))return hold(state,'verification-intent','Matching unresolved verification intent required');
|
|
87
|
+
const valid=r&&['succeeded','failed','unknown','mismatch'].includes(r.status)&&text(r.messageRef)&&fields(d,['actor','descriptor','scope','taskId','changeFingerprint','verdict'])&&['proven','unmet','unverified'].includes(d.verdict)&&same(d.actor,intent.verificationActor)&&same(d.descriptor,intent.descriptor)&&d.scope===intent.scope&&d.taskId===intent.taskId&&d.changeFingerprint===intent.changeFingerprint;
|
|
88
|
+
const fresh=!intent.stale&&intent.changeFingerprint===current(state,intent.scope,intent.taskId,evidence?.changes)&&(intent.scope==='task'||intent.scopeVersion===state.scopeVersion)&&intent.targetVersion===state.targetVersion&&same(intent.target,state.target)&&same(intent.versions,state.versions)&&independent(state,intent.verificationActor,intent.taskId)&&same(intent.approval,approval(state,intent.taskId))&&intent.taskFingerprint===fingerprintRequest(intent.scope==='final'?state.tasks:state.tasks.find(t=>t.id===intent.taskId));
|
|
89
|
+
const status=!valid?'mismatch':r.status;
|
|
90
|
+
const check={id:intent.requestId,operation:intent.operation,scope:intent.scope,taskId:intent.taskId,descriptor:structuredClone(intent.descriptor),verificationActor:structuredClone(intent.verificationActor),changeFingerprint:intent.changeFingerprint,resultRef:r?.messageRef,verdict:valid?d.verdict:'unverified',status:fresh&&status==='succeeded'&&d.verdict==='proven'?'passed':status==='failed'||d?.verdict==='unmet'?'failed':'unverified',stale:!fresh,source:'operator-attested'};
|
|
91
|
+
const findings=[];
|
|
92
|
+
if(check.status==='failed')findings.push({id:intent.requestId+':failure',checkId:check.id,scope:intent.scope,taskId:intent.taskId,origin:intent.operation==='review'?'final-review':'check',status:'open',source:'operator-attested',messageRef:r.messageRef});
|
|
93
|
+
if(check.status==='passed')for(const id of intent.findingIds){const f=state.findings.find(f=>f.id===id);if(!f||f.status!=='resolved'||f.changeFingerprint!==intent.changeFingerprint)return hold(state,'finding-recheck','Finding changed after prepare');findings.push({...f,status:'closed',recheckRef:r.messageRef,recheckActor:structuredClone(d.actor)});}
|
|
94
|
+
const transition={result:{...structuredClone(r),status,source:'operator-attested'},check,findings};
|
|
95
|
+
return check.status==='passed'?allow(state,transition):{...hold(state,'verification-'+status,'Recorded outcome does not establish passing current evidence'),proposedTransition:transition};
|
|
96
|
+
}
|
|
97
|
+
function defaultRunner({cwd,args}){
|
|
98
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([k])=>!k.toUpperCase().startsWith('GIT_')));
|
|
99
|
+
Object.assign(env,{GIT_OPTIONAL_LOCKS:'0',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
100
|
+
return spawnSync('git',['--no-pager','--literal-pathspecs','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'buffer',timeout:10000,maxBuffer:16*1024*1024});
|
|
101
|
+
}
|
|
102
|
+
/** Read tracked/untracked regular bytes, deletions and modes without staging or execution.
|
|
103
|
+
* The snapshot is cooperative evidence at observation time, not an atomic execution sandbox. */
|
|
104
|
+
export async function readCooperativeChanges({state,gitRunner=defaultRunner}){
|
|
105
|
+
const inventory=await gitRunner({cwd:state.target.realpath,args:['ls-files','--cached','--others','--exclude-standard','-z']});
|
|
106
|
+
if(inventory?.status!==0||inventory.error||!Buffer.isBuffer(inventory.stdout))throw Error('Verification inventory query failed');
|
|
107
|
+
const raw=new TextDecoder('utf-8',{fatal:true}).decode(inventory.stdout);
|
|
108
|
+
if(raw&&!raw.endsWith('\0'))throw Error('Incomplete verification inventory');
|
|
109
|
+
const index=await gitRunner({cwd:state.target.realpath,args:['ls-files','--stage','-z']});
|
|
110
|
+
if(index?.status!==0||index.error||!Buffer.isBuffer(index.stdout))throw Error('Verification index query failed');
|
|
111
|
+
const indexText=new TextDecoder('utf-8',{fatal:true}).decode(index.stdout),indexRows=[];
|
|
112
|
+
if(indexText&&!indexText.endsWith('\0'))throw Error('Incomplete verification index');
|
|
113
|
+
for(const entry of indexText?indexText.slice(0,-1).split('\0'):[]){const match=/^(100644|100755) ([a-f0-9]{40}|[a-f0-9]{64}) 0\t([\s\S]+)$/.exec(entry);if(!match)throw Error('Verification requires regular stage-0 index entries');indexRows.push({path:path.join(state.target.realpath,match[3]),entry});}
|
|
114
|
+
const names=[...new Set(raw?raw.slice(0,-1).split('\0'):[])].sort(),rows=[];
|
|
115
|
+
for(const name of names){
|
|
116
|
+
if(!name||name.includes('\\')||name.includes('\n')||name.includes('\r')||path.isAbsolute(name)||name.split('/').some(p=>!p||p==='.'||p==='..'))throw Error('Invalid verification path');
|
|
117
|
+
const file=path.join(state.target.realpath,...name.split('/'));
|
|
118
|
+
try{await inspectPath(file);const stat=await fs.lstat(file);if(!stat.isFile())throw Error('Verification requires regular files');rows.push({path:file,mode:stat.mode&0o777,hash:'sha256:'+createHash('sha256').update(await fs.readFile(file)).digest('hex')});}
|
|
119
|
+
catch(error){if(error.code!=='ENOENT')throw error;rows.push({path:file,deleted:true});}
|
|
120
|
+
}
|
|
121
|
+
const tasks={},taskSnapshots={};for(const task of state.tasks){if(!list(task.paths)||!task.paths.every(p=>absolute(p)&&within(state.target.realpath,p)))throw Error('Invalid task paths');taskSnapshots[task.id]={scope:'task',taskId:task.id,target:Object.fromEntries(Object.entries(state.target).filter(([key])=>key!=='head')),paths:task.paths,index:indexRows.filter(row=>task.paths.some(p=>within(p,row.path))),rows:rows.filter(row=>task.paths.some(p=>within(p,row.path)))};tasks[task.id]=fingerprintRequest(taskSnapshots[task.id]);}
|
|
122
|
+
return {tasks,taskSnapshots,final:fingerprintRequest({scope:'final',target:state.target,index:indexRows,rows})};
|
|
123
|
+
}
|
|
@@ -13,7 +13,7 @@ from typing import TypedDict
|
|
|
13
13
|
ROLE_FILES: dict[str, str] = {
|
|
14
14
|
"arc_scouter": "arc-scouter.toml",
|
|
15
15
|
"arc_planner": "arc-planner.toml",
|
|
16
|
-
"
|
|
16
|
+
"arc_implementer": "arc-implementer.toml",
|
|
17
17
|
"arc_qa": "arc-qa.toml",
|
|
18
18
|
"arc_reviewer": "arc-reviewer.toml",
|
|
19
19
|
}
|
|
@@ -95,13 +95,13 @@ def validate_config(config: dict, path: Path) -> dict:
|
|
|
95
95
|
def load_settings(config_path: Path) -> dict[str, RoleSettings]:
|
|
96
96
|
"""Validate every preset and return the active role settings."""
|
|
97
97
|
_, _, config = _parse_file(config_path, "settings")
|
|
98
|
-
validate_config(config, config_path)
|
|
98
|
+
config = validate_config(config, config_path)
|
|
99
99
|
return {role: dict(variants[config["preset"]]) for role, variants in config["roles"].items()}
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def serialize_settings(config: dict) -> bytes:
|
|
103
103
|
"""Serialize a complete unified configuration without losing inactive presets."""
|
|
104
|
-
validate_config(config, Path("models.toml"))
|
|
104
|
+
config = validate_config(config, Path("models.toml"))
|
|
105
105
|
lines = ["version = 1", f'preset = {json.dumps(config["preset"])}']
|
|
106
106
|
for role in ROLE_FILES:
|
|
107
107
|
for preset in PRESETS:
|
package/lib/cli/postinstall.mjs
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {runCli} from './index.mjs';
|
|
5
|
-
export async function runPostinstall({packageRoot,env=process.env,cwd=process.cwd(),input=process.stdin,output=process.stdout,error=process.stderr,deps={}}){
|
|
6
|
-
let terminal;
|
|
7
|
-
try{
|
|
8
|
-
if(env.npm_lifecycle_event!=='postinstall'){output.write('Run arc to install the ARC harness.\n');return 0;}
|
|
9
|
-
const context=await(deps.inspectNpmContext??inspectNpmContext)({packageRoot,entryPath:path.join(packageRoot,'scripts/postinstall.mjs'),env,cwd});
|
|
10
|
-
if(context.kind!=='global'){output.write('Run arc to install the ARC harness.\n');return 0;}
|
|
11
|
-
terminal=(deps.openInstallerTerminal??openInstallerTerminal)({env,input,output,error});
|
|
12
|
-
if(terminal.kind==='skip'){output.write('Run arc to install the ARC harness.\n');return 0;}
|
|
13
|
-
const invocationCwd=resolveInvocationCwd({env,cwd,lifecycle:true});
|
|
14
|
-
return await(deps.runCli??runCli)([],{...terminal.io,cwd:invocationCwd,env});
|
|
15
|
-
}catch(cause){(terminal?.io.error??error).write(`Error: ${cause.message}\n`);return 1;}
|
|
16
|
-
finally{terminal?.dispose?.();}
|
|
1
|
+
export async function runPostinstall({output=process.stdout}={}){
|
|
2
|
+
output.write('Run arc to install or manage the ARC harness.\n');
|
|
3
|
+
return 0;
|
|
17
4
|
}
|
package/lib/cli/root.mjs
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc upgrade [--help]\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
|
|
4
|
-
const completionUsage='Usage: arc completion <zsh|bash>';
|
|
2
|
+
const usage='Usage: arc\n arc --help\n arc --version';
|
|
5
3
|
export async function runRoot(args=process.argv.slice(2),io={}){
|
|
6
|
-
const output=io.output??process.stdout,error=io.error??process.stderr;
|
|
7
|
-
|
|
8
|
-
if(
|
|
9
|
-
if(
|
|
10
|
-
if(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if(command===undefined||command.startsWith('--'))return(await import('./index.mjs')).runCli(args,io);
|
|
14
|
-
if(command==='uninstall')return(await import('./uninstall.mjs')).runCli(rest,io);
|
|
15
|
-
if(command==='completion'){
|
|
16
|
-
if(rest.length===1&&rest[0]==='--help'){output.write(completionUsage+'\n');return 0;}
|
|
17
|
-
if(rest.length===1&&['zsh','bash'].includes(rest[0])){output.write(renderCompletion(rest[0]));return 0;}
|
|
18
|
-
error.write('Error: Expected a completion shell (zsh or bash).\n'+completionUsage+'\n');return 2;
|
|
19
|
-
}
|
|
20
|
-
error.write(`Error: Unknown command: ${command}\n${usage}\n`);return 2;
|
|
4
|
+
const input=io.input??process.stdin,output=io.output??process.stdout,error=io.error??process.stderr;
|
|
5
|
+
if(args.length===1&&args[0]==='--help'){output.write(usage+'\n\nOpen ARC Management in an interactive terminal to install or manage the harness.\n');return 0;}
|
|
6
|
+
if(args.length===1&&args[0]==='--version'){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
|
|
7
|
+
if(args.length){error.write('Error: Unsupported arguments. Open arc to manage the harness.\n'+usage+'\n');return 2;}
|
|
8
|
+
if(!input.isTTY||!output.isTTY){error.write('Error: arc requires an interactive terminal. Run arc directly in a terminal.\n');return 2;}
|
|
9
|
+
const {runTui}=await import('../tui/app.mjs');
|
|
10
|
+
return runTui({...io,input,output,contextOptions:{...io.contextOptions,...(io.cwd?{cwd:io.cwd}:{}),...(io.env?{env:io.env}:{})}});
|
|
21
11
|
}
|
package/lib/cli/upgrade.mjs
CHANGED
|
@@ -1,57 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import {fileURLToPath} from 'node:url';
|
|
5
|
-
const packageRoot=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
|
|
6
|
-
const usage='Usage: arc upgrade [--help]';
|
|
7
|
-
const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
|
|
8
|
-
const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
|
|
9
|
-
function identity(root){const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(pkg.name!=='@yunhozz/arc'||!/^\d+\.\d+\.\d+$/.test(pkg.version))throw configurationError('Invalid ARC package identity');return pkg;}
|
|
10
|
-
async function runtime(deps){
|
|
11
|
-
const [context,state,upgrade,npm]=await Promise.all([import('../npm/context.mjs'),import('../installer/upgrade-state.mjs'),import('../installer/upgrade.mjs'),import('../npm/upgrade.mjs')]);
|
|
12
|
-
let safeFs=deps.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:root=>openRoot(root,{native})};}
|
|
13
|
-
return{...context,...state,...upgrade,...npm,...deps,safeFs};
|
|
14
|
-
}
|
|
15
|
-
function report(result,output,error){
|
|
16
|
-
for(const item of result.results){output.write(item.platform+': '+item.status+'; rollback: '+item.rollback+'\n');for(const filename of item.residualPaths??[])output.write('Residual path: '+filename+'\n');if(item.error)error.write('Error: '+item.error.message+'\n');}
|
|
17
|
-
if(result.interrupted)return 130;
|
|
18
|
-
if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\nOpen a new shell session to use updated completion.\nzsh completion: eval "$(arc completion zsh)" for this session\n');return 0;}
|
|
19
|
-
error.write('Harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return 1;
|
|
20
|
-
}
|
|
21
|
-
function failure(cause,error){error.write('Error: '+cause.message+'\n');return ['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(cause.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(cause.code)?2:1;}
|
|
22
|
-
export async function runCli(args=process.argv.slice(2),io={},deps={}){
|
|
23
|
-
const output=io.output??process.stdout,error=io.error??process.stderr;let controller,onInterrupt,packageStarted=false;
|
|
24
|
-
try{
|
|
25
|
-
if(args.length===1&&args[0]==='--help'){output.write(usage+'\nUpdates the global CLI and installed harness; npm exec updates the harness only.\n');return 0;}
|
|
26
|
-
if(args.length)throw inputError('Expected no arguments or --help');
|
|
27
|
-
const root=deps.packageRoot??packageRoot,pkg=identity(root),env=io.env??process.env,cwd=fs.realpathSync(io.cwd??process.cwd()),api=await runtime(deps);
|
|
28
|
-
const context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
|
|
29
|
-
if(!['global','npx'].includes(context.kind))throw configurationError('Run upgrade from a verified global ARC installation or npx --yes @yunhozz/arc@latest upgrade');
|
|
30
|
-
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});
|
|
31
|
-
if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
|
|
32
|
-
controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
|
|
33
|
-
if(context.kind==='npx')return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
|
|
34
|
-
const latest=await api.resolveLatest({context,env,cwd,signal:controller.signal});packageStarted=true;
|
|
35
|
-
const replaced=await api.upgradeGlobal({context,expectedVersion:latest.version,env,cwd,signal:controller.signal});
|
|
36
|
-
if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
|
|
37
|
-
output.write('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
|
|
38
|
-
const result=await api.runUpgradeChild({entryPath:path.join(context.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:controller.signal});
|
|
39
|
-
if(result.stdout)output.write(result.stdout);if(result.stderr)error.write(result.stderr);
|
|
40
|
-
if(result.code===0&&!result.signal)return 0;
|
|
41
|
-
error.write('CLI was updated but the harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return result.code===130?130:1;
|
|
42
|
-
}catch(cause){const code=failure(cause,error);if(cause.code==='ARC_CLI_INPUT')error.write(usage+'\n');if(packageStarted)error.write('CLI state may have changed. Retry arc upgrade; if it is unavailable, run npx --yes @yunhozz/arc@latest upgrade.\n');return code;}
|
|
43
|
-
finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
|
|
44
|
-
}
|
|
45
|
-
export async function runChild(request,io={},deps={}){
|
|
46
|
-
const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
|
|
47
|
-
try{
|
|
48
|
-
if(!request||Object.keys(request).sort().join(',')!=='expectedStateToken,expectedVersion,invocationCwd,protocol'||request.protocol!==1||typeof request.expectedVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(request.expectedVersion)||typeof request.expectedStateToken!=='string'||!/^[a-f0-9]{64}$/.test(request.expectedStateToken)||typeof request.invocationCwd!=='string'||!path.isAbsolute(request.invocationCwd)||fs.realpathSync(request.invocationCwd)!==request.invocationCwd)throw inputError('Invalid private upgrade request');
|
|
49
|
-
const root=deps.packageRoot??packageRoot,pkg=identity(root);if(pkg.version!==request.expectedVersion)throw configurationError('The new ARC package version does not match the requested upgrade');
|
|
50
|
-
const api=await runtime(deps),env=io.env??process.env,cwd=request.invocationCwd,context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc-upgrade.mjs'),env,cwd});
|
|
51
|
-
if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
|
|
52
|
-
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});if(state.stateToken!==request.expectedStateToken)throw configurationError('Installed ARC settings changed during CLI replacement; retry upgrade');
|
|
53
|
-
if(!state.targets.length)throw configurationError('No installed ARC harness found');
|
|
54
|
-
const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
|
|
55
|
-
return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
|
|
56
|
-
}catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
|
|
57
|
-
}
|
|
1
|
+
export async function runChild(...args){return(await import('../management/update.mjs')).runChild(...args);}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {COOPERATIVE_RUNTIME_MODULES} from '../runtime/bootstrap.mjs';
|
|
2
|
+
import {RUNTIME_MODULE_PATHS} from './runtime-assets.mjs';
|
|
1
3
|
import {POLICY_TARGETS,buildPolicyOutputs} from '../policy/render.mjs';
|
|
2
4
|
import {isDynamicReferencePath,readReferenceBundle} from './superpowers.mjs';
|
|
3
5
|
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';
|
|
@@ -23,9 +25,12 @@ export function verifyPayload({packageRoot,safeFs}){
|
|
|
23
25
|
for(const item of FIXED)select(item,false);for(const item of ROOTS)select(item,true);
|
|
24
26
|
for(const platform of ['codex','claude'])if(!referenceTrees[platform])throw Error('Required superpowers reference tree is missing');
|
|
25
27
|
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/
|
|
28
|
+
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/tui/app.mjs','lib/tui/terminal.mjs','lib/tui/render.mjs','lib/tui/setup.mjs','lib/tui/workflow.mjs','lib/management/operations.mjs','lib/management/status.mjs','lib/management/update.mjs','lib/management/workflow.mjs','lib/cli/postinstall.mjs','scripts/postinstall.mjs','lib/cli/upgrade.mjs','bin/arc-upgrade.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'];
|
|
29
|
+
required.push('lib/installer/runtime-assets.mjs',...RUNTIME_MODULE_PATHS);
|
|
30
|
+
for(const platform of ['codex','claude'])required.push(`${platform}/skill/runtime/role-policy.json`,...RUNTIME_MODULE_PATHS.map(name=>`${platform}/skill/runtime/${name.slice('lib/runtime/'.length)}`));
|
|
27
31
|
for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
|
|
28
|
-
|
|
32
|
+
for(const platform of ['codex','claude'])for(const name of [...COOPERATIVE_RUNTIME_MODULES,'role-policy.json'])required.push(`${platform}/skill/runtime/${name}`);
|
|
33
|
+
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','implementer','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
34
|
for(const name of required)if(!files.has(name))throw Error(`Required runtime asset is missing: ${name}`);
|
|
30
35
|
const policyInputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,files.get(name)]));
|
|
31
36
|
const policyOutputs=buildPolicyOutputs(policyInputs,JSON.parse(files.get('lib/policy/role-policy.json').toString('utf8')));
|
package/lib/installer/plan.mjs
CHANGED
|
@@ -17,23 +17,19 @@ export function previewLanguage({target,safeFs}){
|
|
|
17
17
|
let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
|
|
18
18
|
return{existing,snapshot};
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
validateLanguage(language);
|
|
22
|
-
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
23
|
-
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
24
|
-
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
25
|
-
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
26
|
-
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
20
|
+
function renderModelAssets({payload,target,preset,modelConfig,language}){
|
|
27
21
|
const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
|
|
28
22
|
const config=modelConfig===undefined?applyPreset(loadModelConfig(input.models,target.platform),preset):validateModelConfig(modelConfig);if(config.platform!==target.platform)fail('Model config platform does not match target');const profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
|
|
29
23
|
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}`);}}
|
|
24
|
+
return{profiles,modelBytes};
|
|
25
|
+
}
|
|
26
|
+
function renderSkillPayload({payload,target,language,modelBytes,limits}){
|
|
30
27
|
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));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const globalChanges=[];summary.push(`ARC limits: total ${limits.max_total_subagents}; concurrent ${limits.max_concurrent_subagents} (policy: ${arc}/limits.toml)`);
|
|
28
|
+
return skill;
|
|
29
|
+
}
|
|
30
|
+
function renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurrentExplicit,limits,summary,language}){
|
|
31
|
+
const home=target.home;
|
|
32
|
+
const globalChanges=[];
|
|
37
33
|
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.`);
|
|
38
34
|
if(skipGlobal)summary.push('Native concurrency update skipped (--skip-global).');
|
|
39
35
|
else for(const filename of target.globalPaths){
|
|
@@ -50,6 +46,24 @@ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,
|
|
|
50
46
|
if(rendered.content!==null&&(original===null||!rendered.content.equals(original)))globalChanges.push({path:filename,original,content:rendered.content,action:rendered.action});
|
|
51
47
|
for(const note of rendered.notes??[])summary.push(`${filename}: ${note}`);
|
|
52
48
|
}
|
|
49
|
+
return globalChanges;
|
|
50
|
+
}
|
|
51
|
+
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
|
|
52
|
+
validateLanguage(language);
|
|
53
|
+
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
54
|
+
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
55
|
+
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
56
|
+
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
57
|
+
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
58
|
+
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language});
|
|
59
|
+
const skill=renderSkillPayload({payload,target,language,modelBytes,limits});
|
|
60
|
+
const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
|
|
61
|
+
for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
|
|
62
|
+
inspectPath(safeFs,arc,{kind:'directory',tree:true,allowLinks:true,snapshot});
|
|
63
|
+
const profilePaths=target.profileNames.map(name=>path.join(agents,name));for(const name of profilePaths)inspectPath(safeFs,name,{kind:'file',snapshot});
|
|
64
|
+
const externalPaths=[],summary=['Fresh superpowers references will be acquired inside ARC'];
|
|
65
|
+
summary.push(`ARC limits: total ${limits.max_total_subagents}; concurrent ${limits.max_concurrent_subagents} (policy: ${arc}/limits.toml)`);
|
|
66
|
+
const globalChanges=renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurrentExplicit,limits,summary,language});
|
|
53
67
|
const managedPaths=[arc,...profilePaths,...(skipGlobal?[]:target.globalPaths)];
|
|
54
68
|
for(const [filename,metadata]of preview.snapshot)snapshot.set(filename,metadata);
|
|
55
69
|
for(const [filename,metadata]of languageState.snapshot)snapshot.set(filename,metadata);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Installation inventory uses repository-root-relative POSIX paths.
|
|
2
|
+
// Keep install-only additions separate from cooperative runtime hash inputs.
|
|
3
|
+
export const RUNTIME_MODULE_PATHS = Object.freeze([
|
|
4
|
+
'lib/runtime/acceptance.mjs',
|
|
5
|
+
'lib/runtime/adapters/claude.mjs',
|
|
6
|
+
'lib/runtime/adapters/codex.mjs',
|
|
7
|
+
'lib/runtime/assignment.mjs',
|
|
8
|
+
'lib/runtime/bootstrap.mjs',
|
|
9
|
+
'lib/runtime/capabilities.mjs',
|
|
10
|
+
'lib/runtime/cli.mjs',
|
|
11
|
+
'lib/runtime/command.mjs',
|
|
12
|
+
'lib/runtime/cooperative-schema.mjs',
|
|
13
|
+
'lib/runtime/cooperative.mjs',
|
|
14
|
+
'lib/runtime/evaluate.mjs',
|
|
15
|
+
'lib/runtime/external.mjs',
|
|
16
|
+
'lib/runtime/gate.mjs',
|
|
17
|
+
'lib/runtime/git-evidence.mjs',
|
|
18
|
+
'lib/runtime/git-query.mjs',
|
|
19
|
+
'lib/runtime/identity.mjs',
|
|
20
|
+
'lib/runtime/operations.mjs',
|
|
21
|
+
'lib/runtime/schema.mjs',
|
|
22
|
+
'lib/runtime/scope.mjs',
|
|
23
|
+
'lib/runtime/store.mjs',
|
|
24
|
+
'lib/runtime/verification.mjs'
|
|
25
|
+
]);
|
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {applyTransaction} from './transaction.mjs';
|
|
3
2
|
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
4
3
|
const start='# >>> arc completion >>>',end='# <<< arc completion <<<';
|
|
5
|
-
|
|
6
|
-
export function renderShellCompletion(original){
|
|
4
|
+
export function removeShellCompletion(original){
|
|
7
5
|
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
6
|
const starts=[...text.matchAll(/# >>> arc completion >>>/g)],ends=[...text.matchAll(/# <<< arc completion <<</g)];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if(starts.length){
|
|
12
|
-
|
|
7
|
+
const malformed=()=>{throw new TargetConfigurationError('Malformed ARC completion markers');};
|
|
8
|
+
if(starts.length!==ends.length||starts.length>1)malformed();
|
|
9
|
+
if(!starts.length)return{changed:false,content:original};
|
|
10
|
+
const first=starts[0].index,last=ends[0].index;let stop=last+end.length;
|
|
11
|
+
if(first>=last||(first>0&&text[first-1]!=='\n')||(last>0&&text[last-1]!=='\n')||!['','\n','\r\n'].includes(text.slice(stop,stop+2).startsWith('\r')?text.slice(stop,stop+2):text.slice(stop,stop+1)))malformed();
|
|
12
|
+
if(text.slice(stop,stop+2)==='\r\n')stop+=2;else if(text[stop]==='\n')stop++;
|
|
13
|
+
return{changed:true,content:Buffer.from(text.slice(0,first)+text.slice(stop))};
|
|
13
14
|
}
|
|
14
|
-
export function
|
|
15
|
-
const plan=plans[0];if(!plan
|
|
15
|
+
export function attachShellCompletionCleanup(plans,{safeFs}){
|
|
16
|
+
const plan=plans[0];if(!plan)return;
|
|
16
17
|
const filename=path.join(plan.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:plan.snapshot}),original=current.bytes??null;
|
|
18
|
+
const removed=removeShellCompletion(original);
|
|
19
|
+
for(const item of plans){item.managedPaths=item.managedPaths.filter(name=>name!==filename);item.globalChanges=item.globalChanges.filter(change=>change.path!==filename);if(item!==plan)item.snapshot.delete(filename);}
|
|
20
|
+
if(!removed.changed)return;
|
|
17
21
|
const metadata=plan.snapshot.get(filename);if(metadata&&(metadata.mode&0o222n)===0n)throw new TargetConfigurationError('.zshrc is not writable');
|
|
18
|
-
|
|
19
|
-
|
|
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'};
|
|
22
|
+
plan.managedPaths.push(filename);plan.globalChanges.push({path:filename,original,content:removed.content,action:'remove managed completion'});
|
|
23
|
+
plan.summary.push(`${filename}: remove managed ARC completion block`);
|
|
29
24
|
}
|
|
@@ -9,7 +9,7 @@ export function resolveTarget(platform,{userHome,env={},cwd}={}){
|
|
|
9
9
|
let home;if(platform==='codex'){const raw=env.CODEX_HOME;if(raw!==undefined&&(typeof raw!=='string'||!raw.trim()))fail('CODEX_HOME is empty or invalid');const expanded=expand(raw??path.join(userHome,'.codex'));if(path.isAbsolute(expanded))home=path.resolve(expanded);else{const base=cwd??process.cwd();if(typeof base!=='string'||!path.isAbsolute(base))fail('cwd must be absolute for relative CODEX_HOME');home=path.resolve(base,expanded);}}
|
|
10
10
|
else{home=path.join(userHome,'.claude');const raw=env.CLAUDE_CONFIG_DIR;if(raw!==undefined&&(typeof raw!=='string'||!raw.trim()||!path.isAbsolute(expand(raw))||path.resolve(expand(raw))!==home))fail('CLAUDE_CONFIG_DIR must select the default ~/.claude directory');}
|
|
11
11
|
if(home==='/'||home===path.resolve(userHome))fail('Target home must not be the filesystem or user-home root');
|
|
12
|
-
const profileNames=['scouter','planner','
|
|
12
|
+
const profileNames=['scouter','planner','implementer','qa','reviewer'].map(role=>`arc-${role}.${platform==='codex'?'toml':'md'}`);
|
|
13
13
|
return Object.freeze({platform,home,userHome:path.resolve(userHome),payloadPrefix:platform,profileNames:Object.freeze(profileNames),instructionPath:path.join(home,platform==='codex'?'AGENTS.md':'CLAUDE.md'),runtimeConfigPath:path.join(home,platform==='codex'?'config.toml':'settings.json'),globalPaths:Object.freeze(platform==='codex'?[path.join(home,'AGENTS.md'),path.join(home,'config.toml')]:[path.join(home,'CLAUDE.md'),path.join(home,'settings.json')])});
|
|
14
14
|
}
|
|
15
15
|
export function typeOf(metadata){return Number(metadata.mode&0o170000n);}
|
|
@@ -3,6 +3,7 @@ import {inspectPath,sameIdentity,TargetConfigurationError} from './targets.mjs';
|
|
|
3
3
|
import {SKILLS,snapshotSkill} from './superpowers.mjs';
|
|
4
4
|
import {validatePlanSet} from './plan.mjs';
|
|
5
5
|
import {removeManagedBlock} from './global-config.mjs';
|
|
6
|
+
import {attachShellCompletionCleanup} from './shell-completion.mjs';
|
|
6
7
|
export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
7
8
|
const snapshot=new Map(),removals=[],managedPaths=[path.join(target.home,'skills/arc'),...target.profileNames.map(name=>path.join(target.home,'agents',name))],globalChanges=[];
|
|
8
9
|
for(const filename of managedPaths){const directory=filename===managedPaths[0];const found=inspectPath(safeFs,filename,{kind:directory?'directory':'file',tree:directory,allowLinks:directory,snapshot});if(found.exists)removals.push(filename);}
|
|
@@ -14,11 +15,14 @@ export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
|
14
15
|
}
|
|
15
16
|
for(const skill of linkedSkills){const root=arc+'/references/superpowers/'+skill,directory=safeFs.openRoot(root);try{if(!sameIdentity(snapshot.get(root),directory.stat()))throw new TargetConfigurationError('Removal reference root changed');snapshotSkill(directory,root,[]);}finally{directory.close();}}
|
|
16
17
|
if(!skipGlobal){const filename=target.instructionPath??target.globalPaths[0],found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});managedPaths.push(filename);if(found.exists){const removed=removeManagedBlock(found.bytes);if(removed.changed)globalChanges.push({path:filename,content:removed.content});}}
|
|
17
|
-
|
|
18
|
+
const plan={target,skipGlobal,snapshot,removals,managedPaths,externalPaths:[],globalChanges,summary:[target.platform==='codex'?'Codex config.toml wait settings and native concurrency key are preserved.':'Claude settings.json native concurrency key is preserved.']};
|
|
19
|
+
attachShellCompletionCleanup([plan],{safeFs});return plan;
|
|
18
20
|
}
|
|
19
21
|
import {assertPlanSnapshot,applyUninstallTransaction} from './transaction.mjs';
|
|
20
22
|
export async function uninstallTargets(plans,{safeFs,signal,hooks={}}){
|
|
21
23
|
const results=plans.map(plan=>({platform:plan.target.platform,status:'not-started',rollback:'not-needed',residualPaths:[],error:null}));let invalid=false;
|
|
24
|
+
// Each standalone preview retains its snapshot; only the first plan owns the shared shell write.
|
|
25
|
+
for(const plan of plans.slice(1)){const filename=path.join(plans[0].target.userHome,'.zshrc');plan.managedPaths=plan.managedPaths.filter(name=>name!==filename);plan.globalChanges=plan.globalChanges.filter(change=>change.path!==filename);plan.snapshot.delete(filename);}
|
|
22
26
|
try{validatePlanSet(plans);}catch(error){return{results:results.map(item=>({...item,status:'failed',error})),interrupted:false};}
|
|
23
27
|
for(let i=0;i<plans.length;i++)try{assertPlanSnapshot(plans[i],{safeFs});}catch(error){results[i]={...results[i],status:'failed',error};invalid=true;}
|
|
24
28
|
if(invalid)return{results,interrupted:false};
|