@yunhozz/arc 0.2.3 → 0.2.4
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 +73 -79
- package/claude/agents/arc-implementer.md +32 -42
- package/claude/agents/arc-planner.md +33 -47
- package/claude/agents/arc-qa.md +28 -44
- package/claude/agents/arc-reviewer.md +28 -41
- package/claude/agents/arc-scouter.md +26 -38
- package/claude/skill/SKILL.md +41 -59
- package/claude/skill/references/arc-visual-companion.md +19 -24
- package/claude/skill/references/commit.md +38 -70
- package/claude/skill/references/coordination.md +77 -126
- package/claude/skill/references/git-recovery.md +17 -35
- package/claude/skill/references/handoff-prep.md +91 -103
- package/claude/skill/references/handoff.md +110 -208
- package/claude/skill/references/limits.md +19 -31
- package/claude/skill/references/planning.md +93 -244
- package/claude/skill/references/verification.md +105 -261
- package/claude/skill/runtime/adapters/claude.mjs +10 -9
- package/claude/skill/runtime/adapters/codex.mjs +7 -8
- package/claude/skill/runtime/bootstrap.mjs +3 -2
- package/claude/skill/runtime/capabilities.mjs +12 -0
- package/claude/skill/runtime/cooperative-schema.mjs +3 -2
- package/claude/skill/runtime/cooperative.mjs +52 -4
- package/claude/skill/runtime/evaluate.mjs +115 -2
- package/claude/skill/runtime/identity.mjs +22 -0
- package/claude/skill/runtime/role-policy.json +18 -18
- package/claude/skill/runtime/schema.mjs +19 -0
- package/claude/skill/scripts/model_config.py +3 -79
- package/codex/agents/arc-implementer.toml +34 -30
- package/codex/agents/arc-planner.toml +27 -23
- package/codex/agents/arc-qa.toml +24 -15
- package/codex/agents/arc-reviewer.toml +25 -16
- package/codex/agents/arc-scouter.toml +20 -26
- package/codex/skill/SKILL.md +42 -57
- package/codex/skill/references/arc-visual-companion.md +21 -27
- package/codex/skill/references/commit.md +35 -81
- package/codex/skill/references/coordination.md +76 -104
- package/codex/skill/references/git-recovery.md +29 -51
- package/codex/skill/references/handoff-prep.md +76 -134
- package/codex/skill/references/handoff-worktree.md +32 -66
- package/codex/skill/references/handoff.md +111 -271
- package/codex/skill/references/limits.md +21 -34
- package/codex/skill/references/planning.md +125 -299
- package/codex/skill/references/verification.md +90 -272
- package/codex/skill/runtime/adapters/claude.mjs +10 -9
- package/codex/skill/runtime/adapters/codex.mjs +7 -8
- package/codex/skill/runtime/bootstrap.mjs +3 -2
- package/codex/skill/runtime/capabilities.mjs +12 -0
- package/codex/skill/runtime/cooperative-schema.mjs +3 -2
- package/codex/skill/runtime/cooperative.mjs +52 -4
- package/codex/skill/runtime/evaluate.mjs +115 -2
- package/codex/skill/runtime/identity.mjs +22 -0
- package/codex/skill/runtime/role-policy.json +18 -18
- package/codex/skill/runtime/schema.mjs +19 -0
- package/lib/installer/global-config.mjs +1 -0
- package/lib/installer/manifest.mjs +1 -1
- package/lib/installer/plan.mjs +0 -3
- package/lib/installer/uninstall.mjs +2 -2
- package/lib/management/operations.mjs +4 -2
- package/lib/management/update.mjs +3 -3
- package/lib/models/profiles.mjs +1 -40
- package/lib/policy/render.mjs +1 -5
- package/lib/policy/role-policy.json +18 -18
- package/lib/runtime/adapters/claude.mjs +10 -9
- package/lib/runtime/adapters/codex.mjs +7 -8
- package/lib/runtime/bootstrap.mjs +3 -2
- package/lib/runtime/capabilities.mjs +12 -0
- package/lib/runtime/cooperative-schema.mjs +3 -2
- package/lib/runtime/cooperative.mjs +52 -4
- package/lib/runtime/evaluate.mjs +115 -2
- package/lib/runtime/identity.mjs +22 -0
- package/lib/runtime/schema.mjs +19 -0
- package/lib/tui/app.mjs +12 -8
- package/lib/tui/setup.mjs +5 -8
- package/package.json +1 -2
- package/claude/skill/hooks/git-policy.json +0 -208
- package/claude/skill/hooks/role-git-guard.mjs +0 -18
- package/claude/skill/hooks/runtime-gate.mjs +0 -12
- package/codex/skill/hooks/runtime-gate.mjs +0 -10
- package/lib/policy/claude-hook.mjs +0 -80
- package/scripts/probe-runtime-hooks.mjs +0 -55
|
@@ -4,6 +4,9 @@ import {evaluateCooperativeVerification,invalidateAffectedEvidence,readCooperati
|
|
|
4
4
|
import {readAssignmentGitEvidence,readCooperativeCommitGitEvidence} from './git-evidence.mjs';
|
|
5
5
|
import {recordCooperativeAssignmentResult} from './assignment.mjs';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
|
+
import {constants} from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {createHash} from 'node:crypto';
|
|
7
10
|
import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
|
|
8
11
|
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath,absolute,within} from './scope.mjs';
|
|
9
12
|
import {fingerprintRequest,saveDurableState,applyCooperativeCommitTransition} from './store.mjs';
|
|
@@ -23,12 +26,14 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
23
26
|
if(!sameCheckout(state.target,target))return result(undefined,{decision:'hold',ruleIds:['cooperative.target']});
|
|
24
27
|
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
25
28
|
const evaluation=evaluateCooperative({state,request:{command,input},evidence:{target,versions:await cooperativeVersions(),changes}});
|
|
26
|
-
const unresolved=state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>['prepared','unknown','mismatch'].includes(i.status)&&!(['check','review'].includes(i.operation)&&i.stale));
|
|
29
|
+
const unresolved=state.plannerLedger?.stages.some(s=>s.pending)||state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>['prepared','unknown','mismatch'].includes(i.status)&&!(['check','review'].includes(i.operation)&&i.stale));
|
|
27
30
|
return {...result(state,evaluation.decision==='allow'&&unresolved?{decision:'hold',ruleIds:['cooperative.unresolved']}:evaluation,{persisted:true}),state,support:cooperativeSupport(),...(changes?{changeFingerprints:changes}:{})};
|
|
28
31
|
}
|
|
29
|
-
let lock;
|
|
32
|
+
let lock,plannerLock;
|
|
30
33
|
try{
|
|
31
34
|
lock=await fs.open(statePath+'.lock','wx',0o600);let state=await read();
|
|
35
|
+
const plannerWork=command==='prepare'&&input.operation==='planner-call'||command==='record'&&input.evidence.kind.startsWith('planner-');
|
|
36
|
+
if(plannerWork)plannerLock=await fs.open(path.join(target.gitCommonDir,'arc','planner.lock'),'wx',0o600);
|
|
32
37
|
if(!sameCheckout(state.target,target))return result(state,{decision:'hold',ruleIds:['cooperative.target']});
|
|
33
38
|
const requestFingerprint=fingerprintRequest({command,input});const prior=state.requests.find(r=>r.requestId===input.requestId);
|
|
34
39
|
if(prior){if(prior.requestFingerprint!==requestFingerprint)throw Error('Request fingerprint conflict');return result(state,{decision:'hold',ruleIds:['cooperative.replay']},{replayed:true});}
|
|
@@ -45,9 +50,12 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
45
50
|
try{git=await readCommitGit({target:currentTarget,...(commitIntent&&input.result?.data?.commit?{commit:input.result.data.commit}:{})});}catch(error){gitError=error.message;}
|
|
46
51
|
}
|
|
47
52
|
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
48
|
-
|
|
53
|
+
const plannerArtifacts=plannerWork?await readPlannerArtifacts(state,command==='record'&&input.evidence.kind==='planner-evidence'?input.evidence:undefined):undefined;
|
|
54
|
+
const plannerHistory=command==='record'&&input.evidence.kind==='planner-stage'?await readPlannerHistory(state,statePath,input.evidence.data.document?.path):{};
|
|
55
|
+
let decision=evaluateCooperative({state,request:{command,input},evidence:{target:currentTarget,versions,git,changes,requestFingerprint,plannerArtifacts,...plannerHistory}});
|
|
49
56
|
if(gitError)decision={decision:'hold',ruleIds:[input.operation==='assign-agent'?'cooperative.documents':'cooperative.git-evidence'],reason:gitError};
|
|
50
57
|
if(command==='prepare'&&decision.decision==='allow'){
|
|
58
|
+
if(decision.proposedTransition?.plannerLedger)state.plannerLedger=decision.proposedTransition.plannerLedger;
|
|
51
59
|
if(decision.proposedTransition?.intent)state.intents.push(decision.proposedTransition.intent);
|
|
52
60
|
if(decision.proposedTransition?.acceptance){state.acceptances.push(decision.proposedTransition.acceptance);state.tasks.find(t=>t.id===input.taskId).status='accepted';}
|
|
53
61
|
}
|
|
@@ -100,6 +108,15 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
100
108
|
if(decision.decision==='allow'&&actualResult.status==='succeeded'&&intent.operation==='assign-agent')state.assignments.push({intentId:intent.requestId,taskId:intent.taskId,agentId:actualResult.data.agentId,status:'active',source:'operator-attested',identityVerified:false});
|
|
101
109
|
}
|
|
102
110
|
else state.unresolved.push({kind:'unmatched-result',requestId:input.intentId,revision:state.revision+1});
|
|
111
|
+
}else if(command==='record'&&input.evidence.kind.startsWith('planner-')){
|
|
112
|
+
if(decision.proposedTransition?.plannerLedger&&(decision.decision==='allow'||decision.proposedTransition.recordUncertainty)){
|
|
113
|
+
state.plannerLedger=decision.proposedTransition.plannerLedger;
|
|
114
|
+
state.records.push({...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion});
|
|
115
|
+
if(input.evidence.kind==='planner-result'){
|
|
116
|
+
const intent=state.intents.find(i=>i.operation==='planner-call'&&i.requestId===input.evidence.data.callRequestId);
|
|
117
|
+
if(intent)intent.status=input.evidence.data.status;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
103
120
|
}else if(command==='record'&&decision.decision==='allow'){
|
|
104
121
|
const entry={...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion};
|
|
105
122
|
if(entry.kind==='scope'){
|
|
@@ -132,5 +149,36 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
132
149
|
state.requests.push({requestId:input.requestId,requestFingerprint,request:structuredClone({command,input}),decision:output});
|
|
133
150
|
state.audit.push({command,requestId:input.requestId,revision:state.revision,decision:output.decision,ruleIds:output.ruleIds,source:'operator-attested',actor:input.actor,...(input.intentId?{intentId:input.intentId,resolution:state.results.at(-1)?.status}:{}),...(input.operation?{operation:input.operation}:{})});
|
|
134
151
|
validateCooperativeState(state);await inspectPath(statePath);await saveDurableState({statePath,state,storage});return output;
|
|
135
|
-
}finally{if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
|
|
152
|
+
}finally{if(plannerLock){await plannerLock.close();await fs.unlink(path.join(target.gitCommonDir,'arc','planner.lock'));}if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readPlannerArtifacts(state,newRecord){
|
|
156
|
+
const entries=(state.plannerLedger?.stages??[]).flatMap(s=>(s.artifacts??[]).map(a=>({...a,stageId:s.id})));
|
|
157
|
+
if(newRecord)entries.push({...newRecord.data,ref:newRecord.messageRef});
|
|
158
|
+
return Promise.all(entries.map(async artifact=>{
|
|
159
|
+
try{
|
|
160
|
+
if(!absolute(artifact.path)||!within(state.target.realpath,artifact.path))throw Error('Artifact path escapes workspace');
|
|
161
|
+
await inspectPath(artifact.path);const handle=await fs.open(artifact.path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);let bytes;
|
|
162
|
+
try{const before=await handle.stat();if(!before.isFile())throw Error('Artifact must be a regular file');bytes=await handle.readFile();const after=await handle.stat(),named=await fs.lstat(artifact.path);if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||after.dev!==named.dev||after.ino!==named.ino)throw Error('Artifact changed while reading');}finally{await handle.close();}
|
|
163
|
+
await inspectPath(artifact.path);if(!within(state.target.realpath,await fs.realpath(artifact.path)))throw Error('Artifact realpath escapes workspace');
|
|
164
|
+
const hash='sha256:'+createHash('sha256').update(bytes).digest('hex');
|
|
165
|
+
if(artifact.hash&&artifact.hash!==hash||artifact.contentHash&&artifact.contentHash!==hash)throw Error('Artifact hash mismatch');
|
|
166
|
+
return {...artifact,hash,verification:{status:'verified',source:'local-bytes'}};
|
|
167
|
+
}catch(error){return {...artifact,verification:{status:'unverified',source:'local-bytes'},error:error.message};}
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
async function readPlannerHistory(state,statePath,documentPath){
|
|
171
|
+
const plannerHistory=[];
|
|
172
|
+
try{
|
|
173
|
+
const root=path.dirname(path.dirname(statePath));await inspectPath(root,{directory:true});
|
|
174
|
+
for(const entry of await fs.readdir(root,{withFileTypes:true})){
|
|
175
|
+
if(entry.name===state.workflowId)continue;
|
|
176
|
+
if(!entry.isDirectory()||entry.isSymbolicLink())throw Error('Unsafe workflow history path');
|
|
177
|
+
const filename=path.join(root,entry.name,'state.json');await inspectPath(filename);const other=validateCooperativeState(JSON.parse(await fs.readFile(filename,'utf8')));
|
|
178
|
+
if(!sameCheckout(state.target,other.target))continue;
|
|
179
|
+
if(!other.plannerLedger&&other.documents.some(d=>d.path===documentPath||d.data?.path===documentPath))throw Error('Existing document history has no Planner ledger');
|
|
180
|
+
for(const stage of other.plannerLedger?.stages??[])plannerHistory.push({workflowId:other.workflowId,stage});
|
|
181
|
+
}
|
|
182
|
+
return {plannerHistory};
|
|
183
|
+
}catch(error){return {plannerHistory,plannerHistoryError:error.message};}
|
|
136
184
|
}
|
package/lib/runtime/evaluate.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {prepareCooperativeAssignment} from './assignment.mjs';
|
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import {validateCooperativeState,validateCooperativeCommand} from './cooperative-schema.mjs';
|
|
18
|
-
import {validateWorkflowState, validateRequest, validateEvidence} from './schema.mjs';
|
|
18
|
+
import {validateWorkflowState, validateRequest, validateEvidence,validatePlannerStage} from './schema.mjs';
|
|
19
19
|
import {ROLES, ROLE_OPERATIONS, GIT_OPERATIONS, EXTERNAL_OPERATIONS} from './operations.mjs';
|
|
20
20
|
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
21
21
|
const within=(base,file)=>{const relative=path.relative(base,file);return relative===''||(!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));};
|
|
@@ -82,7 +82,17 @@ export function evaluate(policy, state, request, evidence){
|
|
|
82
82
|
if(d.checks&&(!Array.isArray(approval.scope.checks)||d.checks.some(c=>!approval.scope.checks.some(approved=>same(approved,c)))))return result('hold','runtime.assignment','delegated checks not approved',['delegated-checks']);
|
|
83
83
|
if(d.temporary&&!same(d.temporary,approval.scope.temporary))return result('hold','runtime.assignment','delegated temporary scope not approved',['delegated-temporary-scope']);
|
|
84
84
|
if(state.assignments.some(a=>a.assignmentId===request.requestId||a.correlationRef===d.correlationRef))return result('hold','runtime.identity','assignment correlation already registered',['new-assignment-correlation']);
|
|
85
|
-
const next=structuredClone(state);
|
|
85
|
+
const next=structuredClone(state);
|
|
86
|
+
if(d.role==='planner'){
|
|
87
|
+
const stage=state.plannerLedger?.stages.find(s=>s.id===d.plannerCall?.stageId),proof=fact('planner-call');
|
|
88
|
+
const contract=stage&&{document:stage.document,inputVersion:stage.inputVersion,criteriaVersion:stage.criteriaVersion,scopeVersion:stage.scopeVersion};
|
|
89
|
+
if(!stage||stage.workflowId!==state.workflowId||stage.approvalRef!==approval.id||!same(approval.scope.plannerStage,contract)||!d.paths.includes(stage.document.path)||proof?.sourceRef!==correlation.sourceRef||!same({stageId:proof?.data.stageId,inputVersion:proof?.data.inputVersion,criteriaVersion:proof?.data.criteriaVersion,scopeVersion:proof?.data.scopeVersion},{stageId:stage.id,inputVersion:stage.inputVersion,criteriaVersion:stage.criteriaVersion,scopeVersion:stage.scopeVersion}))return result('hold','runtime.planner-call','Current approved Planner stage and provider evidence required',['planner-stage-ledger','planner-call-evidence']);
|
|
90
|
+
if(stage.document.kind==='plan'&&!state.documents.some(doc=>doc.kind==='spec'&&state.approvals.some(a=>a.id===doc.approvalRef&&a.contentHash===doc.contentHash&&evidence.provenance.some(p=>p.id===a.id&&p.messageRef===a.messageRef&&p.contentHash===doc.contentHash&&p.verification.sourceRef===a.messageRef))))return result('hold','runtime.planner-call','Approved spec prerequisite required',['approved-spec']);
|
|
91
|
+
const planner=evaluatePlannerCall({stage,request:{...d.plannerCall,requestId:request.requestId},evidence:{artifacts:proof.data.artifacts,semanticProvenance:'operator-attested'}});
|
|
92
|
+
if(planner.decision!=='allow')return planner;
|
|
93
|
+
next.plannerLedger.stages[next.plannerLedger.stages.findIndex(s=>s.id===stage.id)]=planner.proposedTransition;
|
|
94
|
+
}
|
|
95
|
+
next.assignments.push({assignmentId:request.requestId,requestId:request.requestId,requestedBy:request.actorRef,status:'pending',actorRef:null,platform:request.platform,correlationRef:d.correlationRef,role:d.role,taskId:d.taskId,paths:[...d.paths],operations:[...d.operations],approvalRef:d.approvalRef,...(d.role==='planner'?{plannerStageId:d.plannerCall.stageId}:{}),...(d.checks?{checks:structuredClone(d.checks)}:{}),...(d.temporary?{temporary:structuredClone(d.temporary)}:{})});
|
|
86
96
|
try{validateWorkflowState(next);}catch{return result('hold','runtime.assignment','invalid pending assignment scope',['valid-assignment']);}
|
|
87
97
|
return {...result('allow','runtime.assignment','pending assignment authorized; observed spawn result still required'),proposedTransition:next};
|
|
88
98
|
}
|
|
@@ -119,6 +129,7 @@ export function evaluateCooperative({policy,state,request,evidence}={}){
|
|
|
119
129
|
if(!same(evidence.versions,state.versions))return hold('cooperative.versions','Policy or runtime changed');
|
|
120
130
|
if(request.input&&request.input.expectedRevision!==state.revision)return hold('cooperative.revision','Stale revision');
|
|
121
131
|
if(request.command==='record'&&request.input.evidence.kind==='external-approval'&&request.input.actor.role!=='orchestrator')return hold('cooperative.external-approval','Orchestrator records explicit external approval');
|
|
132
|
+
if(request.command==='record'&&request.input.evidence.kind.startsWith('planner-')||request.command==='prepare'&&request.input.operation==='planner-call')return evaluateCooperativePlanner({state,request,evidence});
|
|
122
133
|
if(request.command==='prepare'){
|
|
123
134
|
if(unresolvedExternal(state))return hold('cooperative.external-unresolved','Investigate unresolved external effects before follow-up');
|
|
124
135
|
if(EXTERNAL_ACTIONS.includes(request.input.operation))return evaluateCooperativeExternal({state,request:request.input,approvalEvidence:state.records.findLast(r=>r.kind==='external-approval'&&r.messageRef===request.input.arguments?.approvalRef)});
|
|
@@ -137,3 +148,105 @@ export function reconcileCooperativeResult({state,intent,result,observations}={}
|
|
|
137
148
|
try{validateCooperativeState(state);}catch(error){return {...base,decision:'hold',ruleIds:['cooperative.schema'],reason:error.message};}
|
|
138
149
|
return {...base,decision:'hold',ruleIds:[!intent?'cooperative.intent':result?.status==='unknown'?'cooperative.unknown':'cooperative.reconciliation'],requiredEvidence:[],nextActions:[],proposedTransition:null,reason:!intent?'No matching durable intent':'Recorded operator statement requires reconciliation; no execution authorized'};
|
|
139
150
|
}
|
|
151
|
+
|
|
152
|
+
/** Pure Planner decision. Artifact verification belongs to the trusted reader;
|
|
153
|
+
* semantic judgments retain their recorded source and never become native proof. */
|
|
154
|
+
export function evaluatePlannerCall({stage,request,evidence}={}){
|
|
155
|
+
const base={expectedRevision:stage?.revision??0,proposedTransition:null,coverage:{enforcement:'decision-only',semanticProvenance:evidence?.semanticProvenance==='operator-attested'?'operator-attested':'unverified',nativeVerified:false}};
|
|
156
|
+
const result=(decision,reason,requiredEvidence=[])=>({...base,decision,ruleIds:['runtime.planner-call'],reason,requiredEvidence});
|
|
157
|
+
if(['format-only','re-expression-only','save-error-only','patch-error-only'].includes(request?.intent))return result('deny','Orchestrator handles non-progress delivery and formatting work');
|
|
158
|
+
try{validatePlannerStage(stage);}catch(error){return result('hold',error.message,['planner-stage-ledger']);}
|
|
159
|
+
if(!request||!['initial','correction','further'].includes(request.kind)||request.intent!=='substantive'||typeof request.requestId!=='string'||!request.requestId||request.stageId!==stage.id||!Array.isArray(request.criterionIds)||new Set(request.criterionIds).size!==request.criterionIds.length||!Array.isArray(request.evidenceRefs)||new Set(request.evidenceRefs).size!==request.evidenceRefs.length)return result('hold','Valid stage-bound Planner request required',['planner-request']);
|
|
160
|
+
if(stage.pending)return result('hold','Reconcile the pending Planner call before another request',['confirmed-planner-result']);
|
|
161
|
+
if(stage.calls.some(c=>c.requestId===request.requestId))return result('hold','Planner request already recorded',['new-request-id']);
|
|
162
|
+
const initial=stage.calls.find(c=>c.kind==='initial'),correction=stage.calls.find(c=>c.kind==='correction');
|
|
163
|
+
if(request.kind==='initial'&&stage.calls.length)return result('deny','Initial draft opportunity already used');
|
|
164
|
+
if(request.kind!=='initial'){
|
|
165
|
+
if(!initial||!['succeeded','failed'].includes(initial.status)||!stage.result||!['succeeded','failed'].includes(stage.result.status)||stage.result.requestId!==stage.calls.at(-1).requestId||!['succeeded','failed'].includes(stage.calls.at(-1).status))return result('hold','A confirmed prior result is required',['confirmed-planner-result']);
|
|
166
|
+
if(!stage.review?.full||stage.review.resultRef!==stage.result.ref||stage.review.criteriaVersion!==stage.criteriaVersion||request.reviewRef!==stage.review.ref||stage.review.source!=='operator-attested')return result('hold','A complete review of the recorded result is required',['consolidated-review']);
|
|
167
|
+
if(request.kind==='correction'&&correction)return result('deny','Consolidated correction opportunity already used');
|
|
168
|
+
if(request.kind==='further'&&!correction)return result('hold','Use the consolidated correction before further calls',['consolidated-correction']);
|
|
169
|
+
}
|
|
170
|
+
const used=new Set(stage.calls.flatMap(c=>c.evidenceHashes)),artifacts=request.evidenceRefs.map(ref=>evidence?.artifacts?.find(a=>a.ref===ref));
|
|
171
|
+
if(request.kind==='further'){
|
|
172
|
+
if(!request.criterionIds.length||request.criterionIds.some(id=>!stage.criteria.some(c=>c.id===id&&c.status==='open')))return result('deny','Concrete open criteria are required');
|
|
173
|
+
if(!request.evidenceRefs.length)return result('hold','Unresolved criteria alone are not new progress',['new-criterion-evidence']);
|
|
174
|
+
}
|
|
175
|
+
if(artifacts.some(a=>a&&a.stageId!==stage.id))return result('deny','Evidence belongs to another stage');
|
|
176
|
+
if(artifacts.some(a=>!a||!['input','progress','finding'].includes(a.purpose)||a.verification?.status!=='verified'||a.verification.source!=='local-bytes'||!/^sha256:[a-f0-9]{64}$/.test(a.hash??'')))return result('hold','Accessible locally verified input or progress bytes are required',['verified-artifact-bytes']);
|
|
177
|
+
if(request.kind==='further'){
|
|
178
|
+
if(artifacts.some(a=>used.has(a.hash)))return result('deny','Identical evidence bytes cannot authorize another call');
|
|
179
|
+
if(request.criterionIds.some(id=>!artifacts.some(a=>a.criterionIds?.includes(id))))return result('hold','Evidence must address every requested criterion',['criterion-evidence-link']);
|
|
180
|
+
if(artifacts.some(a=>!a.semantic||a.semantic.meaningful!==true||a.semantic.source!=='operator-attested'||!a.semantic.judgmentRef||a.semantic.targetHash!==a.hash))return result('hold','A hash-bound semantic judgment is required',['semantic-attestation']);
|
|
181
|
+
}
|
|
182
|
+
const next=structuredClone(stage),call={requestId:request.requestId,kind:request.kind,intent:request.intent,criterionIds:[...request.criterionIds],evidenceHashes:artifacts.map(a=>a.hash),evidenceRefs:[...request.evidenceRefs],reviewRef:request.reviewRef??null,status:'reserved',ordinal:stage.calls.length+1};
|
|
183
|
+
next.calls.push(call);next.pending=request.requestId;next.ready=false;
|
|
184
|
+
return {...result('allow','Planner call reserved; dispatch and result remain unverified'),proposedTransition:next};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Cooperative Planner records are attestations with separately observed bytes. */
|
|
188
|
+
export function evaluateCooperativePlanner({state,request,evidence}={}){
|
|
189
|
+
const base={expectedRevision:state.revision,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},proposedTransition:null,requiredEvidence:[]};
|
|
190
|
+
const result=(decision,reason)=>({...base,decision,ruleIds:['cooperative.planner'],reason});
|
|
191
|
+
if(request.input.actor.role!=='orchestrator')return result('hold','Orchestrator records and prepares Planner work');
|
|
192
|
+
if(!state.plannerLedger)return result('hold','Existing workflow Planner history has no ledger; reconciliation required');
|
|
193
|
+
const ledger=structuredClone(state.plannerLedger),input=request.input,data=input.evidence?.data;
|
|
194
|
+
const stage=ledger.stages.find(s=>s.id===(data?.stageId??input.arguments?.stageId));
|
|
195
|
+
const observed=evidence.plannerArtifacts??[];
|
|
196
|
+
const artifact=ref=>observed.find(a=>a.ref===ref&&a.stageId===stage?.id&&a.verification?.status==='verified');
|
|
197
|
+
const transition=(s,reason,decision='allow',uncertainty=false)=>({...result(decision,reason),proposedTransition:{plannerLedger:ledger,...(s?{stageId:s.id}:{}),...(uncertainty?{recordUncertainty:true}:{})}});
|
|
198
|
+
if(request.command==='prepare'){
|
|
199
|
+
if(!stage)return result('hold','Planner stage ledger unavailable');
|
|
200
|
+
if(stage.workflowId!==state.workflowId||stage.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion)return result('hold','Planner stage contract version changed');
|
|
201
|
+
const d=evaluatePlannerCall({stage,request:{...input.arguments,requestId:input.requestId},evidence:{artifacts:observed,semanticProvenance:'operator-attested'}});
|
|
202
|
+
if(d.decision!=='allow')return {...d,coverage:base.coverage,expectedRevision:state.revision};
|
|
203
|
+
ledger.stages[ledger.stages.indexOf(stage)]=d.proposedTransition;
|
|
204
|
+
return {...transition(stage,d.reason),proposedTransition:{plannerLedger:ledger,intent:{requestId:input.requestId,operation:'planner-call',stageId:stage.id,taskId:input.taskId??null,status:'prepared',fingerprint:evidence.requestFingerprint,source:'operator-attested',identityVerified:false,target:structuredClone(state.target),kind:input.arguments.kind}}};
|
|
205
|
+
}
|
|
206
|
+
const kind=input.evidence.kind,ref=input.evidence.messageRef;
|
|
207
|
+
if(kind==='planner-stage'){
|
|
208
|
+
if(ledger.stages.some(s=>s.id===data.id))return result('deny','Planner stage ID already exists');
|
|
209
|
+
const contract={document:data.document,inputVersion:data.inputVersion,criteriaVersion:data.criteriaVersion,scopeVersion:data.scopeVersion};
|
|
210
|
+
const approval=state.records.find(r=>r.kind==='approval'&&r.messageRef===data.approvalRef&&r.source==='operator-attested'&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion);
|
|
211
|
+
const approved=approval?.data?.plannerStage;
|
|
212
|
+
if(!approved||!same(contract.document,approved.document)||['inputVersion','criteriaVersion','scopeVersion'].some(k=>contract[k]!==approved[k])||contract.scopeVersion!==state.scopeVersion||!within(state.target.realpath,data.document?.path))return result('hold','Approved stage contract and current scope are required');
|
|
213
|
+
if(data.document.kind==='plan'&&!approval.data.prerequisiteRefs?.some(ref=>state.records.some(r=>r.kind==='approval'&&r.messageRef===ref&&r.data.kind==='spec'&&r.source==='operator-attested')))return result('hold','Approved spec prerequisite is required for plan drafting');
|
|
214
|
+
const previous=[...ledger.stages,...(evidence.plannerHistory??[]).map(h=>h.stage)].filter(s=>s.document.path===data.document.path&&s.document.kind===data.document.kind&&s.document.phase===data.document.phase);
|
|
215
|
+
if(evidence.plannerHistoryError)return result('hold','Planner history could not be verified: '+evidence.plannerHistoryError);
|
|
216
|
+
if(previous.length){
|
|
217
|
+
const prior=previous.find(s=>data.previousStageRef?.workflowId===s.workflowId&&data.previousStageRef?.stageId===s.id);
|
|
218
|
+
if(!prior||approved.newScope!==true||!same(approved.previousStageRef,data.previousStageRef)||data.inputVersion<=prior.inputVersion||data.approvalRef===prior.approvalRef)return result('hold','Existing stage history requires an approved scope transition and predecessor link');
|
|
219
|
+
}
|
|
220
|
+
const created={...structuredClone(data),workflowId:state.workflowId,targetVersion:state.targetVersion,targetIdentity:{realpath:state.target.realpath,gitCommonDir:state.target.gitCommonDir,gitDir:state.target.gitDir},calls:[],pending:null,result:null,review:null,artifacts:[],ready:false};
|
|
221
|
+
try{validatePlannerStage(created);}catch(error){return result('hold',error.message);}
|
|
222
|
+
ledger.stages.push(created);return transition(created,'Planner stage registered without resetting prior history');
|
|
223
|
+
}
|
|
224
|
+
if(!stage)return result('hold','Planner stage ledger unavailable');
|
|
225
|
+
if(stage.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion)return result('hold','Planner stage contract version changed');
|
|
226
|
+
if(kind==='planner-evidence'){
|
|
227
|
+
if(stage.artifacts.some(a=>a.ref===ref))return result('deny','Planner evidence reference already recorded');
|
|
228
|
+
const item=observed.find(a=>a.ref===ref&&a.stageId===stage.id);
|
|
229
|
+
if(!item||item.verification?.status!=='verified'||!['draft','self-review','input','progress','finding'].includes(data.purpose)||!Array.isArray(data.criterionIds)||data.criterionIds.some(id=>!stage.criteria.some(c=>c.id===id)))return result('hold','Accessible stage-bound artifact bytes and purpose required');
|
|
230
|
+
if(data.semantic&&(data.semantic.source!==undefined&&data.semantic.source!=='operator-attested'||typeof data.semantic.meaningful!=='boolean'||!data.semantic.judgmentRef||data.semantic.targetHash!==item.hash))return result('hold','Semantic attestation must bind the observed artifact hash');
|
|
231
|
+
stage.artifacts.push({...item,purpose:data.purpose,criterionIds:[...data.criterionIds],...(data.semantic?{semantic:{...data.semantic,source:'operator-attested'}}:{})});return transition(stage,'Artifact bytes recorded separately from semantic attestation');
|
|
232
|
+
}
|
|
233
|
+
if(kind==='planner-result'){
|
|
234
|
+
const call=stage.calls.find(c=>c.requestId===data.callRequestId);
|
|
235
|
+
if(!call||stage.pending!==call.requestId||!['reserved','unknown'].includes(call.status)||!['succeeded','failed','unknown'].includes(data.status))return result('hold','A pending Planner call and confirmed result identity are required');
|
|
236
|
+
if(data.status==='succeeded'&&(!artifact(data.artifactRef)||artifact(data.artifactRef).purpose!=='draft'||!artifact(data.selfReviewRef)||artifact(data.selfReviewRef).purpose!=='self-review'))return result('hold','Verified full draft and self-review artifacts are required');
|
|
237
|
+
call.status=data.status;call.resultRef=ref;stage.result={ref,requestId:call.requestId,status:data.status,artifactRef:data.artifactRef??null,selfReviewRef:data.selfReviewRef??null,source:'operator-attested'};stage.ready=false;
|
|
238
|
+
if(data.status!=='unknown')stage.pending=null;
|
|
239
|
+
return transition(stage,data.status==='unknown'?'Planner execution remains unknown':'Planner result statement recorded',data.status==='unknown'?'hold':'allow',data.status==='unknown');
|
|
240
|
+
}
|
|
241
|
+
if(kind==='planner-review'){
|
|
242
|
+
if(stage.pending||!stage.result||!['succeeded','failed'].includes(stage.result.status)||data.resultRef!==stage.result.ref||data.full!==true||typeof data.complete!=='boolean'||!Array.isArray(data.criteria)||data.criteria.length!==stage.criteria.length||new Set(data.criteria.map(c=>c.id)).size!==stage.criteria.length||data.criteria.some(c=>!stage.criteria.some(old=>old.id===c.id)||!['open','resolved'].includes(c.status)||!c.judgmentRef))return result('hold','Full review must bind the confirmed result and all criteria');
|
|
243
|
+
for(const old of stage.criteria)if(old.status==='resolved'&&data.criteria.find(c=>c.id===old.id).status==='open'){
|
|
244
|
+
const reassessment=data.reassessments?.find(r=>r.criterionId===old.id&&r.previousJudgmentRef===old.judgmentRef&&r.reasonRef),proof=artifact(reassessment?.newEvidenceRef);
|
|
245
|
+
if(!reassessment||!proof||!proof.criterionIds.includes(old.id)||!proof.semantic?.meaningful||stage.calls.some(c=>c.evidenceHashes.includes(proof.hash)))return result('hold','Resolved criteria require a new evidence-bound reassessment');
|
|
246
|
+
}
|
|
247
|
+
if(data.complete&&(data.criteria.some(c=>c.status!=='resolved')||!artifact(stage.result.artifactRef)||!artifact(stage.result.selfReviewRef)))return result('hold','Incomplete draft cannot become ready or accepted');
|
|
248
|
+
stage.criteria=structuredClone(data.criteria);stage.review={ref,resultRef:data.resultRef,criteriaVersion:stage.criteriaVersion,full:true,complete:data.complete,source:'operator-attested'};stage.ready=data.complete;
|
|
249
|
+
return transition(stage,'Semantic completeness and criterion judgments remain operator-attested');
|
|
250
|
+
}
|
|
251
|
+
return result('hold','Unsupported Planner record');
|
|
252
|
+
}
|
package/lib/runtime/identity.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {validateWorkflowState, text} from './schema.mjs';
|
|
2
2
|
import {ROLES} from './operations.mjs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fingerprintRequest,saveDurableState} from './store.mjs';
|
|
3
6
|
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
4
7
|
/** Observation is supplied by the trusted platform provider, never agent/tool arguments. */
|
|
5
8
|
export function registerAgentObservation(state, observation){
|
|
@@ -25,3 +28,22 @@ export function registerAgentObservation(state, observation){
|
|
|
25
28
|
validateWorkflowState(next);
|
|
26
29
|
return {decision:'allow',reason:'matching observed spawn bound to pending assignment',state:next,proposedTransition:next};
|
|
27
30
|
}
|
|
31
|
+
|
|
32
|
+
/** A raw start is audit only; verified correlation may bind an existing assignment. */
|
|
33
|
+
export async function recordPlannerAgentObservation({statePath,expectedRevision,input,platform,sessionRef,observation}={}){
|
|
34
|
+
if(!path.isAbsolute(statePath??'')||input?.hook_event_name!=='SubagentStart'||input.session_id!==sessionRef||!text(sessionRef)||!text(input.agent_id)||!['codex','claude'].includes(platform))throw Error('Trusted observation target and complete start required');
|
|
35
|
+
const parent=await fs.realpath(path.dirname(statePath));statePath=path.join(parent,path.basename(statePath));let lock;
|
|
36
|
+
try{
|
|
37
|
+
lock=await fs.open(statePath+'.lock','wx',0o600);const stat=await fs.lstat(statePath);if(!stat.isFile()||stat.isSymbolicLink())throw Error('Observation state must be regular file');
|
|
38
|
+
const state=validateWorkflowState(JSON.parse(await fs.readFile(statePath,'utf8'))),eventHash=fingerprintRequest({input,observation:observation??null});
|
|
39
|
+
if(state.plannerObservations?.some(o=>o.eventHash===eventHash))return {decision:'hold',reason:'Observation replay',proposedTransition:null,replayed:true};
|
|
40
|
+
const candidates=state.assignments.filter(a=>a.status==='pending'&&a.role==='planner');
|
|
41
|
+
let decision={decision:'hold',reason:'Raw event lacks verified assignment correlation',proposedTransition:null};
|
|
42
|
+
if(state.revision!==expectedRevision)decision.reason='Stale provider context cannot bind identity';
|
|
43
|
+
else if(observation?.actorRef===input.agent_id&&observation.platform===platform)decision=registerAgentObservation(state,observation);
|
|
44
|
+
const correlated=decision.decision==='allow'&&Boolean(decision.proposedTransition),next=correlated?decision.proposedTransition:structuredClone(state);
|
|
45
|
+
const entry={eventHash,event:'SubagentStart',source:correlated?'verified-provider':'hook-stdin',sessionRef,agentRef:input.agent_id,classification:correlated?'correlated':candidates.length?'uncorrelated':'unmatched',identityVerified:correlated,reason:decision.reason,stageCandidates:candidates.map(a=>({assignmentId:a.assignmentId,stageId:a.plannerStageId??null,requestId:a.requestId})),coverage:{enforcement:'observation-only',nativeVerified:false,prevention:false},deduplication:'content-hash-only'};
|
|
46
|
+
next.plannerObservations=[...(state.plannerObservations??[]),entry];next.revision=state.revision+1;next.audit.push({event:'SubagentStart',eventHash,revision:next.revision,classification:entry.classification,source:entry.source});validateWorkflowState(next);await saveDurableState({statePath,state:next});
|
|
47
|
+
return {...decision,proposedTransition:correlated?next:null,observation:entry,persisted:true};
|
|
48
|
+
}finally{if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
|
|
49
|
+
}
|
package/lib/runtime/schema.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export function validateWorkflowState(s){
|
|
|
59
59
|
if(s.acceptances.some(a=>!object(a)||!text(a.taskId)||!text(a.actorRef)||!text(a.fingerprint)||!strings(a.criteria)||!strings(a.checkIds)||!strings(a.findingIds)))fail('acceptances');
|
|
60
60
|
if(s.commits.some(c=>!object(c)||!text(c.purpose)||!text(c.taskId)||!text(c.expectedParent)||!text(c.expectedTree)))fail('commits');
|
|
61
61
|
if(s.requests.some(r=>!object(r)||!text(r.requestId)||!hash(r.requestFingerprint)||!object(r.decision))||new Set(s.requests.map(r=>r.requestId)).size!==s.requests.length)fail('request replay');
|
|
62
|
+
if(s.plannerLedger!==undefined){validatePlannerLedger(s.plannerLedger);if(s.plannerLedger.stages.some(stage=>stage.workflowId!==s.workflowId))fail('Planner workflow binding');}
|
|
62
63
|
return s;
|
|
63
64
|
}
|
|
64
65
|
export function validateRequest(r){
|
|
@@ -77,3 +78,21 @@ export function validateEvidence(e){
|
|
|
77
78
|
}
|
|
78
79
|
return e;
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
/** Shared stage shape. Missing ledgers remain valid legacy state, never fresh history. */
|
|
83
|
+
export function validatePlannerStage(stage){
|
|
84
|
+
if(stage?.ready!==undefined&&typeof stage.ready!=='boolean')fail('Planner readiness type');
|
|
85
|
+
if(!object(stage)||!text(stage.id)||!text(stage.workflowId)||!object(stage.document)||!absolute(stage.document.path)||!['spec','plan'].includes(stage.document.kind)||!text(stage.document.phase)||!text(stage.approvalRef)||!Number.isSafeInteger(stage.inputVersion)||stage.inputVersion<1||!Number.isSafeInteger(stage.scopeVersion)||stage.scopeVersion<0||!Number.isSafeInteger(stage.criteriaVersion)||stage.criteriaVersion<1)fail('Planner stage identity');
|
|
86
|
+
if(!Array.isArray(stage.criteria)||!stage.criteria.length||!unique(stage.criteria,'id')||stage.criteria.some(c=>!['open','resolved'].includes(c.status)||!text(c.judgmentRef)))fail('Planner criteria');
|
|
87
|
+
if(!Array.isArray(stage.calls)||!unique(stage.calls,'requestId')||stage.calls.some(c=>!['initial','correction','further'].includes(c.kind)||!['reserved','unknown','succeeded','failed'].includes(c.status)||!Array.isArray(c.evidenceHashes)||c.evidenceHashes.some(h=>!hash(h)))||stage.calls.filter(c=>c.kind==='initial').length>1||stage.calls.filter(c=>c.kind==='correction').length>1)fail('Planner call history');
|
|
88
|
+
if(stage.pending!==null&&(!text(stage.pending)||!stage.calls.some(c=>c.requestId===stage.pending&&['reserved','unknown'].includes(c.status))))fail('Planner pending call');
|
|
89
|
+
const pending=stage.calls.filter(c=>['reserved','unknown'].includes(c.status));if(pending.length>1||pending.length!==(stage.pending===null?0:1))fail('Planner orphan reservation');
|
|
90
|
+
if(stage.artifacts!==undefined&&(!Array.isArray(stage.artifacts)||!unique(stage.artifacts,'ref')||stage.artifacts.some(a=>!absolute(a.path)||!hash(a.hash)||!Array.isArray(a.criterionIds)||!['draft','self-review','input','progress','finding'].includes(a.purpose)||a.verification?.status!=='verified'||a.verification.source!=='local-bytes'||a.semantic&&a.semantic.source!=='operator-attested')))fail('Planner artifact evidence');
|
|
91
|
+
if(stage.ready===true&&(stage.pending!==null||stage.criteria.some(c=>c.status!=='resolved')||stage.result?.status!=='succeeded'||stage.review?.complete!==true||stage.review?.full!==true||stage.review?.resultRef!==stage.result.ref||!stage.artifacts?.some(a=>a.ref===stage.result.artifactRef&&a.purpose==='draft')||!stage.artifacts?.some(a=>a.ref===stage.result.selfReviewRef&&a.purpose==='self-review')))fail('Planner incomplete readiness');
|
|
92
|
+
return stage;
|
|
93
|
+
}
|
|
94
|
+
export function validatePlannerLedger(ledger){
|
|
95
|
+
if(!object(ledger)||ledger.version!==1||!Array.isArray(ledger.stages)||!unique(ledger.stages,'id'))fail('Planner ledger');
|
|
96
|
+
for(const stage of ledger.stages)validatePlannerStage(stage);
|
|
97
|
+
return ledger;
|
|
98
|
+
}
|
package/lib/tui/app.mjs
CHANGED
|
@@ -8,7 +8,7 @@ function goHome(){throw Object.assign(Error('Return home'),{code:'ARC_TUI_HOME'}
|
|
|
8
8
|
function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
|
|
9
9
|
export function createTuiInterface(session,{input,controller}={}){
|
|
10
10
|
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',wordmark=false}={}){
|
|
11
|
+
async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false}={}){
|
|
12
12
|
const actions=backRow?[...rows,'Back']:rows;
|
|
13
13
|
selected=actions.length?Math.max(0,Math.min(selected,actions.length-1)):0;
|
|
14
14
|
while(true){
|
|
@@ -17,7 +17,7 @@ export function createTuiInterface(session,{input,controller}={}){
|
|
|
17
17
|
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
18
|
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
19
|
const rendered=session.render(screen),e=await event();
|
|
20
|
-
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')return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
|
|
20
|
+
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
21
|
}
|
|
22
22
|
}
|
|
23
23
|
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;}}
|
|
@@ -32,7 +32,11 @@ export function createTuiInterface(session,{input,controller}={}){
|
|
|
32
32
|
async function busy(action){session.render({title:'Working',mode:'busy',rows:['Operation in progress. Please wait.'],hint:navigationHint('busy')});session.setBusy(true);const wasFlowing=input?.readableFlowing;const onData=chunk=>{if(String(chunk).includes('\x03')||String(chunk).includes('\x04'))controller?.abort();};input?.on('data',onData);try{const result=await action();if(controller?.signal.aborted){if(typeof result?.code!=='number')throw interrupted();return{...result,code:130,interrupted:true};}return result;}finally{input?.removeListener('data',onData);if(wasFlowing!==true)input?.pause?.();session.setBusy(false);session.discardInput();}}
|
|
33
33
|
return{menu,text,page,busy};
|
|
34
34
|
}
|
|
35
|
-
export function resultRows(result){
|
|
35
|
+
export function resultRows(result){
|
|
36
|
+
const clean=text=>text.replace(/; rollback: [^\r\n]*/g,''),label=platform=>({codex:'Codex',claude:'Claude'}[platform]??platform);
|
|
37
|
+
const removed=result.code===0&&['removed','absent'].includes(result.cli?.status);
|
|
38
|
+
return[...(result.messages??[]).map(s=>clean(s.trim())),...[result.stdout,result.stderr].flatMap(text=>text?text.trimEnd().split(/\r?\n/).map(clean):[]),...(result.results??[]).flatMap(item=>{const platform=item.platform??item.target?.platform??'Target';return[...(removed&&item.status==='uninstalled'?[]:[item.status==='installed'?`ARC installed for ${label(platform)}.`:item.status==='uninstalled'?`${label(platform)} ARC settings removed.`:`${platform}: ${item.status}`]),...(item.residualPaths??[]).map(p=>'Residual path: '+p),...(item.error?['Error: '+item.error.message]:[])];}),...(removed?[result.cli.status==='removed'?'ARC settings and CLI removed.':'ARC settings removed. CLI is not installed.']:result.cli?['CLI: '+result.cli.status,...(result.cli.error?['CLI error: '+result.cli.error.message]:[])]:[]),...(result.error?['Error: '+result.error.message]:[]),'Exit code: '+result.code,...(result.exitSession?[result.cli?'CLI removal ended this session.':'CLI replacement ended this session.']:[])];
|
|
39
|
+
}
|
|
36
40
|
export async function runTui(options={}){
|
|
37
41
|
let session=options.session,code=0,finalReport;const controller=new AbortController(),signals=options.signals??process;const abort=()=>controller.abort();signals.on('SIGINT',abort);signals.on('SIGTERM',abort);
|
|
38
42
|
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;
|
|
@@ -42,11 +46,11 @@ export async function runTui(options={}){
|
|
|
42
46
|
await ui.page('Result',resultRows(result));return false;
|
|
43
47
|
}
|
|
44
48
|
async function uninstall(){
|
|
45
|
-
let
|
|
46
|
-
while(true){const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
49
|
+
let selected=0,target=0;
|
|
50
|
+
while(true){const choice=await ui.menu('Uninstall',['Settings','Remove CLI'],{selected,backRow:true});if(choice===null)return null;selected=choice;
|
|
51
|
+
if(choice===0){const next=await ui.menu('Uninstall settings',['Codex','Claude'],{selected:target,backRow:true});if(next===null)continue;target=next;return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude'][target],removeCli:false}));}
|
|
52
|
+
const proceed=await ui.menu('Remove CLI',['Continue','Back'],{selected:1,escapeBack:true,info:['Removes ARC settings for Codex and Claude, then removes the ARC CLI.','']});
|
|
53
|
+
if(proceed===0)return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:'all',removeCli:true}));
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
while(true){const statuses=await api.collectStatus(context),ordered=[...statuses].sort((a,b)=>({claude:0,codex:1}[a.platform]??2)-({claude:0,codex:1}[b.platform]??2));
|
package/lib/tui/setup.mjs
CHANGED
|
@@ -2,21 +2,18 @@ const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart'}[
|
|
|
2
2
|
const languageLabel=value=>({en:'EN',ko:'KR'}[value]??value);
|
|
3
3
|
import {parsePositiveLimit} from '../limits.mjs';
|
|
4
4
|
export function initialSettings(statuses){return Object.fromEntries(['codex','claude'].map(platform=>{const settings=statuses.find(s=>s.platform===platform)?.settings;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}];}));}
|
|
5
|
-
export function setupOptions(draft,target){if(
|
|
5
|
+
export function setupOptions(draft,target){if(!['codex','claude'].includes(target))throw Object.assign(Error('Unsupported setup target'),{code:'ARC_CLI_INPUT'});return{...draft[target],target};}
|
|
6
6
|
export async function runSetup({ui,statuses,draft,kind,prepare}){
|
|
7
7
|
for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
|
|
8
8
|
let selectedTarget=0;
|
|
9
|
-
while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude'
|
|
10
|
-
|
|
11
|
-
const choice=await ui.menu('Target settings differ',['Use Codex values for both','Use Claude values for both'],{backRow:true,info:['Codex: '+summary(draft.codex),'Claude: '+summary(draft.claude),'Choosing common values replaces the other target values.']});if(choice===null)continue;const source=choice===0?'codex':'claude';draft.codex={...draft[source]};draft.claude={...draft[source]};
|
|
12
|
-
}
|
|
13
|
-
const settings=draft[target==='all'?'codex':target];let editing=true,selectedField=kind==='preset'?1:0;
|
|
9
|
+
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;
|
|
14
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;
|
|
15
12
|
if(field===0){const value=await ui.menu('Performance preset',['Economy','Balanced','Smart'],{backRow:true,selected:['economy','balanced','smart'].indexOf(settings.preset)});if(value!==null)settings.preset=['economy','balanced','smart'][value];if(kind==='preset')selectedField=1;}
|
|
16
13
|
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];}
|
|
17
|
-
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;
|
|
14
|
+
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;}}
|
|
18
15
|
else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
|
|
19
|
-
else{
|
|
16
|
+
else{return prepare({kind,...setupOptions(draft,target)});}
|
|
20
17
|
}
|
|
21
18
|
}
|
|
22
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunhozz/arc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"scripts/check-installer-parity.mjs",
|
|
32
32
|
"scripts/pack-release.mjs",
|
|
33
33
|
"scripts/postinstall.mjs",
|
|
34
|
-
"scripts/probe-runtime-hooks.mjs",
|
|
35
34
|
"scripts/update-manifest.mjs",
|
|
36
35
|
"scripts/update-role-policy.mjs",
|
|
37
36
|
"scripts/update-runtime-assets.mjs",
|