@yunhozz/arc 0.2.4 → 0.2.6
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 +88 -78
- package/claude/agents/arc-implementer.md +8 -23
- package/claude/agents/arc-planner.md +14 -23
- package/claude/agents/arc-qa.md +9 -19
- package/claude/agents/arc-reviewer.md +11 -20
- package/claude/agents/arc-scouter.md +10 -17
- package/claude/skill/SKILL.md +9 -9
- package/claude/skill/models.toml +11 -11
- package/claude/skill/references/arc-visual-companion.md +3 -3
- package/claude/skill/references/brainstorming.md +153 -62
- package/claude/skill/references/commit.md +6 -6
- package/claude/skill/references/coordination.md +20 -8
- package/claude/skill/references/git-recovery.md +2 -2
- package/claude/skill/references/handoff-prep.md +7 -5
- package/claude/skill/references/handoff.md +62 -9
- package/claude/skill/references/limits.md +5 -5
- package/claude/skill/references/planning.md +22 -16
- package/claude/skill/references/scripts/start-server.sh +7 -0
- package/claude/skill/references/superpowers-provenance.json +4 -4
- package/claude/skill/references/verification.md +6 -4
- package/claude/skill/runtime/bootstrap.mjs +2 -2
- package/claude/skill/runtime/cooperative-schema.mjs +32 -1
- package/claude/skill/runtime/cooperative.mjs +68 -13
- package/claude/skill/runtime/evaluate.mjs +85 -6
- package/claude/skill/runtime/git-evidence.mjs +98 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +6 -6
- package/claude/skill/runtime/store.mjs +10 -0
- package/claude/skill/runtime/target-preparation.mjs +110 -0
- package/claude/skill/runtime/verification.mjs +6 -4
- package/claude/skill/scripts/model_config.py +17 -5
- package/codex/agents/arc-implementer.toml +10 -25
- package/codex/agents/arc-planner.toml +12 -19
- package/codex/agents/arc-qa.toml +7 -15
- package/codex/agents/arc-reviewer.toml +9 -16
- package/codex/agents/arc-scouter.toml +8 -13
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +13 -13
- package/codex/skill/references/arc-visual-companion.md +3 -3
- package/codex/skill/references/brainstorming.md +153 -62
- package/codex/skill/references/commit.md +6 -6
- package/codex/skill/references/coordination.md +20 -8
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +5 -5
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +61 -8
- package/codex/skill/references/limits.md +5 -5
- package/codex/skill/references/planning.md +23 -17
- package/codex/skill/references/scripts/start-server.sh +7 -0
- package/codex/skill/references/superpowers-provenance.json +4 -4
- package/codex/skill/references/verification.md +6 -4
- package/codex/skill/runtime/bootstrap.mjs +2 -2
- package/codex/skill/runtime/cooperative-schema.mjs +32 -1
- package/codex/skill/runtime/cooperative.mjs +68 -13
- package/codex/skill/runtime/evaluate.mjs +85 -6
- package/codex/skill/runtime/git-evidence.mjs +98 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +6 -6
- package/codex/skill/runtime/store.mjs +10 -0
- package/codex/skill/runtime/target-preparation.mjs +110 -0
- package/codex/skill/runtime/verification.mjs +6 -4
- package/codex/skill/scripts/model_config.py +25 -6
- package/lib/installer/global-config.mjs +1 -1
- package/lib/installer/manifest.mjs +6 -2
- package/lib/installer/plan.mjs +8 -5
- package/lib/installer/preset.mjs +7 -4
- package/lib/installer/runtime-assets.mjs +1 -0
- package/lib/management/operations.mjs +1 -1
- package/lib/models/catalog-anthropic.mjs +46 -0
- package/lib/models/catalog.mjs +62 -0
- package/lib/models/codex-catalog.mjs +103 -0
- package/lib/models/config.mjs +19 -11
- package/lib/models/index.mjs +1 -1
- package/lib/policy/role-policy.json +6 -6
- package/lib/prompts/agent-fragments.json +254 -0
- package/lib/prompts/render.mjs +199 -0
- package/lib/runtime/bootstrap.mjs +2 -2
- package/lib/runtime/cooperative-schema.mjs +32 -1
- package/lib/runtime/cooperative.mjs +68 -13
- package/lib/runtime/evaluate.mjs +85 -6
- package/lib/runtime/git-evidence.mjs +98 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/store.mjs +10 -0
- package/lib/runtime/target-preparation.mjs +110 -0
- package/lib/runtime/verification.mjs +6 -4
- package/lib/tui/app.mjs +15 -9
- package/lib/tui/custom-preset.mjs +122 -0
- package/lib/tui/render.mjs +3 -2
- package/lib/tui/setup.mjs +65 -8
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
2
|
+
const text=value=>typeof value==='string'&&value.length>0;
|
|
3
|
+
const relative=value=>text(value)&&!/[\\\x00\r\n:]/.test(value)&&!value.startsWith('/')&&!value.split('/').some(part=>!part||part==='.'||part==='..');
|
|
4
|
+
|
|
5
|
+
export async function readTargetPreparationEvidence({state,targetIntent,destination,observe=observeCooperativeTarget,readChanges=readCooperativeChanges,gitRunner}={}){
|
|
6
|
+
const sourceTarget=await observe(state.target.realpath);
|
|
7
|
+
const snapshot=await readChanges({state:{...state,target:sourceTarget},...(gitRunner?{gitRunner}:{})});
|
|
8
|
+
const stage=state.plannerLedger?.stages?.find(s=>s.id===targetIntent?.firstStageId);
|
|
9
|
+
const source={target:sourceTarget,fingerprint:snapshot.final,snapshot:snapshot.transitionSnapshot,preserved:!!targetIntent?.sourceSnapshot&&same(snapshot.transitionSnapshot,targetIntent.sourceSnapshot),inputVersion:stage?.inputVersion,candidate:targetIntent?.candidate,base:targetIntent?.base};
|
|
10
|
+
if(!destination)return {source};
|
|
11
|
+
const target=await observe(destination.realpath),tasks=state.tasks.map(task=>({...task,paths:task.paths.map(p=>{if(!within(state.target.realpath,p))throw Error('Task path escapes source');return path.join(target.realpath,path.relative(state.target.realpath,p));})})),destinationSnapshot=await readChanges({state:{...state,target,tasks},...(gitRunner?{gitRunner}:{})}),result={target,snapshot:destinationSnapshot.transitionSnapshot};
|
|
12
|
+
source.preserved=source.preserved&&same(destinationSnapshot.transitionSnapshot,targetIntent.sourceSnapshot);
|
|
13
|
+
if(target.realpath!==state.target.realpath){
|
|
14
|
+
const query=await (gitRunner??defaultRunner)({cwd:sourceTarget.realpath,args:['worktree','list','--porcelain','-z']});
|
|
15
|
+
if(query?.status!==0||query.error||!Buffer.isBuffer(query.stdout))throw Error('Worktree registration query failed');
|
|
16
|
+
const raw=new TextDecoder('utf-8',{fatal:true}).decode(query.stdout);if(raw&&!raw.endsWith('\0\0'))throw Error('Incomplete worktree registration');
|
|
17
|
+
const entries=raw?raw.slice(0,-2).split('\0\0').map(entry=>entry.split('\0')):[];
|
|
18
|
+
result.registered=entries.some(entry=>entry.includes('worktree '+target.realpath)&&entry.includes('HEAD '+target.head)&&entry.includes('branch refs/heads/'+target.branch));
|
|
19
|
+
const attachment=state.records?.find(r=>r.kind==='note'&&r.messageRef===destination.attachmentRef&&r.targetVersion===state.targetVersion&&r.source==='operator-attested'&&r.data?.type==='worktree-attachment'&&r.data.realpath===target.realpath&&r.data.gitDir===target.gitDir&&r.data.gitCommonDir===target.gitCommonDir);
|
|
20
|
+
if(attachment)result.attachmentRef=attachment.messageRef;
|
|
21
|
+
}
|
|
22
|
+
if(destination.path!==undefined){
|
|
23
|
+
if(!relative(destination.path))throw Error('Invalid destination path');
|
|
24
|
+
const filename=path.join(target.realpath,...destination.path.split('/'));
|
|
25
|
+
if(!within(target.realpath,filename))throw Error('Destination path escapes workspace');
|
|
26
|
+
await inspectPath(filename);const handle=await fs.open(filename,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
27
|
+
let bytes,after;
|
|
28
|
+
try{const before=await handle.stat();if(!before.isFile())throw Error('Destination must be a regular file');bytes=await handle.readFile();after=await handle.stat();const named=await fs.lstat(filename);if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.mode!==after.mode||after.dev!==named.dev||after.ino!==named.ino)throw Error('Destination changed while reading');}finally{await handle.close();}
|
|
29
|
+
await inspectPath(filename);if(!within(target.realpath,await fs.realpath(filename)))throw Error('Destination path escapes workspace');
|
|
30
|
+
result.document={path:destination.path,hash:'sha256:'+createHash('sha256').update(bytes).digest('hex'),mode:0o100000|(after.mode&0o777),verification:{status:'verified',source:'local-bytes'}};
|
|
31
|
+
}
|
|
32
|
+
return {source,destination:result};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateTargetPreparationRecord(kind,data){
|
|
36
|
+
if(!data||data.type!==kind||!['intent','checkpoint','transition','destination'].includes(kind))throw Error('Invalid target preparation type');
|
|
37
|
+
if(kind==='intent'&&(!text(data.id)||!text(data.workflowId)||!data.sourceTarget||!Number.isSafeInteger(data.sourceTargetVersion)||!text(data.selectionRef)||!['current','branch','worktree'].includes(data.kind)||!text(data.candidate)||!text(data.base)||!text(data.firstStageId)||!Array.isArray(data.documents)||!data.documents.length||data.documents.some(d=>!['spec','plan'].includes(d.kind)||!relative(d.path))||new Set(data.documents.map(d=>d.kind)).size!==data.documents.length))throw Error('Invalid target preparation intent');
|
|
38
|
+
if(kind==='intent'&&(!Number.isSafeInteger(data.sourceTargetVersion)||data.sourceTargetVersion<1||!['realpath','gitCommonDir','gitDir'].every(k=>typeof data.sourceTarget[k]==='string'&&path.isAbsolute(data.sourceTarget[k])&&path.normalize(data.sourceTarget[k])===data.sourceTarget[k])||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(data.sourceTarget.head)||!(data.sourceTarget.branch===null||text(data.sourceTarget.branch))||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(data.base)||new Set(data.documents.map(d=>d.path)).size!==data.documents.length))throw Error('Invalid target preparation intent identity');
|
|
39
|
+
if(kind==='checkpoint'&&(!text(data.intentId)||!text(data.stageId)||!text(data.resultRef)||!text(data.selfReviewRef)))throw Error('Invalid target preparation checkpoint');
|
|
40
|
+
if(kind==='transition'&&(!text(data.intentId)||!text(data.checkpointRef)||!Number.isSafeInteger(data.expectedRevision)||data.expectedRevision<0))throw Error('Invalid target preparation transition');
|
|
41
|
+
if(kind==='destination'&&(!text(data.intentId)||!text(data.transitionRef)||!text(data.stageId)||!text(data.resultRef)||!text(data.selfReviewRef)||!relative(data.path)||!/^sha256:[a-f0-9]{64}$/.test(data.hash)||![0o100644,0o100755].includes(data.mode)||!text(data.reviewRef)||!text(data.approvalRef)||!Number.isSafeInteger(data.targetVersion)||data.targetVersion<1))throw Error('Invalid target preparation destination');
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function evaluateTargetPreparation({state,request,evidence}={}){
|
|
46
|
+
const hold=reason=>({decision:'hold',ruleIds:['cooperative.target-preparation'],reason});
|
|
47
|
+
try{validateTargetPreparationRecord(request?.type,request);}catch{return hold('Invalid target preparation record');}
|
|
48
|
+
if(request.type==='intent'){
|
|
49
|
+
if(state.targetIntents?.some(intent=>!state.targetTransitions?.some(transition=>transition.intentId===intent.id)))return hold('Existing target preparation intent is still active');
|
|
50
|
+
const approval=state.records?.find(r=>r.kind==='approval'&&r.messageRef===request.approvalRef&&r.source==='operator-attested'&&r.actor?.role==='orchestrator'&&r.targetVersion===state.targetVersion&&r.scopeVersion===state.scopeVersion);
|
|
51
|
+
const approved=approval?.data?.targetPreparation;
|
|
52
|
+
if(!['Bounded','Architectural'].includes(request.depth)||(request.depth==='Architectural'&&(!request.documents.some(d=>d.kind==='spec')||!request.documents.some(d=>d.kind==='plan'))))return hold('Approved depth requires its complete document set');
|
|
53
|
+
if(!text(request.approvalRef)||!text(request.depth)||['depth','selectionRef','kind','candidate','base','workflowId','firstStageId','sourceTargetVersion'].some(k=>approved?.[k]!==request[k])||!same(approved?.sourceTarget,request.sourceTarget)||!same(approved?.documents,request.documents)||request.workflowId!==state.workflowId||request.sourceTargetVersion!==state.targetVersion||!same(request.sourceTarget,state.target)||!same(evidence?.source?.target,state.target)||!text(evidence?.source?.fingerprint)||!state.selections?.some(s=>s.messageRef===request.selectionRef&&s.targetVersion===state.targetVersion)||state.targetIntents?.some(i=>i.id===request.id)||request.base!==state.target.head)return hold('Current approval, document set, source, explicit choice and preservation snapshot required');
|
|
54
|
+
return {decision:'allow',ruleIds:['cooperative.target-preparation'],proposedTransition:{intent:{...structuredClone(request),scopeVersion:state.scopeVersion,sourceFingerprint:evidence.source.fingerprint,...(evidence.source.snapshot?{sourceSnapshot:structuredClone(evidence.source.snapshot)}:{}),source:'operator-attested'}}};
|
|
55
|
+
}
|
|
56
|
+
if(request.type==='transition'){
|
|
57
|
+
const intent=state?.targetIntents?.find(i=>i.id===request.intentId),checkpoint=state?.targetCheckpoints?.find(c=>c.intentId===request.intentId&&c.messageRef===request.checkpointRef);
|
|
58
|
+
const source=evidence?.source,destination=evidence?.destination?.target;
|
|
59
|
+
if(state.plannerLedger?.stages?.some(s=>s.pending||s.calls?.some(c=>['reserved','unknown'].includes(c.status))))return hold('Pending or unknown Planner execution');
|
|
60
|
+
const stage=state.plannerLedger?.stages?.find(s=>s.id===intent?.firstStageId),result=stage?.result,chat=result?.chatDraft;
|
|
61
|
+
if(!intent||intent.scopeVersion!==state.scopeVersion||stage?.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion||!state.records?.some(r=>r.kind==='approval'&&r.messageRef===intent.approvalRef&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&r.actor?.role==='orchestrator'&&r.source==='operator-attested'))return hold('Current scope approval and source stage required');
|
|
62
|
+
if(!stage||stage.targetIntentId!==intent.id||checkpoint?.stageId!==stage.id||checkpoint.resultRef!==result?.ref||checkpoint.selfReviewRef!==chat?.selfReviewRef||checkpoint.candidate!==intent.candidate||checkpoint.base!==intent.base||checkpoint.inputVersion!==stage.inputVersion||result.status!=='succeeded'||chat?.intentId!==intent.id||chat.complete!==true||!text(chat.draftRef)||!stage.calls?.some(c=>c.requestId===result.requestId&&c.status==='succeeded'))return hold('Checkpoint source result changed');
|
|
63
|
+
if(!intent||!checkpoint||state.targetTransitions?.some(t=>t.intentId===intent.id)||request.expectedRevision!==state.revision||intent.sourceTargetVersion!==state.targetVersion||!same(intent.sourceTarget,state.target)||!same(evidence?.versions,state.versions)||!source?.preserved||!destination||source.target?.realpath!==state.target.realpath||source.target?.gitCommonDir!==state.target.gitCommonDir||source.target?.gitDir!==state.target.gitDir||destination.gitCommonDir!==state.target.gitCommonDir||state.unresolved?.some(u=>!u.resolvedBy)||state.assignments?.some(a=>a.status==='active')||state.intents?.some(i=>['prepared','unknown','mismatch'].includes(i.status)))return hold('Current checkpoint, preservation and independent target evidence required');
|
|
64
|
+
if(intent.kind==='branch'&&!(destination.realpath===state.target.realpath&&destination.gitDir===state.target.gitDir&&destination.branch===intent.candidate&&destination.head===intent.base&&source.target.branch===intent.candidate&&source.target.head===intent.base))return hold('Branch target differs from intent');
|
|
65
|
+
if(intent.kind==='current'&&(!same(destination,state.target)||destination.branch!==intent.candidate||destination.head!==intent.base))return hold('Current target changed');
|
|
66
|
+
if(intent.kind==='worktree'&&state.tasks?.some(task=>task.paths?.length))return hold('Worktree transition with source-bound task paths is unsupported');
|
|
67
|
+
if(intent.kind==='worktree'&&!(same(source.target,state.target)&&destination.realpath!==state.target.realpath&&destination.gitDir!==state.target.gitDir&&destination.branch===intent.candidate&&destination.head===intent.base&&evidence.destination.registered===true&&text(evidence.destination.attachmentRef)&&state.records?.some(r=>r.kind==='note'&&r.messageRef===evidence.destination.attachmentRef&&r.targetVersion===state.targetVersion&&r.source==='operator-attested'&&r.data?.type==='worktree-attachment'&&r.data.realpath===destination.realpath&&r.data.gitDir===destination.gitDir&&r.data.gitCommonDir===destination.gitCommonDir)))return hold('Worktree registration and attachment required');
|
|
68
|
+
return {decision:'allow',ruleIds:['cooperative.target-preparation'],proposedTransition:{target:structuredClone(destination),intentId:intent.id,checkpointRef:request.checkpointRef,sourceTargetVersion:state.targetVersion,expectedRevision:state.revision,validation:{request:structuredClone(request),evidence:structuredClone(evidence)}}};
|
|
69
|
+
}
|
|
70
|
+
if(request.type==='destination'){
|
|
71
|
+
const intent=state.targetIntents?.find(i=>i.id===request.intentId),document=intent?.documents?.find(d=>d.path===request.path),transition=state.targetTransitions?.find(t=>t.intentId===request.intentId&&t.messageRef===request.transitionRef&&t.targetVersion===state.targetVersion),checkpoint=state.targetCheckpoints?.find(c=>c.intentId===request.intentId&&c.messageRef===transition?.checkpointRef),stage=state.plannerLedger?.stages?.find(s=>s.id===request.stageId),chat=stage?.result?.chatDraft,observed=evidence?.destination?.document;
|
|
72
|
+
const review=state.records?.find(r=>r.kind==='planner-review'&&r.messageRef===request.reviewRef&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&r.source==='operator-attested'),approval=state.records?.find(r=>r.kind==='approval'&&r.messageRef===request.approvalRef&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&r.source==='operator-attested');
|
|
73
|
+
if(!intent||!document||intent.firstStageId!==request.stageId||!transition||!same(transition.target,state.target)||request.targetVersion!==state.targetVersion||!checkpoint||checkpoint.stageId!==request.stageId||checkpoint.resultRef!==request.resultRef||checkpoint.selfReviewRef!==request.selfReviewRef||stage?.targetIntentId!==intent.id||stage.result?.ref!==request.resultRef||stage.result.status!=='succeeded'||chat?.intentId!==intent.id||chat.complete!==true||chat.selfReviewRef!==request.selfReviewRef||!same(evidence?.destination?.target,state.target)||observed?.verification?.status!=='verified'||observed.verification.source!=='local-bytes'||observed.path!==request.path||observed.hash!==request.hash||observed.mode!==request.mode)return hold('Current transition, source attestation and observed destination bytes required');
|
|
74
|
+
if(stage.scopeVersion!==state.scopeVersion||review?.data?.resultRef!==request.resultRef||review.data.full!==true||review.data.complete!==true||review.data.hash!==request.hash||!Array.isArray(stage.criteria)||!Array.isArray(review.data.criteria)||review.data.criteria.length!==stage.criteria.length||new Set(review.data.criteria.map(c=>c.id)).size!==stage.criteria.length||review.data.criteria.some(c=>!stage.criteria.some(s=>s.id===c.id)||c.status!=='resolved'||!text(c.judgmentRef))||approval?.data?.kind!==document.kind||approval.data.contentHash!==request.hash||![request.path,path.join(state.target.realpath,request.path)].includes(approval.data.path))return hold('Current full review and document approval required');
|
|
75
|
+
return {decision:'allow',ruleIds:['cooperative.target-preparation'],proposedTransition:{destination:{...structuredClone(request),scopeVersion:state.scopeVersion,sourceLink:'operator-attested',verification:{status:'verified',source:'local-bytes'}}}};
|
|
76
|
+
}
|
|
77
|
+
if(request?.type!=='checkpoint')return hold('Unsupported target preparation request');
|
|
78
|
+
const intent=state?.targetIntents?.find(i=>i.id===request.intentId);
|
|
79
|
+
const stage=state?.plannerLedger?.stages?.find(s=>s.id===request.stageId);
|
|
80
|
+
const source=evidence?.source,result=stage?.result,chat=result?.chatDraft;
|
|
81
|
+
if(!intent||intent.scopeVersion!==state.scopeVersion||stage?.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion||!state.records?.some(r=>r.kind==='approval'&&r.messageRef===intent.approvalRef&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&r.actor?.role==='orchestrator'&&r.source==='operator-attested'))return hold('Current scope approval and source stage required');
|
|
82
|
+
if(!intent||!stage||intent.firstStageId!==stage.id||stage.targetIntentId!==intent.id||intent.sourceTargetVersion!==state.targetVersion||!same(intent.sourceTarget,state.target)||!same(source?.target,state.target)||source.preserved!==true||source.inputVersion!==stage.inputVersion||source.candidate!==intent.candidate||source.base!==intent.base)return hold('Current source and intent evidence required');
|
|
83
|
+
if(stage.pending||result?.ref!==request.resultRef||result?.status!=='succeeded'||!stage.calls?.some(c=>c.requestId===result.requestId&&c.status==='succeeded')||stage.calls.some(c=>['reserved','unknown'].includes(c.status))||chat?.intentId!==intent.id||!chat.draftRef||!chat.selfReviewRef||chat.selfReviewRef!==request.selfReviewRef||chat.complete!==true)return hold('Complete confirmed chat draft and self-review required');
|
|
84
|
+
return {decision:'allow',ruleIds:['cooperative.target-preparation'],reason:'Chat checkpoint attested',proposedTransition:{checkpoint:{intentId:intent.id,stageId:stage.id,resultRef:result.ref,selfReviewRef:chat.selfReviewRef,candidate:intent.candidate,base:intent.base,inputVersion:stage.inputVersion}}};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function applyTargetPreparationTransition({state,decision}={}){
|
|
88
|
+
const next=structuredClone(state),transition=decision?.proposedTransition;
|
|
89
|
+
if(decision?.decision!=='allow'||!transition?.target||transition.sourceTargetVersion!==state.targetVersion||transition.expectedRevision!==state.revision||state.targetTransitions?.some(t=>t.intentId===transition.intentId))return next;
|
|
90
|
+
if(!transition.validation)return next;
|
|
91
|
+
const checked=evaluateTargetPreparation({state,...transition.validation});
|
|
92
|
+
if(checked.decision!=='allow'||!same(checked.proposedTransition,transition))return next;
|
|
93
|
+
const intent=state.targetIntents?.find(i=>i.id===transition.intentId),checkpoint=state.targetCheckpoints?.find(c=>c.intentId===transition.intentId&&c.messageRef===transition.checkpointRef);
|
|
94
|
+
if(!intent||!checkpoint||intent.sourceTargetVersion!==state.targetVersion||!same(intent.sourceTarget,state.target))return next;
|
|
95
|
+
next.target=structuredClone(transition.target);next.targetVersion++;
|
|
96
|
+
(next.targetTransitions??=[]).push({...structuredClone(transition),targetVersion:next.targetVersion});
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
import fs from 'node:fs/promises';
|
|
100
|
+
import {constants} from 'node:fs';
|
|
101
|
+
import path from 'node:path';
|
|
102
|
+
import {createHash} from 'node:crypto';
|
|
103
|
+
import {inspectPath,within,observeCooperativeTarget} from './scope.mjs';
|
|
104
|
+
import {readCooperativeChanges} from './verification.mjs';
|
|
105
|
+
import {spawnSync} from 'node:child_process';
|
|
106
|
+
function defaultRunner({cwd,args}){
|
|
107
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
|
|
108
|
+
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'});
|
|
109
|
+
return spawnSync('git',['--no-pager','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'buffer',timeout:10000,maxBuffer:1024*1024});
|
|
110
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {createHash} from 'node:crypto';
|
|
4
5
|
import {spawnSync} from 'node:child_process';
|
|
@@ -108,16 +109,17 @@ export async function readCooperativeChanges({state,gitRunner=defaultRunner}){
|
|
|
108
109
|
if(raw&&!raw.endsWith('\0'))throw Error('Incomplete verification inventory');
|
|
109
110
|
const index=await gitRunner({cwd:state.target.realpath,args:['ls-files','--stage','-z']});
|
|
110
111
|
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
|
+
const indexText=new TextDecoder('utf-8',{fatal:true}).decode(index.stdout),indexRows=[],transitionIndex=[];
|
|
112
113
|
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
|
+
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||!match[3]||/[\\\x00\r\n:]/.test(match[3])||match[3].startsWith('/')||match[3].split('/').some(p=>!p||p==='.'||p==='..'))throw Error('Verification requires regular stage-0 index entries');indexRows.push({path:path.join(state.target.realpath,match[3]),entry});transitionIndex.push({path:match[3],mode:match[1],blob:match[2],stage:0});}
|
|
114
115
|
const names=[...new Set(raw?raw.slice(0,-1).split('\0'):[])].sort(),rows=[];
|
|
115
116
|
for(const name of names){
|
|
116
117
|
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
118
|
const file=path.join(state.target.realpath,...name.split('/'));
|
|
118
|
-
try{await inspectPath(file);const
|
|
119
|
+
try{await inspectPath(file);const handle=await fs.open(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);let stat,bytes;try{stat=await handle.stat();if(!stat.isFile())throw Error('Verification requires regular files');bytes=await handle.readFile();const after=await handle.stat(),named=await fs.lstat(file);if(stat.dev!==after.dev||stat.ino!==after.ino||stat.size!==after.size||stat.mtimeMs!==after.mtimeMs||stat.mode!==after.mode||after.dev!==named.dev||after.ino!==named.ino)throw Error('Verification file changed while reading');}finally{await handle.close();}await inspectPath(file);rows.push({path:file,mode:stat.mode&0o777,hash:'sha256:'+createHash('sha256').update(bytes).digest('hex')});}
|
|
119
120
|
catch(error){if(error.code!=='ENOENT')throw error;rows.push({path:file,deleted:true});}
|
|
120
121
|
}
|
|
121
122
|
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
|
-
|
|
123
|
+
const transitionSnapshot={index:transitionIndex.sort((a,b)=>a.path.localeCompare(b.path)),files:rows.map(row=>({...row,path:path.relative(state.target.realpath,row.path).split(path.sep).join('/')}))};
|
|
124
|
+
return {tasks,taskSnapshots,final:fingerprintRequest({scope:'final',target:state.target,index:indexRows,rows}),transitionSnapshot};
|
|
123
125
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
import tomllib
|
|
@@ -77,10 +78,14 @@ PRESETS = ("economy", "balanced", "smart")
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def validate_config(config: dict, path: Path) -> dict:
|
|
80
|
-
if not isinstance(config, dict) or
|
|
81
|
-
raise _error(path, "settings", "update to version 1 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if not isinstance(config, dict) or type(config.get("version")) is not int or config["version"] not in (1, 2):
|
|
82
|
+
raise _error(path, "settings", "update to version 1 or 2 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
83
|
+
has_custom = "custom" in config
|
|
84
|
+
allowed = {"version", "preset", "roles"} | ({"custom"} if config["version"] == 2 and has_custom else set())
|
|
85
|
+
if set(config) != allowed:
|
|
86
|
+
raise _error(path, "settings", "update to version 1 or 2 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
87
|
+
if not isinstance(config["preset"], str) or config["preset"] not in (*PRESETS, "custom") or (config["preset"] == "custom" and not has_custom):
|
|
88
|
+
raise _error(path, "preset", "must select an available preset")
|
|
84
89
|
roles = config["roles"]
|
|
85
90
|
if not isinstance(roles, dict) or set(roles) != set(ROLE_FILES):
|
|
86
91
|
raise _error(path, "roles", "must define exactly the five ARC roles")
|
|
@@ -89,24 +94,38 @@ def validate_config(config: dict, path: Path) -> dict:
|
|
|
89
94
|
raise _error(path, f"roles.{role}", "must contain exactly economy, balanced and smart")
|
|
90
95
|
for preset, values in variants.items():
|
|
91
96
|
_settings_for_profile(values, f"{role}.{preset}", path)
|
|
92
|
-
|
|
97
|
+
if has_custom:
|
|
98
|
+
custom = config["custom"]
|
|
99
|
+
if not isinstance(custom, dict) or set(custom) != set(ROLE_FILES):
|
|
100
|
+
raise _error(path, "custom", "must define exactly the five ARC roles")
|
|
101
|
+
for role, values in custom.items():
|
|
102
|
+
_settings_for_profile(values, f"custom.{role}", path)
|
|
103
|
+
result = copy.deepcopy(config)
|
|
104
|
+
result["version"] = 2
|
|
105
|
+
return result
|
|
93
106
|
|
|
94
107
|
|
|
95
108
|
def load_settings(config_path: Path) -> dict[str, RoleSettings]:
|
|
96
109
|
"""Validate every preset and return the active role settings."""
|
|
97
110
|
_, _, config = _parse_file(config_path, "settings")
|
|
98
111
|
config = validate_config(config, config_path)
|
|
112
|
+
if config["preset"] == "custom":
|
|
113
|
+
return config["custom"]
|
|
99
114
|
return {role: dict(variants[config["preset"]]) for role, variants in config["roles"].items()}
|
|
100
115
|
|
|
101
116
|
|
|
102
117
|
def serialize_settings(config: dict) -> bytes:
|
|
103
118
|
"""Serialize a complete unified configuration without losing inactive presets."""
|
|
104
119
|
config = validate_config(config, Path("models.toml"))
|
|
105
|
-
lines = ["version =
|
|
120
|
+
lines = ["version = 2", f'preset = {json.dumps(config["preset"])}']
|
|
106
121
|
for role in ROLE_FILES:
|
|
107
122
|
for preset in PRESETS:
|
|
108
123
|
lines.extend(["", f"[roles.{role}.{preset}]"])
|
|
109
124
|
lines.extend(f"{key} = {json.dumps(config['roles'][role][preset][key], ensure_ascii=False)}" for key in _MODEL_FIELDS)
|
|
125
|
+
if "custom" in config:
|
|
126
|
+
for role in ROLE_FILES:
|
|
127
|
+
lines.extend(["", f"[custom.{role}]"])
|
|
128
|
+
lines.extend(f"{key} = {json.dumps(config['custom'][role][key], ensure_ascii=False)}" for key in _MODEL_FIELDS)
|
|
110
129
|
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
111
130
|
|
|
112
131
|
|
|
@@ -39,5 +39,5 @@ export function renderWaitSettings(original){
|
|
|
39
39
|
return{content:Buffer.from(updated),action:'set ARC wait settings'};
|
|
40
40
|
}
|
|
41
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
|
|
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 and brainstorming drafts and first complete spec and plan drafts are authored only by the Planner; route drafting skills to the Planner. Orchestrator reviews and directly revises the same spec or plan when needed; a missing or failed first draft does not transfer initial authorship. New product decisions outside approved scope require user input first.\n'+
|
|
43
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';}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {validateAgentTemplates} from '../prompts/render.mjs';
|
|
1
2
|
import {COOPERATIVE_RUNTIME_MODULES} from '../runtime/bootstrap.mjs';
|
|
2
3
|
import {RUNTIME_MODULE_PATHS} from './runtime-assets.mjs';
|
|
3
4
|
import {POLICY_TARGETS,buildPolicyOutputs} from '../policy/render.mjs';
|
|
@@ -40,18 +41,21 @@ export function verifyPayload({packageRoot,safeFs,payloadMode='auto'}){
|
|
|
40
41
|
for(const platform of ['codex','claude'])referenceTrees[platform]=validateFlatReferenceFiles(files,platform);
|
|
41
42
|
for(const{destination}of REFERENCE_PATH_MAP)if(!referenceTrees.codex.entries.get(destination).bytes.equals(referenceTrees.claude.entries.get(destination).bytes))throw Error('Imported platform parity mismatch: '+destination);
|
|
42
43
|
for(const name of listed.keys())if(!files.has(name))throw Error(`Manifest entry is missing: ${name}`);
|
|
43
|
-
const required=['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/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'];
|
|
44
|
+
const required=['lib/prompts/agent-fragments.json','lib/prompts/render.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/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'];
|
|
44
45
|
required.push('lib/installer/runtime-assets.mjs',...RUNTIME_MODULE_PATHS);
|
|
45
46
|
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)}`));
|
|
46
47
|
for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
|
|
47
48
|
for(const platform of ['codex','claude'])for(const name of [...COOPERATIVE_RUNTIME_MODULES,'role-policy.json'])required.push(`${platform}/skill/runtime/${name}`);
|
|
48
49
|
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};}
|
|
49
50
|
for(const name of required)if(!files.has(name))throw Error(`Required runtime asset is missing: ${name}`);
|
|
51
|
+
const agentRegistry=JSON.parse(files.get('lib/prompts/agent-fragments.json').toString('utf8'));
|
|
52
|
+
const agentTemplates=new Map();for(const platform of ['codex','claude'])for(const [name,bytes] of modelInputs[platform].profiles)agentTemplates.set(`${platform}/agents/${name}`,bytes);
|
|
53
|
+
validateAgentTemplates(agentTemplates,agentRegistry);
|
|
50
54
|
const policyInputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,files.get(name)]));
|
|
51
55
|
const policyOutputs=buildPolicyOutputs(policyInputs,JSON.parse(files.get('lib/policy/role-policy.json').toString('utf8')));
|
|
52
56
|
for(const [name,bytes] of policyOutputs)if(!bytes.equals(files.get(name)))throw Error(`Stale role policy output: ${name}`);
|
|
53
57
|
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');
|
|
54
58
|
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)]))};}
|
|
55
|
-
return{packageRoot,files,modelInputs,snapshot,referenceTrees};
|
|
59
|
+
return{packageRoot,files,modelInputs,agentRegistry,snapshot,referenceTrees};
|
|
56
60
|
}catch(cause){throw new PayloadValidationError(`Payload verification failed: ${cause.message}`,{cause});}finally{root?.close();}
|
|
57
61
|
}
|
package/lib/installer/plan.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {renderAgentProfile} from '../prompts/render.mjs';
|
|
2
|
+
import {PayloadValidationError} from './manifest.mjs';
|
|
1
3
|
import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
|
|
2
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';
|
|
3
5
|
import{validateModelConfig}from'../models/config.mjs';
|
|
4
|
-
import
|
|
6
|
+
import{assertCatalogRoles}from'../models/catalog.mjs';
|
|
7
|
+
import path from'node:path';import{loadModelConfig,applyPreset,applyCustomRoles,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
|
|
5
8
|
function fail(message){throw new TargetConfigurationError(message);}
|
|
6
9
|
export function previewLimits({target,safeFs}){
|
|
7
10
|
const snapshot=new Map(),filename=path.join(target.home,'skills/arc/limits.toml');
|
|
@@ -15,9 +18,9 @@ export function previewLanguage({target,safeFs}){
|
|
|
15
18
|
let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
|
|
16
19
|
return{existing,snapshot};
|
|
17
20
|
}
|
|
18
|
-
function renderModelAssets({payload,target,preset,modelConfig,language}){
|
|
21
|
+
function renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog}){
|
|
19
22
|
const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
|
|
20
|
-
|
|
23
|
+
let config;if(modelConfig!==undefined)config=validateModelConfig(modelConfig);else{const base=loadModelConfig(input.models,target.platform);if(preset==='custom'){assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});config=applyCustomRoles(base,customRoles);}else {if(customRoles!==undefined)assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});config=applyPreset(customRoles!==undefined?applyCustomRoles(base,customRoles):preservedCustomRoles!==undefined?applyCustomRoles(base,preservedCustomRoles):base,preset);}}if(config.platform!==target.platform)fail('Model config platform does not match target');const settings=resolveModelSettings(config),profiles=renderProfiles(input.profiles,settings);try{for(const[name,bytes]of profiles){const expanded=renderAgentProfile(`${target.platform}/agents/${name}`,bytes,payload.agentRegistry);const final=renderLanguagePolicy(expanded,language);if(final.toString('utf8').includes('ARC_AGENT_FRAGMENT'))throw Error('Unresolved fragment marker');profiles.set(name,final);}const checked=renderProfiles(profiles,settings);if(checked.size!==profiles.size||[...profiles].some(([name,bytes],i)=>{const entry=[...checked][i];return !entry||entry[0]!==name||!entry[1].equals(bytes);}))throw Error('Final profile model settings changed');}catch(cause){throw new PayloadValidationError(`Payload profile rendering failed: ${cause.message}`,{cause});}const modelBytes=Buffer.from(serializeModelConfig(config));
|
|
21
24
|
return{profiles,modelBytes};
|
|
22
25
|
}
|
|
23
26
|
function renderSkillPayload({payload,target,language,modelBytes,limits}){
|
|
@@ -45,14 +48,14 @@ function renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurre
|
|
|
45
48
|
}
|
|
46
49
|
return globalChanges;
|
|
47
50
|
}
|
|
48
|
-
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
|
|
51
|
+
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig,customRoles,preservedCustomRoles,verifiedCatalog}){
|
|
49
52
|
validateLanguage(language);
|
|
50
53
|
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
51
54
|
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
52
55
|
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
53
56
|
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
54
57
|
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
55
|
-
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language});
|
|
58
|
+
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog});
|
|
56
59
|
const skill=renderSkillPayload({payload,target,language,modelBytes,limits});
|
|
57
60
|
const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
|
|
58
61
|
for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
|
package/lib/installer/preset.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
3
|
-
import {loadModelConfig,applyPreset,serializeModelConfig,resolveModelSettings,renderProfiles} from '../models/index.mjs';
|
|
3
|
+
import {loadModelConfig,applyPreset,applyCustomRoles,serializeModelConfig,resolveModelSettings,renderProfiles} from '../models/index.mjs';
|
|
4
4
|
import {assertPlanSnapshot,applyTransaction} from './transaction.mjs';
|
|
5
|
+
import {assertCatalogRoles} from '../models/catalog.mjs';
|
|
5
6
|
import {validatePlanSet} from './plan.mjs';
|
|
6
|
-
export function buildPresetPlan({target,preset,safeFs}){
|
|
7
|
+
export function buildPresetPlan({target,preset,safeFs,customRoles,verifiedCatalog}){
|
|
7
8
|
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
9
|
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)
|
|
10
|
+
const originalModels=read(modelsPath),config=loadModelConfig(originalModels,target.platform);
|
|
11
|
+
if(preset==='custom'||customRoles!==undefined)assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});
|
|
12
|
+
const next=customRoles!==undefined?applyPreset(applyCustomRoles(config,customRoles),preset):applyPreset(config,preset),originalProfiles=new Map(target.profileNames.map((name,i)=>[name,read(managedPaths[i+1])]));
|
|
10
13
|
const profiles=renderProfiles(originalProfiles,resolveModelSettings(next)),specs=[];
|
|
11
|
-
if(config.preset!==preset)specs.push({path:modelsPath,bytes:Buffer.from(serializeModelConfig(next))});
|
|
14
|
+
if(config.preset!==preset||(customRoles!==undefined&&(!config.custom||Object.keys(next.custom).some(role=>Object.keys(next.custom[role]).some(key=>config.custom[role]?.[key]!==next.custom[role][key])))))specs.push({path:modelsPath,bytes:Buffer.from(serializeModelConfig(next))});
|
|
12
15
|
for(const[name,bytes]of profiles)if(!bytes.equals(originalProfiles.get(name)))specs.push({path:path.join(target.home,'agents',name),bytes});
|
|
13
16
|
return{target,preset,snapshot,managedPaths,externalPaths:[],specs};
|
|
14
17
|
}
|
|
@@ -21,7 +21,7 @@ export async function prepareOperation(context,options,{selections=[]}={}){
|
|
|
21
21
|
if(!['install','preset','uninstall'].includes(options.kind))throw Object.assign(Error('Unknown management operation'),{code:'ARC_CLI_INPUT'});
|
|
22
22
|
if(options.kind==='uninstall')options={...options,removeCli:options.removeCli!==false};
|
|
23
23
|
const platforms=options.target==='all'?['codex','claude']:[options.target],payload=options.kind==='install'?verifyPayload(context):null;
|
|
24
|
-
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal,removeCompletion:options.removeCli!==false});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
24
|
+
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs,customRoles:options.customRoles,verifiedCatalog:options.verifiedCatalog});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal,removeCompletion:options.removeCli!==false});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,customRoles:options.customRoles,preservedCustomRoles:options.preservedCustomRoles,verifiedCatalog:options.verifiedCatalog,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
25
25
|
if(options.kind==='install')attachShellCompletionCleanup(plans,context);
|
|
26
26
|
// Uninstall plans initially share the completion snapshot; its executor assigns one owner.
|
|
27
27
|
if(options.kind!=='uninstall')validatePlanSet(plans);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const API = 'https://api.anthropic.com/v1/models';
|
|
2
|
+
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
3
|
+
const unavailable = (code, message) => ({ status: 'unavailable', code, message });
|
|
4
|
+
|
|
5
|
+
export async function discoverAnthropicCatalog({ apiKey, fetchImpl = globalThis.fetch, signal } = {}) {
|
|
6
|
+
if (typeof apiKey !== 'string' || !apiKey.trim() || /[\r\n]/.test(apiKey)) return unavailable('missing_api_key', 'Anthropic API key is unavailable.');
|
|
7
|
+
if (typeof fetchImpl !== 'function') return unavailable('unavailable', 'Anthropic catalog is unavailable.');
|
|
8
|
+
const pairs = [], unavailableModels = [], seenPairs = new Set(), seenModels = new Set(), supportedModels = new Set(), cursors = new Set();
|
|
9
|
+
let cursor;
|
|
10
|
+
try {
|
|
11
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber++) {
|
|
12
|
+
if (signal?.aborted) return unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
13
|
+
const url = new URL(API);
|
|
14
|
+
if (cursor) url.searchParams.set('after_id', cursor);
|
|
15
|
+
const response = await fetchImpl(url, {
|
|
16
|
+
method: 'GET', redirect: 'manual', signal,
|
|
17
|
+
headers: { 'anthropic-version': '2026-10-21', Authorization: `Bearer ${apiKey}` },
|
|
18
|
+
});
|
|
19
|
+
if (!response?.ok) return unavailable('http_error', 'Anthropic catalog is unavailable.');
|
|
20
|
+
if (response.url) {
|
|
21
|
+
const actual = new URL(response.url);
|
|
22
|
+
if (actual.origin !== url.origin || actual.pathname !== url.pathname) return unavailable('http_error', 'Anthropic catalog is unavailable.');
|
|
23
|
+
}
|
|
24
|
+
const page = await response.json();
|
|
25
|
+
if (!page || !Array.isArray(page.data) || !page.data.length || typeof page.has_more !== 'boolean') throw Error('invalid page');
|
|
26
|
+
for (const model of page.data) {
|
|
27
|
+
if (!model || typeof model.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/@+\-]*$/.test(model.id) || model.id.includes(apiKey)) throw Error('invalid model');
|
|
28
|
+
const capability = model.capabilities?.effort;
|
|
29
|
+
const supported = capability?.supported === true ? EFFORTS.filter(effort => capability[effort]?.supported === true) : [];
|
|
30
|
+
if (!supported.length && !seenModels.has(model.id)) unavailableModels.push({ model: model.id, reason: 'Effort capability is unavailable.' });
|
|
31
|
+
seenModels.add(model.id);
|
|
32
|
+
for (const effort of supported) {
|
|
33
|
+
supportedModels.add(model.id);
|
|
34
|
+
const key = JSON.stringify([model.id, effort]);
|
|
35
|
+
if (!seenPairs.has(key)) { seenPairs.add(key); pairs.push({ model: model.id, effort }); }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!page.has_more) return { status: 'ready', catalog: { platform: 'claude', pairs, unavailable: unavailableModels.filter(item => !supportedModels.has(item.model)) } };
|
|
39
|
+
if (typeof page.last_id !== 'string' || !page.last_id || page.last_id !== page.data.at(-1).id || cursors.has(page.last_id)) throw Error('invalid cursor');
|
|
40
|
+
cursor = page.last_id; cursors.add(cursor);
|
|
41
|
+
}
|
|
42
|
+
throw Error('too many pages');
|
|
43
|
+
} catch {
|
|
44
|
+
return signal?.aborted ? unavailable('aborted', 'Catalog lookup was cancelled.') : unavailable('invalid_response', 'Anthropic catalog is unavailable.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { effortField, validateRoles, ModelValidationError } from './config.mjs';
|
|
2
|
+
import { discoverAnthropicCatalog } from './catalog-anthropic.mjs';
|
|
3
|
+
export { discoverAnthropicCatalog } from './catalog-anthropic.mjs';
|
|
4
|
+
export { listCodexModelCapabilities } from './codex-catalog.mjs';
|
|
5
|
+
|
|
6
|
+
const unavailable = (code, message) => ({ status: 'unavailable', code, message });
|
|
7
|
+
const validText = value => typeof value === 'string' && value.length > 0 && value === value.trim() && !/[\p{Cc}\u2028\u2029]/u.test(value);
|
|
8
|
+
|
|
9
|
+
function codexCatalog(models, secrets) {
|
|
10
|
+
if (!Array.isArray(models) || !models.length) throw Error('invalid catalog');
|
|
11
|
+
const pairs = [], missing = [], seenPairs = new Set(), seenModels = new Set(), supportedModels = new Set();
|
|
12
|
+
for (const item of models) {
|
|
13
|
+
if (!item || !validText(item.model) || secrets.some(secret => item.model.includes(secret)) || !Array.isArray(item.efforts) || !item.efforts.every(value => validText(value) && !secrets.some(secret => value.includes(secret)))) throw Error('invalid model');
|
|
14
|
+
if (!item.efforts.length && !seenModels.has(item.model)) missing.push({ model: item.model, reason: 'Effort capability is unavailable.' });
|
|
15
|
+
seenModels.add(item.model);
|
|
16
|
+
for (const effort of item.efforts) {
|
|
17
|
+
supportedModels.add(item.model);
|
|
18
|
+
const key = JSON.stringify([item.model, effort]);
|
|
19
|
+
if (!seenPairs.has(key)) { seenPairs.add(key); pairs.push({ model: item.model, effort }); }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { platform: 'codex', pairs, unavailable: missing.filter(item => !supportedModels.has(item.model)) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function discoverModelCatalog({ platform, env = process.env, fetchImpl = globalThis.fetch, listCodexModels, signal, timeoutMs = 10000 } = {}) {
|
|
26
|
+
if (!['codex', 'claude'].includes(platform)) return unavailable('unsupported_platform', 'Catalog is unavailable.');
|
|
27
|
+
if (signal?.aborted) return unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
28
|
+
if (platform === 'codex' && typeof listCodexModels !== 'function') return unavailable('unavailable', 'Codex catalog is unavailable.');
|
|
29
|
+
const controller = new AbortController(); let timedOut = false, resolveAbort;
|
|
30
|
+
const abortPromise = new Promise(resolve => { resolveAbort = resolve; });
|
|
31
|
+
const onAbort = () => { controller.abort(); resolveAbort(); };
|
|
32
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
const timer = setTimeout(() => { timedOut = true; onAbort(); }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 10000);
|
|
34
|
+
const operation = Promise.resolve().then(async () => platform === 'codex'
|
|
35
|
+
? { status: 'ready', catalog: codexCatalog(await listCodexModels({ env, signal: controller.signal }), [env?.OPENAI_API_KEY, env?.ANTHROPIC_API_KEY].filter(value => typeof value === 'string' && value.length)) }
|
|
36
|
+
: discoverAnthropicCatalog({ apiKey: env?.ANTHROPIC_API_KEY, fetchImpl, signal: controller.signal }));
|
|
37
|
+
const settled = operation.then(value => ({ value }), () => ({ failed: true }));
|
|
38
|
+
try {
|
|
39
|
+
const first = await Promise.race([settled, abortPromise.then(() => ({ aborted: true }))]);
|
|
40
|
+
if (first.aborted) {
|
|
41
|
+
if (platform === 'codex') {
|
|
42
|
+
let graceTimer;
|
|
43
|
+
await Promise.race([settled, new Promise(resolve => { graceTimer = setTimeout(resolve, 2100); })]);
|
|
44
|
+
clearTimeout(graceTimer);
|
|
45
|
+
}
|
|
46
|
+
return timedOut ? unavailable('timeout', 'Catalog lookup timed out.') : unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
47
|
+
}
|
|
48
|
+
return first.failed ? unavailable('unavailable', 'Catalog is unavailable.') : first.value;
|
|
49
|
+
} finally { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function assertCatalogRoles({ platform, roles, catalog } = {}) {
|
|
53
|
+
const validRoles = validateRoles(roles, platform);
|
|
54
|
+
if (!catalog || catalog.platform !== platform || !Array.isArray(catalog.pairs)) throw new ModelValidationError('Catalog is unavailable.');
|
|
55
|
+
const pairs = new Set();
|
|
56
|
+
for (const pair of catalog.pairs) {
|
|
57
|
+
if (!pair || !validText(pair.model) || !validText(pair.effort)) throw new ModelValidationError('Catalog is invalid.');
|
|
58
|
+
pairs.add(JSON.stringify([pair.model, pair.effort]));
|
|
59
|
+
}
|
|
60
|
+
const effort = effortField(platform);
|
|
61
|
+
if (Object.values(validRoles).some(value => !pairs.has(JSON.stringify([value.model, value[effort]])))) throw new ModelValidationError('Roles contain a pair absent from the catalog.');
|
|
62
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
|
|
5
|
+
const ENV_KEYS = ['PATH', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'SYSTEMROOT', 'WINDIR', 'TMPDIR', 'TEMP', 'TMP', 'CODEX_HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME'];
|
|
6
|
+
const validText = value => typeof value === 'string' && value.length > 0 && value === value.trim() && !/[\p{Cc}\u2028\u2029]/u.test(value);
|
|
7
|
+
const failure = () => new Error('Codex catalog is unavailable.');
|
|
8
|
+
|
|
9
|
+
export async function listCodexModelCapabilities({ env = process.env, cwd, spawnImpl = spawn, signal, timeoutMs = 10000 } = {}) {
|
|
10
|
+
if (typeof cwd !== 'string' || !path.isAbsolute(cwd) || path.normalize(cwd) !== cwd || typeof spawnImpl !== 'function' || signal?.aborted) throw failure();
|
|
11
|
+
const childEnv = Object.fromEntries(ENV_KEYS.filter(key => typeof env?.[key] === 'string').map(key => [key, env[key]]));
|
|
12
|
+
const secrets = [env?.OPENAI_API_KEY, env?.ANTHROPIC_API_KEY].filter(value => typeof value === 'string' && value.length);
|
|
13
|
+
const containsSecret = value => secrets.some(secret => value.includes(secret));
|
|
14
|
+
let child;
|
|
15
|
+
try { child = spawnImpl('codex', ['app-server'], { cwd, env: childEnv, stdio: ['pipe', 'pipe', 'pipe'], shell: false }); }
|
|
16
|
+
catch { throw failure(); }
|
|
17
|
+
if (!child?.stdin || !child?.stdout || !child?.stderr || typeof child.on !== 'function' || typeof child.kill !== 'function') throw failure();
|
|
18
|
+
|
|
19
|
+
let closed = false, interrupted = false, cancelled = false, nextId = 0, buffer = '';
|
|
20
|
+
const pending = new Map(), decoder = new StringDecoder('utf8');
|
|
21
|
+
let resolveClosed;
|
|
22
|
+
const closedPromise = new Promise(resolve => { resolveClosed = resolve; });
|
|
23
|
+
const rejectPending = () => { interrupted = true; for (const entry of pending.values()) entry.reject(failure()); pending.clear(); };
|
|
24
|
+
const onClose = () => { closed = true; resolveClosed(); rejectPending(); };
|
|
25
|
+
const onError = () => rejectPending();
|
|
26
|
+
const onData = chunk => {
|
|
27
|
+
buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
28
|
+
if (buffer.length > 1048576) { rejectPending(); return; }
|
|
29
|
+
let newline;
|
|
30
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
31
|
+
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
|
32
|
+
if (!line.trim()) continue;
|
|
33
|
+
let message; try { message = JSON.parse(line); } catch { rejectPending(); return; }
|
|
34
|
+
if (message?.id === undefined) continue;
|
|
35
|
+
const entry = pending.get(message.id);
|
|
36
|
+
if (!entry) { rejectPending(); return; }
|
|
37
|
+
pending.delete(message.id);
|
|
38
|
+
if (message.error || !Object.hasOwn(message, 'result')) entry.reject(failure()); else entry.resolve(message.result);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
child.on('close', onClose); child.on('error', onError);
|
|
42
|
+
child.stdout.on('data', onData); child.stdout.on('error', onError); child.stdin.on('error', onError); child.stderr.on('error', onError); child.stderr.resume();
|
|
43
|
+
const onAbort = () => { cancelled = true; rejectPending(); };
|
|
44
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
if (signal?.aborted) onAbort();
|
|
46
|
+
const timer = setTimeout(onAbort, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 10000);
|
|
47
|
+
const send = value => { if (interrupted || closed) throw failure(); child.stdin.write(JSON.stringify(value) + '\n'); };
|
|
48
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
49
|
+
if (interrupted || closed) { reject(failure()); return; }
|
|
50
|
+
const id = ++nextId; pending.set(id, { resolve, reject });
|
|
51
|
+
try { send({ id, method, params }); } catch { pending.delete(id); reject(failure()); }
|
|
52
|
+
});
|
|
53
|
+
let result, error;
|
|
54
|
+
try {
|
|
55
|
+
const initialized = await request('initialize', { clientInfo: { name: 'arc', title: 'ARC', version: '1.0.0' } });
|
|
56
|
+
if (!initialized || typeof initialized !== 'object' || Array.isArray(initialized) || !validText(initialized.userAgent)) throw failure();
|
|
57
|
+
send({ method: 'initialized', params: {} });
|
|
58
|
+
const models = new Map(), cursors = new Set(); let cursor;
|
|
59
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber++) {
|
|
60
|
+
const page = await request('model/list', { limit: 100, includeHidden: true, ...(cursor ? { cursor } : {}) });
|
|
61
|
+
if (interrupted) throw failure();
|
|
62
|
+
if (!page || !Array.isArray(page.data) || !page.data.length || !(page.nextCursor === null || validText(page.nextCursor))) throw failure();
|
|
63
|
+
for (const descriptor of page.data) {
|
|
64
|
+
if (descriptor?.hidden === true) continue;
|
|
65
|
+
if (!descriptor || !validText(descriptor.model) || containsSecret(descriptor.model) || !Array.isArray(descriptor.supportedReasoningEfforts)) throw failure();
|
|
66
|
+
let efforts = models.get(descriptor.model);
|
|
67
|
+
if (!efforts) { efforts = []; models.set(descriptor.model, efforts); }
|
|
68
|
+
for (const option of descriptor.supportedReasoningEfforts) {
|
|
69
|
+
if (!validText(option?.reasoningEffort) || containsSecret(option.reasoningEffort)) throw failure();
|
|
70
|
+
if (!efforts.includes(option.reasoningEffort)) efforts.push(option.reasoningEffort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (page.nextCursor === null) { result = [...models].map(([model, efforts]) => ({ model, efforts })); break; }
|
|
74
|
+
if (cursors.has(page.nextCursor)) throw failure();
|
|
75
|
+
cursors.add(page.nextCursor); cursor = page.nextCursor;
|
|
76
|
+
}
|
|
77
|
+
if (!result) throw failure();
|
|
78
|
+
} catch { error = failure(); }
|
|
79
|
+
finally {
|
|
80
|
+
rejectPending();
|
|
81
|
+
try { child.stdin.end(); } catch { error = failure(); }
|
|
82
|
+
if (!closed) { try { child.kill('SIGTERM'); } catch { error = failure(); } }
|
|
83
|
+
const wait = async ms => { if (closed) return true; let timerId; const timeout = new Promise(resolve => { timerId = setTimeout(() => resolve(false), ms); }); const value = await Promise.race([closedPromise.then(() => true), timeout]); clearTimeout(timerId); return value; };
|
|
84
|
+
if (!await wait(1000)) {
|
|
85
|
+
try { child.kill('SIGKILL'); } catch { error = failure(); }
|
|
86
|
+
if (!await wait(1000)) error = failure();
|
|
87
|
+
}
|
|
88
|
+
if (!closed) {
|
|
89
|
+
// A failed close may still emit late errors; keep only a guard until close.
|
|
90
|
+
const guard = () => {};
|
|
91
|
+
const streams = [child.stdin, child.stdout, child.stderr];
|
|
92
|
+
child.on('error', guard); for (const stream of streams) stream.on('error', guard);
|
|
93
|
+
child.once('close', () => { child.removeListener('error', guard); for (const stream of streams) stream.removeListener('error', guard); });
|
|
94
|
+
for (const stream of streams) stream.destroy();
|
|
95
|
+
}
|
|
96
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
97
|
+
if (cancelled) error = failure();
|
|
98
|
+
child.stdout.removeListener('data', onData); child.stdout.removeListener('error', onError); child.stdin.removeListener('error', onError); child.stderr.removeListener('error', onError);
|
|
99
|
+
child.removeListener('close', onClose); child.removeListener('error', onError);
|
|
100
|
+
}
|
|
101
|
+
if (error) throw error;
|
|
102
|
+
return result;
|
|
103
|
+
}
|