@yunhozz/arc 0.2.3 → 0.2.5

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.
Files changed (95) hide show
  1. package/MANIFEST.sha256 +86 -88
  2. package/claude/agents/arc-implementer.md +32 -42
  3. package/claude/agents/arc-planner.md +33 -47
  4. package/claude/agents/arc-qa.md +28 -44
  5. package/claude/agents/arc-reviewer.md +28 -41
  6. package/claude/agents/arc-scouter.md +26 -38
  7. package/claude/skill/SKILL.md +41 -59
  8. package/claude/skill/models.toml +11 -11
  9. package/claude/skill/references/arc-visual-companion.md +19 -24
  10. package/claude/skill/references/brainstorming.md +111 -42
  11. package/claude/skill/references/commit.md +38 -70
  12. package/claude/skill/references/coordination.md +77 -126
  13. package/claude/skill/references/git-recovery.md +17 -35
  14. package/claude/skill/references/handoff-prep.md +91 -103
  15. package/claude/skill/references/handoff.md +110 -208
  16. package/claude/skill/references/limits.md +19 -31
  17. package/claude/skill/references/planning.md +93 -244
  18. package/claude/skill/references/superpowers-provenance.json +3 -3
  19. package/claude/skill/references/verification.md +105 -261
  20. package/claude/skill/runtime/adapters/claude.mjs +10 -9
  21. package/claude/skill/runtime/adapters/codex.mjs +7 -8
  22. package/claude/skill/runtime/bootstrap.mjs +3 -2
  23. package/claude/skill/runtime/capabilities.mjs +12 -0
  24. package/claude/skill/runtime/cooperative-schema.mjs +3 -2
  25. package/claude/skill/runtime/cooperative.mjs +52 -4
  26. package/claude/skill/runtime/evaluate.mjs +115 -2
  27. package/claude/skill/runtime/identity.mjs +22 -0
  28. package/claude/skill/runtime/role-policy.json +18 -18
  29. package/claude/skill/runtime/schema.mjs +19 -0
  30. package/claude/skill/scripts/model_config.py +20 -84
  31. package/codex/agents/arc-implementer.toml +34 -30
  32. package/codex/agents/arc-planner.toml +27 -23
  33. package/codex/agents/arc-qa.toml +24 -15
  34. package/codex/agents/arc-reviewer.toml +25 -16
  35. package/codex/agents/arc-scouter.toml +20 -26
  36. package/codex/skill/SKILL.md +42 -57
  37. package/codex/skill/models.toml +13 -13
  38. package/codex/skill/references/arc-visual-companion.md +21 -27
  39. package/codex/skill/references/brainstorming.md +111 -42
  40. package/codex/skill/references/commit.md +35 -81
  41. package/codex/skill/references/coordination.md +76 -104
  42. package/codex/skill/references/git-recovery.md +29 -51
  43. package/codex/skill/references/handoff-prep.md +76 -134
  44. package/codex/skill/references/handoff-worktree.md +32 -66
  45. package/codex/skill/references/handoff.md +111 -271
  46. package/codex/skill/references/limits.md +21 -34
  47. package/codex/skill/references/planning.md +125 -299
  48. package/codex/skill/references/superpowers-provenance.json +3 -3
  49. package/codex/skill/references/verification.md +90 -272
  50. package/codex/skill/runtime/adapters/claude.mjs +10 -9
  51. package/codex/skill/runtime/adapters/codex.mjs +7 -8
  52. package/codex/skill/runtime/bootstrap.mjs +3 -2
  53. package/codex/skill/runtime/capabilities.mjs +12 -0
  54. package/codex/skill/runtime/cooperative-schema.mjs +3 -2
  55. package/codex/skill/runtime/cooperative.mjs +52 -4
  56. package/codex/skill/runtime/evaluate.mjs +115 -2
  57. package/codex/skill/runtime/identity.mjs +22 -0
  58. package/codex/skill/runtime/role-policy.json +18 -18
  59. package/codex/skill/runtime/schema.mjs +19 -0
  60. package/codex/skill/scripts/model_config.py +25 -6
  61. package/lib/installer/global-config.mjs +2 -1
  62. package/lib/installer/manifest.mjs +1 -1
  63. package/lib/installer/plan.mjs +6 -8
  64. package/lib/installer/preset.mjs +7 -4
  65. package/lib/installer/uninstall.mjs +2 -2
  66. package/lib/management/operations.mjs +4 -2
  67. package/lib/management/update.mjs +3 -3
  68. package/lib/models/catalog-anthropic.mjs +46 -0
  69. package/lib/models/catalog.mjs +62 -0
  70. package/lib/models/codex-catalog.mjs +103 -0
  71. package/lib/models/config.mjs +19 -11
  72. package/lib/models/index.mjs +1 -1
  73. package/lib/models/profiles.mjs +1 -40
  74. package/lib/policy/render.mjs +1 -5
  75. package/lib/policy/role-policy.json +18 -18
  76. package/lib/runtime/adapters/claude.mjs +10 -9
  77. package/lib/runtime/adapters/codex.mjs +7 -8
  78. package/lib/runtime/bootstrap.mjs +3 -2
  79. package/lib/runtime/capabilities.mjs +12 -0
  80. package/lib/runtime/cooperative-schema.mjs +3 -2
  81. package/lib/runtime/cooperative.mjs +52 -4
  82. package/lib/runtime/evaluate.mjs +115 -2
  83. package/lib/runtime/identity.mjs +22 -0
  84. package/lib/runtime/schema.mjs +19 -0
  85. package/lib/tui/app.mjs +26 -16
  86. package/lib/tui/custom-preset.mjs +122 -0
  87. package/lib/tui/render.mjs +3 -2
  88. package/lib/tui/setup.mjs +65 -12
  89. package/package.json +1 -2
  90. package/claude/skill/hooks/git-policy.json +0 -208
  91. package/claude/skill/hooks/role-git-guard.mjs +0 -18
  92. package/claude/skill/hooks/runtime-gate.mjs +0 -12
  93. package/codex/skill/hooks/runtime-gate.mjs +0 -10
  94. package/lib/policy/claude-hook.mjs +0 -80
  95. package/scripts/probe-runtime-hooks.mjs +0 -55
@@ -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);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.checks?{checks:structuredClone(d.checks)}:{}),...(d.temporary?{temporary:structuredClone(d.temporary)}:{})});
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
+ }
@@ -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
+ }
@@ -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
@@ -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',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
- while(true){
15
- 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(info,viewport.columns);
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 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;
21
- }
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;
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;
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){
@@ -32,21 +36,27 @@ export function createTuiInterface(session,{input,controller}={}){
32
36
  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
37
  return{menu,text,page,busy};
34
38
  }
35
- export function resultRows(result){return[...(result.messages??[]).map(s=>s.trim()),...[result.stdout,result.stderr].flatMap(text=>text?text.trimEnd().split(/\r?\n/):[]),...(result.results??[]).flatMap(item=>[`${item.platform??item.target?.platform??'Target'}: ${item.status}; rollback: ${item.rollback??'not needed'}`,...(item.residualPaths??[]).map(p=>'Residual path: '+p),...(item.error?['Error: '+item.error.message]:[])]),...(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.code===0?['Open a new agent session to use changed harness settings.']:['Review the failed target and retry from the menu.']),...(result.exitSession?[(result.cli?'CLI removal':'CLI replacement')+' ended this session. Run arc again for further management.']:[])];}
39
+ export function resultRows(result){
40
+ const clean=text=>text.replace(/; rollback: [^\r\n]*/g,''),label=platform=>({codex:'Codex',claude:'Claude'}[platform]??platform);
41
+ const removed=result.code===0&&['removed','absent'].includes(result.cli?.status);
42
+ 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.']:[])];
43
+ }
36
44
  export async function runTui(options={}){
37
45
  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
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;
39
47
  async function execute(prepared){
40
48
  if(!prepared)return;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;
41
- if(code===130||result.exitSession){finalReport=resultRows(result);if(result.exitSession)session.render({title:'Result',rows:finalReport,hint:'Session ended'});return true;}
42
- await ui.page('Result',resultRows(result));return false;
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;
43
53
  }
44
54
  async function uninstall(){
45
- let target=0,global=0;
46
- while(true){const nextTarget=await ui.menu('Uninstall target',['Codex','Claude','Both targets'],{selected:target,backRow:true});if(nextTarget===null)return null;target=nextTarget;
47
- while(true){const nextGlobal=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks'],{selected:global,backRow:true});if(nextGlobal===null)break;global=nextGlobal;
48
- return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));
49
- }
55
+ let selected=0,target=0;
56
+ while(true){const choice=await ui.menu('Uninstall',['Settings','Remove CLI'],{selected,backRow:true});if(choice===null)return null;selected=choice;
57
+ 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}));}
58
+ 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.','']});
59
+ if(proceed===0)return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:'all',removeCli:true}));
50
60
  }
51
61
  }
52
62
  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));
@@ -54,7 +64,7 @@ export async function runTui(options={}){
54
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;
55
65
  try{
56
66
  let prepared;
57
- if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:choice===0?'install':'preset',prepare:settings=>ui.busy(()=>api.prepareOperation(context,settings))});
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))});}
58
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;}
59
69
  else prepared=await uninstall();
60
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
+ }
@@ -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 lines=[...heading,...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),...position,...footer];
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,22 +1,75 @@
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
- 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(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude))throw Error('Target settings differ. Explicitly select common values before applying to both.');return{...draft[target==='all'?'codex':target],target};}
6
- export async function runSetup({ui,statuses,draft,kind,prepare}){
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
- while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude','Both targets'],{selected:selectedTarget,backRow:true});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude','all'][targetIndex];
10
- if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
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;
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];
55
+ const settings=draft[target];let editing=true,selectedField=0;
14
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 field=await ui.menu('Settings — '+target,labels,{selected:selectedField,separatorBefore:labels.length-2});if(field===null||field===labels.length-1)break;selectedField=field;
15
- 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;}
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&&value<3)settings.preset=presets[value];
66
+ 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';}}}
67
+ if(kind==='preset')selectedField=1;
68
+ }
16
69
  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;if(target==='all')draft.claude={...settings};break;}}
70
+ 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
71
  else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
19
- else{if(target==='all')draft.claude={...settings};return prepare({kind,...setupOptions(draft,target)});}
72
+ 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)});}
20
73
  }
21
74
  }
22
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",