@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
|
}
|
package/lib/tui/app.mjs
CHANGED
|
@@ -2,23 +2,27 @@ import {createTerminalSession} from './terminal.mjs';
|
|
|
2
2
|
import {escapeTerminalText,navigationHint,wrapDetailRows} from './render.mjs';
|
|
3
3
|
import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
|
|
4
4
|
import {collectStatus} from '../management/status.mjs';
|
|
5
|
-
import {runSetup} from './setup.mjs';
|
|
5
|
+
import {runSetup,loadPresetPreviewConfig} from './setup.mjs';
|
|
6
|
+
import {loadCustomCatalog} from './custom-preset.mjs';
|
|
6
7
|
const home=['Install / Update','Change Preset','Check Version','Uninstall','Exit'];
|
|
7
8
|
function goHome(){throw Object.assign(Error('Return home'),{code:'ARC_TUI_HOME'});}
|
|
8
9
|
function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
|
|
9
10
|
export function createTuiInterface(session,{input,controller}={}){
|
|
10
11
|
async function event(){const e=await session.nextEvent();if(e.name==='interrupt')throw interrupted();if(e.name==='eof')throw Object.assign(Error('Session closed'),{code:'ARC_TUI_EOF'});if(e.name==='error')throw e.error;return e;}
|
|
11
|
-
async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false}={}){
|
|
12
|
+
async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false,renderBody,refreshMs}={}){
|
|
12
13
|
const actions=backRow?[...rows,'Back']:rows;
|
|
13
14
|
selected=actions.length?Math.max(0,Math.min(selected,actions.length-1)):0;
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
let pending,timer,blink=true;
|
|
16
|
+
try{while(true){
|
|
17
|
+
const viewport=session.viewport(),display=actions.flatMap((row,index)=>(index===separatorBefore||backRow&&index===rows.length)?['',row]:[row]),displayIndex=selected+(Number.isInteger(separatorBefore)&&selected>=separatorBefore?1:0)+(backRow&&selected===rows.length?1:0),information=wrapDetailRows(typeof info==='function'?info({selected,...viewport}):info,viewport.columns);
|
|
16
18
|
if(wordmark&&information.length)information.push('');
|
|
17
19
|
const capacity=Math.max(1,viewport.rows-(wordmark?6:1)-information.length-4),offset=Math.max(0,Math.min(displayIndex-capacity+1,display.length-capacity)),visible=display.slice(offset,offset+capacity);
|
|
18
20
|
const screen={title,wordmark,rows:[...information,...visible],selected:actions.length?information.length+displayIndex-offset:undefined,hint:navigationHint('menu',{backLabel}),...(display.length>capacity?{position:(offset+1)+'-'+Math.min(offset+capacity,display.length)+'/'+display.length}:{})};
|
|
19
|
-
const
|
|
21
|
+
const body=renderBody?.({selected,blink,...viewport});if(body){screen.rows=body.rows;screen.selected=body.selected;screen.showSelection=body.showSelection;}
|
|
22
|
+
const rendered=session.render(screen);pending??=event();
|
|
23
|
+
let e;if(refreshMs>0){e=await Promise.race([pending,new Promise(resolve=>{timer=setTimeout(()=>resolve(null),refreshMs);})]);clearTimeout(timer);timer=undefined;if(e===null){blink=!blink;continue;}}else e=await pending;pending=undefined;
|
|
20
24
|
if(e.name==='up'&&actions.length)selected=(selected+actions.length-1)%actions.length;else if(e.name==='down'&&actions.length)selected=(selected+1)%actions.length;else if(e.name==='escape'){if(backLabel==='exit'||escapeBack)return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
|
|
21
|
-
}
|
|
25
|
+
}}finally{if(timer!==undefined)clearTimeout(timer);}
|
|
22
26
|
}
|
|
23
27
|
async function text(title,initial=''){let value=initial,selected=2;while(true){const rendered=session.render({title,mode:'text',rows:[value,'','Save','Back',''],selected,hint:navigationHint('text')});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='backspace')value=[...value].slice(0,-1).join('');else if(e.name==='up'||e.name==='down')selected=selected===2?3:2;else if(e.name==='escape')goHome();else if(e.name==='enter'&&rendered.selectionEnabled!==false)return selected===2?value:null;}}
|
|
24
28
|
async function page(title,rows){
|
|
@@ -42,8 +46,10 @@ export async function runTui(options={}){
|
|
|
42
46
|
try{session??=createTerminalSession(options);const ui=createTuiInterface(session,{input:options.input??process.stdin,controller}),api={collectStatus,prepareOperation,executeOperation,...options.services},context=options.context?{...options.context,signal:controller.signal}:await createManagementContext({...options.contextOptions,signal:controller.signal});const draft={};let selected=0;
|
|
43
47
|
async function execute(prepared){
|
|
44
48
|
if(!prepared)return;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;
|
|
45
|
-
if(code===
|
|
46
|
-
|
|
49
|
+
if(code===0&&['install','preset'].includes(prepared.kind))for(const plan of prepared.plans??[]){const platform=plan.target?.platform;if(draft[platform])delete draft[platform].customEdited;}
|
|
50
|
+
const rows=prepared.kind==='update'&&code===0?[`ARC updated to ${prepared.latestVersion}.`,'Start a new agent session to use the updated settings.']:resultRows(result);
|
|
51
|
+
if(code===130||result.exitSession){finalReport=rows;if(result.exitSession)session.render({title:'Result',rows:finalReport,hint:'Session ended'});return true;}
|
|
52
|
+
await ui.page('Result',rows);return false;
|
|
47
53
|
}
|
|
48
54
|
async function uninstall(){
|
|
49
55
|
let selected=0,target=0;
|
|
@@ -58,7 +64,7 @@ export async function runTui(options={}){
|
|
|
58
64
|
const info=ordered.length?[ordered.map(status=>label(status)+': '+state(status)).join(' / '),...ordered.flatMap(status=>(status.diagnostics??[]).map(error=>label(status)+': '+error.message))]:[];const choice=await ui.menu('ARC',home,{selected,info,backLabel:'exit',wordmark:true});if(choice===null||choice===4)return code;selected=choice;
|
|
59
65
|
try{
|
|
60
66
|
let prepared;
|
|
61
|
-
if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:
|
|
67
|
+
if(choice===0||choice===1){const kind=choice===0?'install':'preset';prepared=await runSetup({ui,statuses,draft,kind,loadPreviewConfig:target=>loadPresetPreviewConfig({context,statuses,kind,target}),loadCatalog:target=>api.loadCatalog?api.loadCatalog(target):loadCustomCatalog({platform:target,env:context.env??process.env,invocationCwd:context.cwd??process.cwd(),userHome:context.userHome,signal:controller.signal}),prepare:settings=>ui.busy(()=>api.prepareOperation(context,settings))});}
|
|
62
68
|
else if(choice===2){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));const action=await ui.menu('Check Version',prepared.canUpgrade?['Version Upgrade']:[],{backRow:true,info:['Current version: '+prepared.currentVersion,'Latest version: '+(prepared.latestVersion??'Unavailable'),...(prepared.latestError?[prepared.latestError.message]:[]),...(prepared.canUpgrade?['Continue?']:[])]});if(action!==0)prepared=null;}
|
|
63
69
|
else prepared=await uninstall();
|
|
64
70
|
if(await execute(prepared))return code;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ROLES, effortField, validateRoles } from '../models/config.mjs';
|
|
5
|
+
import { assertCatalogRoles, discoverModelCatalog, listCodexModelCapabilities } from '../models/catalog.mjs';
|
|
6
|
+
import { escapeTerminalText, wrapDetailRows } from './render.mjs';
|
|
7
|
+
import { resolveTarget } from '../installer/targets.mjs';
|
|
8
|
+
import { presetPreviewRows } from './setup.mjs';
|
|
9
|
+
|
|
10
|
+
const names = ['Scouter', 'Planner', 'Implementer', 'QA', 'Reviewer'];
|
|
11
|
+
const unavailable = () => ({ status: 'unavailable', code: 'unavailable', message: 'Catalog is unavailable.' });
|
|
12
|
+
|
|
13
|
+
async function assertProjectless(directory) {
|
|
14
|
+
for (let current = directory;; current = path.dirname(current)) {
|
|
15
|
+
for (const name of ['.git', '.codex']) {
|
|
16
|
+
try { await fs.lstat(path.join(current, name)); throw Error('Project configuration is present'); }
|
|
17
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
18
|
+
}
|
|
19
|
+
if (path.dirname(current) === current) break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadCustomCatalog({ platform, env = process.env, signal, fetchImpl, listCodexModels = listCodexModelCapabilities, tempDir = os.tmpdir(), invocationCwd = process.cwd(), userHome = os.homedir() } = {}) {
|
|
24
|
+
if (platform !== 'codex') return discoverModelCatalog({ platform, env, signal, fetchImpl });
|
|
25
|
+
let directory, result;
|
|
26
|
+
try {
|
|
27
|
+
const childEnv = env?.CODEX_HOME === undefined ? env : { ...env, CODEX_HOME: resolveTarget('codex', { env, cwd: invocationCwd, userHome }).home };
|
|
28
|
+
const base = await fs.realpath(tempDir);
|
|
29
|
+
await assertProjectless(base);
|
|
30
|
+
directory = await fs.mkdtemp(path.join(base, 'arc-model-catalog-'));
|
|
31
|
+
const cwd = await fs.realpath(directory);
|
|
32
|
+
await assertProjectless(cwd);
|
|
33
|
+
result = await discoverModelCatalog({ platform, env: childEnv, signal, listCodexModels: options => listCodexModels({ ...options, cwd }) });
|
|
34
|
+
} catch { result = unavailable(); }
|
|
35
|
+
finally { if (directory) try { await fs.rm(directory, { recursive: true, force: true }); } catch { result = unavailable(); } }
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pairKey(model, effort) { return JSON.stringify([model, effort]); }
|
|
40
|
+
function short(value, limit = 12) {
|
|
41
|
+
const characters = [...escapeTerminalText(value)];
|
|
42
|
+
return characters.length > limit ? characters.slice(0, limit - 1).join('') + '…' : characters.join('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function details(model, effort, { columns = 80, rows = 24 } = {}) {
|
|
46
|
+
return [...wrapDetailRows([`Model: ${model}`], columns), ...wrapDetailRows([`Effort: ${effort}`], columns), ''];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detailGuard(info) {
|
|
50
|
+
return state => {
|
|
51
|
+
const requiredRows = wrapDetailRows(info(state), state.columns).length + 6;
|
|
52
|
+
return state.rows < requiredRows ? { rows: [`Menu requires ${requiredRows} rows at ${state.columns} columns.`], selected: undefined } : undefined;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function editCustomPreset({ ui, config, initialRoles, catalog }) {
|
|
57
|
+
const platform = config.platform, field = effortField(platform), roles = validateRoles(initialRoles, platform);
|
|
58
|
+
if (catalog?.platform !== platform || !Array.isArray(catalog.pairs) || !catalog.pairs.length) return null;
|
|
59
|
+
const available = new Set(catalog.pairs.map(pair => pairKey(pair.model, pair.effort)));
|
|
60
|
+
let selectedRole = 0;
|
|
61
|
+
while (true) {
|
|
62
|
+
const labels = ROLES.map((role, index) => {
|
|
63
|
+
const value = roles[role], missing = !available.has(pairKey(value.model, value[field]));
|
|
64
|
+
return `${names[index]}: ${short(value.model)} / ${short(value[field], 8)}${missing ? ' !' : ''}`;
|
|
65
|
+
});
|
|
66
|
+
const selected = await ui.menu('Custom preset', [...labels, 'Done'], { selected: selectedRole, backRow: true, separatorBefore: labels.length, refreshMs:600, renderBody:({selected,blink,columns,rows})=>{
|
|
67
|
+
const values=ROLES.map((role,index)=>({role:names[index]+(!available.has(pairKey(roles[role].model,roles[role][field]))?' !':''),model:roles[role].model,effort:roles[role][field]}));
|
|
68
|
+
const diagram=presetPreviewRows(values,{columns,rows,selectedRole:selected<ROLES.length?selected:undefined,blink});
|
|
69
|
+
const body=[...diagram,'','Done','Back'];
|
|
70
|
+
return {rows:body,selected:diagram.length===1?undefined:selected<3?1:selected<5?9:selected===5?diagram.length+1:diagram.length+2,showSelection:selected>=ROLES.length};
|
|
71
|
+
}});
|
|
72
|
+
if (selected === null) return null;
|
|
73
|
+
if (selected === ROLES.length) {
|
|
74
|
+
try { assertCatalogRoles({ platform, roles, catalog }); return roles; }
|
|
75
|
+
catch { await ui.page('Custom preset unavailable', ['Select an available model and effort for each role.']); continue; }
|
|
76
|
+
}
|
|
77
|
+
if (!ROLES[selected]) continue;
|
|
78
|
+
selectedRole = selected;
|
|
79
|
+
const role = ROLES[selected], current = roles[role], initial = catalog.pairs.findIndex(pair => pairKey(pair.model, pair.effort) === pairKey(current.model, current[field]));
|
|
80
|
+
const models = [...new Set(catalog.pairs.map(pair => pair.model))];
|
|
81
|
+
const excluded = Array.isArray(catalog.unavailable) ? catalog.unavailable.filter(item => typeof item?.model === 'string' && typeof item.reason === 'string') : [];
|
|
82
|
+
const rows = [...models, ...(excluded.length ? ['Unavailable models'] : [])];
|
|
83
|
+
while (true) {
|
|
84
|
+
const info = state => {
|
|
85
|
+
const model = models[state.selected];
|
|
86
|
+
return [...(initial < 0 ? ['Current model and effort are unavailable.'] : []), ...details(model ?? '-', model ? catalog.pairs.filter(pair => pair.model === model).map(pair => pair.effort).join(', ') : '-', state)];
|
|
87
|
+
};
|
|
88
|
+
const renderModelMenu = state => {
|
|
89
|
+
const guarded = detailGuard(info)(state);
|
|
90
|
+
if (guarded) return guarded;
|
|
91
|
+
const information = wrapDetailRows(info(state), state.columns), items = [...rows, 'Back'], display = [], starts = [], lengths = [];
|
|
92
|
+
for (let index = 0; index < items.length; index++) {
|
|
93
|
+
if (index === rows.length) display.push('');
|
|
94
|
+
starts[index] = display.length;
|
|
95
|
+
const block = wrapDetailRows([items[index]], state.columns);
|
|
96
|
+
lengths[index] = block.length;
|
|
97
|
+
display.push(...block);
|
|
98
|
+
}
|
|
99
|
+
const selected = Math.max(0, Math.min(state.selected, items.length - 1)), commonCapacity = Math.max(1, state.rows - information.length - 5), positionNeeded = rows.length + 2 > commonCapacity, reservedRows = positionNeeded ? 5 : 4, requiredRows = information.length + lengths[selected] + reservedRows;
|
|
100
|
+
if (state.rows < requiredRows) return { rows: [`Menu requires ${requiredRows} rows at ${state.columns} columns.`], selected: undefined };
|
|
101
|
+
const capacity = Math.max(1, state.rows - information.length - reservedRows), maxOffset = Math.max(0, display.length - capacity);
|
|
102
|
+
const offset = Math.max(0, Math.min(maxOffset, starts[selected] + lengths[selected] - capacity));
|
|
103
|
+
return { rows: [...information, ...display.slice(offset, offset + capacity)], selected: information.length + starts[selected] - offset };
|
|
104
|
+
};
|
|
105
|
+
const choice = await ui.menu(`Select model — ${names[selected]}`, rows, {
|
|
106
|
+
selected: Math.max(0, models.indexOf(current.model)), backRow: true, info,
|
|
107
|
+
renderBody: renderModelMenu,
|
|
108
|
+
});
|
|
109
|
+
if (choice === null) break;
|
|
110
|
+
if (choice === models.length && excluded.length) {
|
|
111
|
+
await ui.page('Unavailable models', excluded.map(item => `${escapeTerminalText(item.model)}: ${escapeTerminalText(item.reason)}`));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (models[choice]) {
|
|
115
|
+
const model=models[choice], efforts=[...new Set(catalog.pairs.filter(pair=>pair.model===model).map(pair=>pair.effort))];
|
|
116
|
+
const effortInfo=state=>details(model,efforts[state.selected]??'-',state);
|
|
117
|
+
const effort=await ui.menu(`Select effort — ${names[selected]}`,efforts,{selected:Math.max(0,efforts.indexOf(current[field])),backRow:true,info:effortInfo,renderBody:detailGuard(effortInfo)});
|
|
118
|
+
if(effort!==null&&efforts[effort]){roles[role]={model,[field]:efforts[effort]};break;}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/tui/render.mjs
CHANGED
|
@@ -28,13 +28,14 @@ export function renderScreen(screen={}, {columns=80,rows=24}={}){
|
|
|
28
28
|
const entries=screen.rows??[],hint=screen.hint??navigationHint('menu');
|
|
29
29
|
const heading=screen.wordmark?[...wordmark,'─'.repeat(Math.min(columns,34))]:[escapeTerminalText(screen.title)],position=screen.position?[escapeTerminalText(screen.position)]:[];
|
|
30
30
|
const footer=screen.mode==='busy'||screen.hint==='Session ended'?[escapeTerminalText(hint)]:[...(screen.mode==='text'||screen.mode==='page'?[]:['']),'─'.repeat(columns),escapeTerminalText(hint)];
|
|
31
|
-
const
|
|
31
|
+
const showSelection=screen.showSelection!==false;
|
|
32
|
+
const lines=[...heading,...entries.map((row,index)=>(index===screen.selected&&showSelection?'> ':' ')+escapeTerminalText(row)),...position,...footer];
|
|
32
33
|
const fitted=lines.map(line=>fit(line,columns));
|
|
33
34
|
const bodySize=Math.max(0,rows-footer.length),visible=[...fitted.slice(0,Math.min(lines.length-footer.length,bodySize)),...fitted.slice(-Math.min(rows,footer.length))].map(line=>line.text);
|
|
34
35
|
const styles=[];
|
|
35
36
|
if(screen.wordmark){for(let index=0;index<Math.min(wordmark.length,visible.length);index++){if(visible[index]===fitted[index].text)styles.push({line:index,color:'logo',cells:[...visible[index]].flatMap((character,column)=>character==='█'?[{column,rgb:logoColor(column)}]:[])});}if(visible.length>wordmark.length&&visible[wordmark.length]===fitted[wordmark.length].text)styles.push({line:wordmark.length,color:'green'});}
|
|
36
37
|
const selectedLine=heading.length+screen.selected;
|
|
37
|
-
if(Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<Math.min(bodySize,lines.length-footer.length)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
|
|
38
|
+
if(showSelection&&Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<Math.min(bodySize,lines.length-footer.length)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
|
|
38
39
|
|
|
39
40
|
const selectionEnabled=Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<rows-footer.length-position.length&&fitted[selectedLine].complete;
|
|
40
41
|
return{text:visible.join('\n'),selectionEnabled,styles};
|
package/lib/tui/setup.mjs
CHANGED
|
@@ -1,19 +1,76 @@
|
|
|
1
|
-
const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart'}[value]??value);
|
|
1
|
+
const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart',custom:'Custom'}[value]??value);
|
|
2
2
|
const languageLabel=value=>({en:'EN',ko:'KR'}[value]??value);
|
|
3
3
|
import {parsePositiveLimit} from '../limits.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {readFileSync} from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {loadModelConfig,applyPreset,applyCustomRoles,resolveModelSettings,ModelValidationError} from '../models/index.mjs';
|
|
7
|
+
import {escapeTerminalText,wrapDetailRows} from './render.mjs';
|
|
8
|
+
import {editCustomPreset} from './custom-preset.mjs';
|
|
9
|
+
import {assertCatalogRoles} from '../models/catalog.mjs';
|
|
10
|
+
import {ROLES,effortField,validateRoles} from '../models/config.mjs';
|
|
11
|
+
const presets=['economy','balanced','smart','custom'];
|
|
12
|
+
const roleNames=['Scouter','Planner','Implementer','QA','Reviewer'];
|
|
13
|
+
export function loadPresetPreviewConfig({context,statuses,kind,target,readText=filename=>readFileSync(filename,'utf8')}){
|
|
14
|
+
if(!['codex','claude'].includes(target)||!['install','preset'].includes(kind))throw Object.assign(Error('Unsupported preset preview target or operation'),{code:'ARC_CLI_INPUT'});
|
|
15
|
+
if(kind==='install')return loadModelConfig(readText(path.join(context.packageRoot,target,'skill/models.toml')),target);
|
|
16
|
+
const config=statuses.find(status=>status.platform===target)?.settings?.modelConfig;
|
|
17
|
+
if(!config)throw new ModelValidationError('Installed model configuration unavailable for '+target);
|
|
18
|
+
const validated=applyPreset(config,config.preset);
|
|
19
|
+
if(validated.platform!==target)throw new ModelValidationError('Model configuration platform does not match target');
|
|
20
|
+
return validated;
|
|
21
|
+
}
|
|
22
|
+
export function presetRoleRows(config,preset){const resolved=resolveModelSettings(applyPreset(config,preset));return Object.values(resolved.roles).map((settings,index)=>({role:roleNames[index],model:settings.model,effort:settings[resolved.platform==='codex'?'model_reasoning_effort':'effort']}));}
|
|
23
|
+
export function presetPreviewRows(roleRows,{columns=80,rows=24,selectedRole,blink=true,compact=false}={}){
|
|
24
|
+
const values=roleRows.map(({role,model,effort})=>({role:escapeTerminalText(role),model:escapeTerminalText(model),effort:escapeTerminalText(effort)}));
|
|
25
|
+
const cells=value=>[...value].reduce((total,character)=>total+(/[\p{Mark}]/u.test(character)?0:/[\u2190-\u2193\u2500-\u257f\u2588]/u.test(character)?1:character.codePointAt(0)>=0x1100?2:1),0);
|
|
26
|
+
const width=Math.max(20,...values.flatMap(value=>[cells(value.role)+2,cells(value.model),cells('effort: '+value.effort)])),boxWidth=width+4,total=boxWidth*3+4,requiredRows=24;
|
|
27
|
+
if(columns<total+3||rows<requiredRows){
|
|
28
|
+
if(compact){
|
|
29
|
+
const available=Math.max(2,Math.floor(Number(columns)||80)-3),shorten=(value,limit)=>{if(cells(value)<=limit)return value;let result='',used=0;for(const character of value){const size=cells(character);if(used+size>Math.max(0,limit-1))break;result+=character;used+=size;}return result+'…';};
|
|
30
|
+
const roleRow=value=>{const limit=Math.max(1,available-cells(value.role)-cells(': / '+value.effort));return value.role+': '+shorten(value.model,limit)+' / '+value.effort;};
|
|
31
|
+
return [...values.map(roleRow),''];
|
|
32
|
+
}
|
|
33
|
+
return [`Diagram requires ${total+3} columns and ${requiredRows} rows.`];
|
|
34
|
+
}
|
|
35
|
+
const border=(left,right,junction,hidden)=>{const line=[left,...'─'.repeat(width+2),right];if(junction)line[Math.floor(boxWidth/2)]=junction;return hidden?' '.repeat(boxWidth):line.join('');};
|
|
36
|
+
const box=(value,index,up)=>{const hidden=index===selectedRole&&!blink,edge=hidden?' ':'│',label=(index===selectedRole?'> ':'')+value.role;return [border('┌','┐',up?'┴':null,hidden),...[label,value.model,'effort: '+value.effort].map(text=>edge+' '+text+' '.repeat(width-cells(text))+' '+edge),border('└','┘',up?null:'┬',hidden)];};
|
|
37
|
+
const group=(indices,indent,up)=>{const boxes=indices.map(index=>box(values[index],index,up));return Array.from({length:5},(_,row)=>' '.repeat(indent)+boxes.map(lines=>lines[row]).join(' '));};
|
|
38
|
+
const center=Math.floor(boxWidth/2),middle=boxWidth+2+center,right=2*(boxWidth+2)+center,indent=Math.floor((total-(boxWidth*2+2))/2),bottomLeft=indent+center,bottomRight=bottomLeft+boxWidth+2;
|
|
39
|
+
const bus=(left,right,branches,up=false)=>{const line=Array(total).fill(' ');for(let index=left;index<=right;index++)line[index]='─';for(const index of branches)line[index]=index===middle?'┼':up?'┴':'┬';return line.join('').trimEnd();};
|
|
40
|
+
return [...group([0,1,2],0,false),bus(center,right,[center,middle,right],true),' '.repeat(Math.max(0,middle-6))+'Orchestrator',bus(bottomLeft,bottomRight,[bottomLeft,middle,bottomRight]),...group([3,4],indent,true)];
|
|
41
|
+
}
|
|
42
|
+
export function initialSettings(statuses){return Object.fromEntries(['codex','claude'].map(platform=>{const settings=statuses.find(s=>s.platform===platform)?.settings,custom=settings?.modelConfig?.custom;return[platform,{preset:settings?.modelConfig?.preset??'balanced',language:settings?.language??'en',maxTotalSubagents:settings?.limits?.max_total_subagents??100,maxConcurrentSubagents:settings?.limits?.max_concurrent_subagents??5,skipGlobal:false,...(custom?{customRoles:structuredClone(custom)}:{})}];}));}
|
|
43
|
+
export function setupOptions(draft,target,verifiedCatalog,kind){if(!['codex','claude'].includes(target))throw Object.assign(Error('Unsupported setup target'),{code:'ARC_CLI_INPUT'});const {customEdited,customRoles,...settings}=draft[target];return{...settings,target,...(settings.preset==='custom'||customEdited?{customRoles,verifiedCatalog}:kind==='install'&&customRoles?{preservedCustomRoles:customRoles}:{})};}
|
|
44
|
+
export async function runSetup({ui,statuses,draft,kind,prepare,loadPreviewConfig,loadCatalog}){
|
|
7
45
|
for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
|
|
46
|
+
const catalogs=new Map();
|
|
47
|
+
async function catalogFor(target){
|
|
48
|
+
if(catalogs.has(target))return catalogs.get(target);
|
|
49
|
+
let result;try{result=typeof loadCatalog==='function'?await(ui.busy?ui.busy(()=>loadCatalog(target)):loadCatalog(target)):null;}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF','ARC_TUI_HOME'].includes(error?.code))throw error;}
|
|
50
|
+
if(result?.status==='ready'&&result.catalog?.platform===target&&Array.isArray(result.catalog.pairs)&&result.catalog.pairs.length){try{const pair=result.catalog.pairs[0],field=effortField(target),sample=Object.fromEntries(ROLES.map(role=>[role,{model:pair.model,[field]:pair.effort}]));assertCatalogRoles({platform:target,roles:sample,catalog:result.catalog});for(const candidate of result.catalog.pairs)validateRoles(Object.fromEntries(ROLES.map(role=>[role,{model:candidate.model,[field]:candidate.effort}])),target);catalogs.set(target,result.catalog);return result.catalog;}catch{}}
|
|
51
|
+
await ui.page('Catalog unavailable',['Model catalog could not be loaded.']);return null;
|
|
52
|
+
}
|
|
8
53
|
let selectedTarget=0;
|
|
9
54
|
while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude'],{selected:selectedTarget,backRow:true});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude'][targetIndex];
|
|
10
|
-
const settings=draft[target];let editing=true,selectedField=0;
|
|
11
|
-
while(editing){const labels=kind==='preset'?['Preset: '+presetLabel(settings.preset),'Apply','Back']:['Preset: '+presetLabel(settings.preset),'Harness language: '+languageLabel(settings.language),'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Apply','Back'];const field=await ui.menu('Settings — '+target,labels,{selected:selectedField,separatorBefore:labels.length-2});if(field===null||field===labels.length-1)break;selectedField=field;
|
|
12
|
-
if(field===0){
|
|
55
|
+
const settings=draft[target];let editing=true,selectedField=0,openPresetOnEntry=kind==='preset';
|
|
56
|
+
while(editing){const labels=kind==='preset'?['Preset: '+presetLabel(settings.preset),'Apply','Back']:['Preset: '+presetLabel(settings.preset),'Harness language: '+languageLabel(settings.language),'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Apply','Back'];const enteredDirectly=openPresetOnEntry,field=enteredDirectly?0:await ui.menu('Settings — '+target,labels,{selected:selectedField,separatorBefore:labels.length-2});openPresetOnEntry=false;if(field===null||field===labels.length-1)break;selectedField=field;
|
|
57
|
+
if(field===0){
|
|
58
|
+
const config=loadPreviewConfig(target);let preview=presets.indexOf(settings.preset);if(preview<0)preview=1;
|
|
59
|
+
const value=await ui.menu('Performance preset',['Economy','Balanced','Smart','Custom'],{backRow:true,selected:preview,info:({selected,columns,rows})=>{
|
|
60
|
+
const custom=settings.customRoles??config.custom;
|
|
61
|
+
const choice=selected>=presets.length||selected===3&&!custom?settings.preset:presets[selected];
|
|
62
|
+
const previewConfig=choice==='custom'&&custom?applyCustomRoles(config,custom):config;
|
|
63
|
+
return presetPreviewRows(presetRoleRows(previewConfig,choice),{columns,rows,compact:true});
|
|
64
|
+
}});
|
|
65
|
+
if(value===null&&enteredDirectly)break;
|
|
66
|
+
if(value!==null&&value<3)settings.preset=presets[value];
|
|
67
|
+
else if(value===3){const catalog=await catalogFor(target);if(catalog){const initial=settings.customRoles??config.custom??resolveModelSettings(applyPreset(config,settings.preset==='custom'?'balanced':settings.preset)).roles;const roles=await editCustomPreset({ui,config,initialRoles:initial,catalog});if(roles){settings.customRoles=roles;settings.customEdited=true;settings.preset='custom';}}}
|
|
68
|
+
if(kind==='preset')selectedField=1;
|
|
69
|
+
}
|
|
13
70
|
else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['EN','KR'],{backRow:true,selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
|
|
14
71
|
else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';let raw=String(settings[key]);while(true){raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',raw);if(raw===null)break;let value;try{value=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);continue;}settings[key]=value;break;}}
|
|
15
72
|
else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
|
|
16
|
-
else{return prepare({kind,...setupOptions(draft,target)});}
|
|
73
|
+
else{let catalog;if(settings.preset==='custom'||settings.customEdited){catalog=await catalogFor(target);if(!catalog)continue;try{assertCatalogRoles({platform:target,roles:settings.customRoles,catalog});}catch{await ui.page('Custom preset unavailable',['Select an available model and effort for each role.']);continue;}}return prepare({kind,...setupOptions(draft,target,catalog,kind)});}
|
|
17
74
|
}
|
|
18
75
|
}
|
|
19
76
|
}
|