@yunhozz/arc 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/MANIFEST.sha256 +109 -75
  2. package/README.md +19 -31
  3. package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
  4. package/claude/agents/arc-planner.md +4 -4
  5. package/claude/agents/arc-qa.md +4 -4
  6. package/claude/agents/arc-reviewer.md +5 -5
  7. package/claude/agents/arc-scouter.md +3 -3
  8. package/claude/skill/SKILL.md +7 -7
  9. package/claude/skill/models.toml +3 -3
  10. package/claude/skill/references/commit.md +22 -7
  11. package/claude/skill/references/coordination.md +25 -18
  12. package/claude/skill/references/git-recovery.md +1 -1
  13. package/claude/skill/references/handoff-prep.md +25 -2
  14. package/claude/skill/references/handoff.md +116 -24
  15. package/claude/skill/references/planning.md +78 -22
  16. package/claude/skill/references/verification.md +155 -17
  17. package/claude/skill/runtime/acceptance.mjs +68 -0
  18. package/claude/skill/runtime/assignment.mjs +42 -0
  19. package/claude/skill/runtime/bootstrap.mjs +35 -0
  20. package/claude/skill/runtime/cli.mjs +8 -0
  21. package/claude/skill/runtime/command.mjs +21 -0
  22. package/claude/skill/runtime/cooperative-schema.mjs +55 -0
  23. package/claude/skill/runtime/cooperative.mjs +136 -0
  24. package/claude/skill/runtime/evaluate.mjs +40 -4
  25. package/claude/skill/runtime/external.mjs +51 -0
  26. package/claude/skill/runtime/gate.mjs +7 -7
  27. package/claude/skill/runtime/git-evidence.mjs +46 -0
  28. package/claude/skill/runtime/operations.mjs +2 -2
  29. package/claude/skill/runtime/role-policy.json +64 -19
  30. package/claude/skill/runtime/schema.mjs +1 -0
  31. package/claude/skill/runtime/scope.mjs +36 -0
  32. package/claude/skill/runtime/store.mjs +25 -4
  33. package/claude/skill/runtime/verification.mjs +123 -0
  34. package/claude/skill/scripts/model_config.py +2 -2
  35. package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
  36. package/codex/agents/arc-planner.toml +6 -6
  37. package/codex/agents/arc-qa.toml +6 -6
  38. package/codex/agents/arc-reviewer.toml +6 -6
  39. package/codex/agents/arc-scouter.toml +5 -5
  40. package/codex/skill/SKILL.md +9 -9
  41. package/codex/skill/models.toml +3 -3
  42. package/codex/skill/references/commit.md +23 -8
  43. package/codex/skill/references/coordination.md +25 -18
  44. package/codex/skill/references/git-recovery.md +2 -2
  45. package/codex/skill/references/handoff-prep.md +25 -2
  46. package/codex/skill/references/handoff-worktree.md +4 -4
  47. package/codex/skill/references/handoff.md +126 -34
  48. package/codex/skill/references/planning.md +81 -23
  49. package/codex/skill/references/verification.md +160 -22
  50. package/codex/skill/runtime/acceptance.mjs +68 -0
  51. package/codex/skill/runtime/assignment.mjs +42 -0
  52. package/codex/skill/runtime/bootstrap.mjs +35 -0
  53. package/codex/skill/runtime/cli.mjs +8 -0
  54. package/codex/skill/runtime/command.mjs +21 -0
  55. package/codex/skill/runtime/cooperative-schema.mjs +55 -0
  56. package/codex/skill/runtime/cooperative.mjs +136 -0
  57. package/codex/skill/runtime/evaluate.mjs +40 -4
  58. package/codex/skill/runtime/external.mjs +51 -0
  59. package/codex/skill/runtime/gate.mjs +7 -7
  60. package/codex/skill/runtime/git-evidence.mjs +46 -0
  61. package/codex/skill/runtime/operations.mjs +2 -2
  62. package/codex/skill/runtime/role-policy.json +64 -19
  63. package/codex/skill/runtime/schema.mjs +1 -0
  64. package/codex/skill/runtime/scope.mjs +36 -0
  65. package/codex/skill/runtime/store.mjs +25 -4
  66. package/codex/skill/runtime/verification.mjs +123 -0
  67. package/codex/skill/scripts/model_config.py +3 -3
  68. package/lib/cli/postinstall.mjs +3 -16
  69. package/lib/cli/root.mjs +8 -18
  70. package/lib/cli/upgrade.mjs +1 -57
  71. package/lib/installer/manifest.mjs +7 -2
  72. package/lib/installer/plan.mjs +27 -13
  73. package/lib/installer/runtime-assets.mjs +25 -0
  74. package/lib/installer/shell-completion.mjs +15 -20
  75. package/lib/installer/targets.mjs +1 -1
  76. package/lib/installer/uninstall.mjs +5 -1
  77. package/lib/installer/upgrade-state.mjs +2 -2
  78. package/lib/installer/upgrade.mjs +2 -5
  79. package/lib/management/operations.mjs +39 -0
  80. package/lib/management/status.mjs +18 -0
  81. package/lib/management/update.mjs +58 -0
  82. package/lib/management/workflow.mjs +51 -0
  83. package/lib/models/config.mjs +1 -1
  84. package/lib/models/profiles.mjs +1 -1
  85. package/lib/policy/placement.mjs +6 -6
  86. package/lib/policy/render.mjs +3 -3
  87. package/lib/policy/role-policy.json +64 -19
  88. package/lib/release/artifact.mjs +1 -1
  89. package/lib/runtime/acceptance.mjs +68 -0
  90. package/lib/runtime/assignment.mjs +42 -0
  91. package/lib/runtime/bootstrap.mjs +35 -0
  92. package/lib/runtime/cli.mjs +8 -0
  93. package/lib/runtime/command.mjs +21 -0
  94. package/lib/runtime/cooperative-schema.mjs +55 -0
  95. package/lib/runtime/cooperative.mjs +136 -0
  96. package/lib/runtime/evaluate.mjs +40 -4
  97. package/lib/runtime/external.mjs +51 -0
  98. package/lib/runtime/gate.mjs +7 -7
  99. package/lib/runtime/git-evidence.mjs +46 -0
  100. package/lib/runtime/operations.mjs +2 -2
  101. package/lib/runtime/schema.mjs +1 -0
  102. package/lib/runtime/scope.mjs +36 -0
  103. package/lib/runtime/store.mjs +25 -4
  104. package/lib/runtime/verification.mjs +123 -0
  105. package/lib/tui/app.mjs +41 -0
  106. package/lib/tui/render.mjs +21 -0
  107. package/lib/tui/setup.mjs +20 -0
  108. package/lib/tui/terminal.mjs +71 -0
  109. package/lib/tui/workflow.mjs +12 -0
  110. package/package.json +1 -1
  111. package/scripts/postinstall.mjs +1 -3
  112. package/scripts/update-runtime-assets.mjs +6 -1
  113. package/lib/cli/choices.mjs +0 -47
  114. package/lib/cli/completion.mjs +0 -55
  115. package/lib/cli/index.mjs +0 -55
  116. package/lib/cli/preset.mjs +0 -29
  117. package/lib/cli/terminal.mjs +0 -19
  118. package/lib/cli/uninstall.mjs +0 -17
@@ -36,6 +36,7 @@ const unique = (items,key) => items.every(x=>object(x)&&text(x[key])) && new Set
36
36
  function approval(a){return object(a)&&text(a.id)&&['user-message','human-confirmation'].includes(a.source)&&text(a.messageRef)&&hash(a.contentHash)&&object(a.scope)&&text(a.scope.taskId)&&absolute(a.scope.target)&&strings(a.scope.operations)&&a.scope.operations.every(op=>OPERATIONS.includes(op))&&strings(a.scope.paths)&&a.scope.paths.every(absolute);}
37
37
  /** Validate stored workflow data. Observed provenance must still be checked by evaluate. */
38
38
  export function validateWorkflowState(s){
39
+ if(object(s)&&s.workflowKind!==undefined)fail('provider state cannot use cooperative contract');
39
40
  if(!object(s)||s.schemaVersion!==1||!text(s.workflowId)||!Number.isSafeInteger(s.revision)||s.revision<0)fail('version or identity');
40
41
  if(!object(s.versions)||['policy','runtime','adapter','policyHash','runtimeHash','adapterHash'].some(k=>!text(s.versions[k])))fail('versions');
41
42
  if(['policyHash','runtimeHash','adapterHash'].some(k=>!hash(s.versions[k])))fail('hash algorithm');
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {spawnSync} from 'node:child_process';
4
+ export const absolute=value=>typeof value==='string'&&path.isAbsolute(value)&&path.normalize(value)===value;
5
+ export const within=(base,file)=>absolute(file)&&!path.relative(base,file).split(path.sep).includes('..')&&!path.isAbsolute(path.relative(base,file));
6
+ /** Inspect every component, including ancestors outside the ARC namespace. */
7
+ export async function inspectPath(filename,{missing=false,directory=false}={}){
8
+ if(!absolute(filename))throw Error('Unsafe absolute path');
9
+ let cursor=path.parse(filename).root;
10
+ const parts=filename.slice(cursor.length).split(path.sep).filter(Boolean);
11
+ for(let i=0;i<parts.length;i++){
12
+ cursor=path.join(cursor,parts[i]);let stat;
13
+ try{stat=await fs.lstat(cursor);}catch(error){if(missing&&error.code==='ENOENT')return;throw error;}
14
+ if(stat.isSymbolicLink())throw Error('Unsafe symlink path');
15
+ if(i<parts.length-1||directory){if(!stat.isDirectory())throw Error('Unsafe directory type');}
16
+ else if(!stat.isFile())throw Error('State must be a regular file');
17
+ }
18
+ }
19
+ export function validateWorkflowId(id){if(typeof id!=='string'||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(id))throw Error('Invalid workflow ID');return id;}
20
+ /** Shell-free, read-only observation; inherited Git target/config overrides are removed. */
21
+ export async function observeCooperativeTarget(cwd){
22
+ if(!absolute(cwd))throw Error('Absolute workspace required');await inspectPath(cwd,{directory:true});
23
+ const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
24
+ Object.assign(env,{GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
25
+ const query=(args,detached=false)=>{const r=spawnSync('git',['--no-pager','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});if(detached&&r.status===1&&!r.error)return null;if(r.status!==0||r.error||!r.stdout.endsWith('\n')||r.stdout.slice(0,-1).includes('\n'))throw Error('Git target observation failed');return r.stdout.slice(0,-1);};
26
+ const target={realpath:query(['rev-parse','--show-toplevel']),gitCommonDir:query(['rev-parse','--path-format=absolute','--git-common-dir']),gitDir:query(['rev-parse','--absolute-git-dir']),head:query(['rev-parse','HEAD']),branch:query(['symbolic-ref','--quiet','--short','HEAD'],true)};
27
+ await validateObservedTarget(cwd,target);return target;
28
+ }
29
+ export async function validateObservedTarget(cwd,target){
30
+ if(!absolute(cwd)||!target||target.realpath!==cwd||!absolute(target.gitCommonDir)||!absolute(target.gitDir)||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(target.head)||!(target.branch===null||typeof target.branch==='string'&&target.branch))throw Error('Invalid observed target');
31
+ await inspectPath(cwd,{directory:true});await inspectPath(target.gitCommonDir,{directory:true});
32
+ return target;
33
+ }
34
+ export async function workflowStatePath(target,workflowId){
35
+ validateWorkflowId(workflowId);const statePath=path.join(target.gitCommonDir,'arc','workflows',workflowId,'state.json');await inspectPath(statePath,{missing:true});return statePath;
36
+ }
@@ -1,3 +1,4 @@
1
+ import {verifiedCooperativeReconciliation} from './acceptance.mjs';
1
2
  import {verifiedReconciliation} from './gate.mjs';
2
3
  import fs from 'node:fs/promises';
3
4
  import path from 'node:path';
@@ -51,13 +52,33 @@ export async function transactWorkflow({statePath,expectedRevision,requestId,req
51
52
  next.revision=state.revision+1;next.requests=prior?state.requests.map(r=>r.requestId===requestId?{requestId,requestFingerprint,decision}:r):[...state.requests,{requestId,requestFingerprint,decision}];
52
53
  next.audit=[...state.audit,{requestId,revision:next.revision,decision:decision.decision,ruleIds:decision.ruleIds??[],result:'pending'}];
53
54
  validateWorkflowState(next);
54
- temporary=path.join(parent,`.${path.basename(statePath)}.${crypto.randomUUID()}.tmp`);
55
- const handle=await fs.open(temporary,'wx',0o600);try{await handle.writeFile(JSON.stringify(next,null,2)+'\n');await handle.sync();}finally{await handle.close();}
56
- await fs.rename(temporary,statePath);temporary=undefined;
57
- const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
55
+ await saveDurableState({statePath,state:next});
58
56
  return {state:next,decision,replayed:false};
59
57
  }finally{
60
58
  if(temporary)await fs.unlink(temporary).catch(()=>{});
61
59
  if(lock){await lock.close();await fs.unlink(lockPath);}
62
60
  }
63
61
  }
62
+
63
+ /** Shared durability primitive. A post-rename sync error is reported as failure;
64
+ * the caller must re-read rather than assume rollback or repeat an effect. */
65
+ export async function saveDurableState({statePath,state,create=false,storage={}}){
66
+ const parent=path.dirname(statePath),temporary=path.join(parent,`.${path.basename(statePath)}.${crypto.randomUUID()}.tmp`);
67
+ let exists=false;
68
+ try{
69
+ const handle=await fs.open(temporary,'wx',0o600);exists=true;
70
+ try{await handle.writeFile(JSON.stringify(state,null,2)+'\n');await handle.sync();}finally{await handle.close();}
71
+ if(create){await (storage.link??fs.link)(temporary,statePath);await fs.unlink(temporary);}else await (storage.rename??fs.rename)(temporary,statePath);
72
+ exists=false;
73
+ const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
74
+ }finally{if(exists)await fs.unlink(temporary).catch(()=>{});}
75
+ }
76
+
77
+ /** Only a locally verified cooperative commit may advance the stored target. */
78
+ export function applyCooperativeCommitTransition({state,decision}){
79
+ if(decision?.decision!=='allow'||!verifiedCooperativeReconciliation(decision,state)||!decision.proposedTransition?.commit)throw Error('Verified cooperative reconciliation required');
80
+ const next=structuredClone(state),transition=decision.proposedTransition;
81
+ next.targetHistory.push({target:structuredClone(state.target),revision:state.revision+1,source:'local-observation',kind:'verified-commit-parent'});
82
+ next.target=structuredClone(transition.target);next.commits.push(structuredClone(transition.commit));next.tasks.find(t=>t.id===transition.commit.taskId).status='committed';
83
+ return next;
84
+ }
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {createHash} from 'node:crypto';
4
+ import {spawnSync} from 'node:child_process';
5
+ import {fingerprintRequest} from './store.mjs';
6
+ import {absolute,within,inspectPath} from './scope.mjs';
7
+ const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
8
+ const text=v=>typeof v==='string'&&v.length>0;
9
+ const hash=v=>typeof v==='string'&&/^sha256:[a-f0-9]{64}$/.test(v);
10
+ const list=v=>Array.isArray(v)&&v.every(text)&&new Set(v).size===v.length;
11
+ const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).every(k=>keys.includes(k));
12
+ const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null});
13
+ const hold=(state,id,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.'+id],reason});
14
+ const allow=(state,transition)=>({...base(state),decision:'allow',ruleIds:['cooperative.verification'],proposedTransition:transition});
15
+ const current=(state,scope,taskId,changes)=>scope==='final'?changes?.final:changes?.tasks?.[taskId];
16
+ const approval=(state,taskId)=>state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(taskId))).at(-1);
17
+ function authorized(state,task){
18
+ const a=approval(state,task?.id);
19
+ return task&&!task.invalidated&&a&&a.source==='operator-attested'&&a.targetVersion===state.targetVersion&&task.scopeVersion===a.scopeVersion&&task.targetVersion===state.targetVersion&&task.approvalRef===a.messageRef&&task.approvalVersion===a.data.approvalVersion&&task.documentVersion===a.data.documentVersion;
20
+ }
21
+ function descriptorValid(state,d,operation){
22
+ if(operation==='review')return fields(d,['kind','paths'])&&d.kind==='source-review'&&list(d.paths)&&d.paths.length>0&&d.paths.every(p=>absolute(p)&&within(state.target.realpath,p));
23
+ return fields(d,['executable','args','cwd','env','outputs'])&&absolute(d.executable)&&Array.isArray(d.args)&&d.args.every(v=>typeof v==='string')&&absolute(d.cwd)&&within(state.target.realpath,d.cwd)&&fields(d.env,Object.keys(d.env??{}))&&Object.values(d.env).every(v=>typeof v==='string')&&Array.isArray(d.outputs)&&d.outputs.every(p=>absolute(p)&&within(state.target.realpath,p));
24
+ }
25
+ const assignedImplementer=(state,id,taskId)=>state.assignments.some(a=>a.agentId===id&&a.taskId===taskId&&a.status==='active');
26
+ function independent(state,actor,taskId){
27
+ if(!fields(actor,['id','role'])||!text(actor.id))return false;
28
+ if(state.assignments.some(a=>a.agentId===actor.id))return false;
29
+ return !(state.intents??[]).some(i=>i.verificationActor?.id===actor.id&&(i.verificationActor.role!==actor.role||actor.role==='qa'&&i.taskId!==taskId))&&!(state.checks??[]).some(c=>c.verificationActor?.id===actor.id&&(c.verificationActor.role!==actor.role||actor.role==='qa'&&c.taskId!==taskId));
30
+ }
31
+ /** Invalidate only the affected dependency closure; preserve all historical evidence. */
32
+ export function invalidateAffectedEvidence({state,changedVersions={},affectedTaskIds}={}){
33
+ const next=structuredClone(state),ids=new Set(affectedTaskIds??next.tasks.map(t=>t.id));
34
+ let changed=true;while(changed){changed=false;for(const t of next.tasks)if(!ids.has(t.id)&&t.dependencies?.some(id=>ids.has(id))){ids.add(t.id);changed=true;}}
35
+ for(const t of next.tasks)if(ids.has(t.id)){t.invalidated=true;t.status='held';t.invalidatedBy=structuredClone(changedVersions);t.invalidatedAt=state.revision+1;}
36
+ for(const key of ['checks','acceptances'])for(const entry of next[key]??[])if(entry.scope==='final'||ids.has(entry.taskId)){entry.stale=true;entry.invalidatedBy=structuredClone(changedVersions);}
37
+ for(const intent of next.intents??[])if(['check','review'].includes(intent.operation)&&(intent.scope==='final'||ids.has(intent.taskId)))intent.stale=true;
38
+ // Old approval statements remain immutable; no change creates renewed authority.
39
+ if(Number.isSafeInteger(changedVersions.scopeVersion))next.scopeVersion=changedVersions.scopeVersion;
40
+ return next;
41
+ }
42
+ function record({state,request,evidence}){
43
+ const entry=request.evidence,d=entry?.data;
44
+ if(!entry||!text(entry.messageRef)||!d)return hold(state,'verification-record','Structured evidence required');
45
+ if(entry.kind==='acceptance-evidence'&&d.kind==='task'){
46
+ const t=d.task,previous=state.tasks.find(task=>task.id===t?.id),a=approval(state,t?.id);
47
+ if(previous?.invalidated&&(!a||!Number.isSafeInteger(a.revision)||a.revision<previous.invalidatedAt||a.scopeVersion!==state.scopeVersion))return hold(state,'task-approval-stale','Invalidated task requires a renewed approval statement');
48
+ if(!fields(t,['id','paths','criteria','requiredChecks','dependencies','approvalRef','approvalVersion','documentVersion','scopeVersion','targetVersion','status'])||!text(t.id)||!list(t.paths)||!t.paths.length||!t.paths.every(p=>absolute(p)&&within(state.target.realpath,p))||!list(t.criteria)||!t.criteria.length||!list(t.dependencies)||t.dependencies.includes(t.id)||t.dependencies.some(id=>!state.tasks.some(task=>task.id===id))||!Array.isArray(t.requiredChecks)||!t.requiredChecks.length||!t.requiredChecks.every(c=>descriptorValid(state,c,'check'))||!['pending',undefined].includes(t.status)||state.tasks.some(task=>task.id===t.id&&['accepted','committed'].includes(task.status))||!authorized(state,t))return hold(state,'task-contract','Current approval and explicit pending task contract required');
49
+ return allow(state,{task:{...structuredClone(t),status:'pending',source:'operator-attested',messageRef:entry.messageRef}});
50
+ }
51
+ if(entry.kind!=='finding')return hold(state,'unsupported','Check and review outcomes require durable intent/result');
52
+ if(d.action==='open'){
53
+ const check=state.checks.find(c=>c.id===d.checkId&&!c.stale);
54
+ if(!fields(d,['action','id','checkId','message'])||!text(d.id)||!text(d.message)||state.findings.some(f=>f.id===d.id)||!check)return hold(state,'finding','Finding must link an existing check or review');
55
+ return allow(state,{finding:{id:d.id,checkId:check.id,scope:check.scope,taskId:check.taskId,origin:check.scope==='final'&&check.operation==='review'?'final-review':'check',status:'open',message:d.message,messageRef:entry.messageRef,source:'operator-attested'}});
56
+ }
57
+ const finding=state.findings.find(f=>f.id===d.id);
58
+ const affected=finding?.scope==='final'?state.tasks.map(t=>t.id):[finding?.taskId];
59
+ if(!fields(d,['action','id','actor','changeFingerprint'])||d.action!=='resolve'||!finding||finding.status==='closed'||!fields(d.actor,['id','role'])||d.actor.role!=='implementer'||!affected.some(id=>assignedImplementer(state,d.actor.id,id))||!hash(d.changeFingerprint)||d.changeFingerprint!==current(state,finding.scope,finding.taskId,evidence?.changes))return hold(state,'finding-resolution','Assigned Implementer correction at the current fingerprint required');
60
+ return allow(state,{finding:{...finding,status:'resolved',resolutionRef:entry.messageRef,resolutionActor:structuredClone(d.actor),changeFingerprint:d.changeFingerprint}});
61
+ }
62
+ /** Pure cooperative gate: verifies recorded consistency, never identity or semantic sufficiency. */
63
+ export function evaluateCooperativeVerification({state,request,evidence}={}){
64
+ if(state?.mode!=='plan')return hold(state,'unsupported','Only the complete spec and plan path is supported');
65
+ if(!request||request.expectedRevision!==state.revision)return hold(state,'revision','Stale verification request');
66
+ if(request.actor?.role!=='orchestrator')return hold(state,'verification-actor','Orchestrator statement required');
67
+ if(request.operation==='verification-record')return record({state,request,evidence});
68
+ if(request.operation==='verification-result')return outcome({state,request,evidence});
69
+ if(!['check','review'].includes(request.operation))return hold(state,'unsupported','Unsupported verification operation');
70
+ const a=request.arguments,t=state.tasks.find(t=>t.id===request.taskId);
71
+ if(!fields(a,['scope','actor','descriptor','changeFingerprint','findingIds'])||!['task','final'].includes(a.scope)||!hash(a.changeFingerprint)||a.changeFingerprint!==current(state,a.scope,request.taskId,evidence?.changes)||!descriptorValid(state,a.descriptor,request.operation))return hold(state,'verification-fingerprint','Exact scope descriptor and current fingerprint required');
72
+ if(a.scope==='task'&&(!authorized(state,t)||t.status==='held'||request.operation==='review'||!t.requiredChecks.some(d=>same(d,a.descriptor))))return hold(state,'verification-task','Current task approval and exact assigned check required; Reviewer is final only');
73
+ if(a.scope==='final'&&(request.taskId!==undefined||request.operation!=='review'||!state.tasks.length||state.tasks.some(t=>!authorized(state,t)||!['accepted','committed'].includes(t.status))))return hold(state,'verification-final','Final review follows accepted tasks and required task checks');
74
+ const finalCorrection=a.scope==='final'&&state.findings.some(f=>f.origin==='final-review'&&f.status==='resolved'&&a.findingIds?.includes(f.id));
75
+ if(a.scope==='final'&&!finalCorrection&&state.tasks.some(task=>task.requiredChecks.some(descriptor=>!state.checks.some(c=>c.scope==='task'&&c.taskId===task.id&&c.operation==='check'&&!c.stale&&c.status==='passed'&&c.verdict==='proven'&&text(c.resultRef)&&c.verificationActor?.role==='qa'&&same(c.descriptor,descriptor)&&(c.changeFingerprint===evidence.changes?.tasks?.[task.id]||task.status==='committed'&&state.commits?.some(commit=>commit.taskId===task.id&&commit.observationSource==='local-git'&&commit.fingerprint===c.changeFingerprint&&commit.postCommitFingerprint===evidence.changes?.tasks?.[task.id]))))))return hold(state,'verification-final-checks','Initial final review requires current task QA evidence');
76
+ if(a.actor?.role!==(request.operation==='review'?'reviewer':'qa')||a.actor.id===request.actor.id||!independent(state,a.actor,request.taskId))return hold(state,'verification-independence','Independent actor statement and fresh task QA required');
77
+ const findings=state.findings.filter(f=>f.status!=='closed'&&(a.scope==='final'||f.scope==='task'&&f.taskId===request.taskId));
78
+ if(a.findingIds!==undefined&&(!list(a.findingIds)||a.findingIds.some(id=>!findings.some(f=>f.id===id))))return hold(state,'finding-recheck','Unknown finding recheck');
79
+ if(findings.some(f=>f.status!=='resolved'||!text(f.resolutionRef)||f.resolutionActor?.role!=='implementer'||f.changeFingerprint!==a.changeFingerprint||!a.findingIds?.includes(f.id)||f.origin==='final-review'&&request.operation!=='review'||f.origin!=='final-review'&&request.operation!=='check'))return hold(state,'finding-recheck','Implementer resolution and the required QA or final Reviewer recheck required');
80
+ if(state.intents.some(i=>['check','review'].includes(i.operation)&&['prepared','unknown','mismatch'].includes(i.status)&&i.scope===a.scope&&i.taskId===request.taskId&&same(i.descriptor,a.descriptor)&&!i.stale))return hold(state,'verification-active','Reconcile the existing verification intent');
81
+ const intent={requestId:request.requestId,operation:request.operation,status:'prepared',scope:a.scope,taskId:request.taskId,verificationActor:structuredClone(a.actor),descriptor:structuredClone(a.descriptor),changeFingerprint:a.changeFingerprint,findingIds:[...(a.findingIds??[])],scopeVersion:state.scopeVersion,targetVersion:state.targetVersion,versions:structuredClone(state.versions),target:structuredClone(state.target),approval:structuredClone(approval(state,request.taskId)),taskFingerprint:fingerprintRequest(a.scope==='final'?state.tasks:t),fingerprint:fingerprintRequest(request),source:'operator-attested'};
82
+ return allow(state,{intent});
83
+ }
84
+ function outcome({state,request,evidence}){
85
+ const intent=state.intents.find(i=>i.requestId===request.intentId),r=request.result,d=r?.data;
86
+ if(!intent||!['check','review'].includes(intent.operation)||!['prepared','unknown','mismatch'].includes(intent.status))return hold(state,'verification-intent','Matching unresolved verification intent required');
87
+ const valid=r&&['succeeded','failed','unknown','mismatch'].includes(r.status)&&text(r.messageRef)&&fields(d,['actor','descriptor','scope','taskId','changeFingerprint','verdict'])&&['proven','unmet','unverified'].includes(d.verdict)&&same(d.actor,intent.verificationActor)&&same(d.descriptor,intent.descriptor)&&d.scope===intent.scope&&d.taskId===intent.taskId&&d.changeFingerprint===intent.changeFingerprint;
88
+ const fresh=!intent.stale&&intent.changeFingerprint===current(state,intent.scope,intent.taskId,evidence?.changes)&&(intent.scope==='task'||intent.scopeVersion===state.scopeVersion)&&intent.targetVersion===state.targetVersion&&same(intent.target,state.target)&&same(intent.versions,state.versions)&&independent(state,intent.verificationActor,intent.taskId)&&same(intent.approval,approval(state,intent.taskId))&&intent.taskFingerprint===fingerprintRequest(intent.scope==='final'?state.tasks:state.tasks.find(t=>t.id===intent.taskId));
89
+ const status=!valid?'mismatch':r.status;
90
+ const check={id:intent.requestId,operation:intent.operation,scope:intent.scope,taskId:intent.taskId,descriptor:structuredClone(intent.descriptor),verificationActor:structuredClone(intent.verificationActor),changeFingerprint:intent.changeFingerprint,resultRef:r?.messageRef,verdict:valid?d.verdict:'unverified',status:fresh&&status==='succeeded'&&d.verdict==='proven'?'passed':status==='failed'||d?.verdict==='unmet'?'failed':'unverified',stale:!fresh,source:'operator-attested'};
91
+ const findings=[];
92
+ if(check.status==='failed')findings.push({id:intent.requestId+':failure',checkId:check.id,scope:intent.scope,taskId:intent.taskId,origin:intent.operation==='review'?'final-review':'check',status:'open',source:'operator-attested',messageRef:r.messageRef});
93
+ if(check.status==='passed')for(const id of intent.findingIds){const f=state.findings.find(f=>f.id===id);if(!f||f.status!=='resolved'||f.changeFingerprint!==intent.changeFingerprint)return hold(state,'finding-recheck','Finding changed after prepare');findings.push({...f,status:'closed',recheckRef:r.messageRef,recheckActor:structuredClone(d.actor)});}
94
+ const transition={result:{...structuredClone(r),status,source:'operator-attested'},check,findings};
95
+ return check.status==='passed'?allow(state,transition):{...hold(state,'verification-'+status,'Recorded outcome does not establish passing current evidence'),proposedTransition:transition};
96
+ }
97
+ function defaultRunner({cwd,args}){
98
+ const env=Object.fromEntries(Object.entries(process.env).filter(([k])=>!k.toUpperCase().startsWith('GIT_')));
99
+ Object.assign(env,{GIT_OPTIONAL_LOCKS:'0',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
100
+ return spawnSync('git',['--no-pager','--literal-pathspecs','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'buffer',timeout:10000,maxBuffer:16*1024*1024});
101
+ }
102
+ /** Read tracked/untracked regular bytes, deletions and modes without staging or execution.
103
+ * The snapshot is cooperative evidence at observation time, not an atomic execution sandbox. */
104
+ export async function readCooperativeChanges({state,gitRunner=defaultRunner}){
105
+ const inventory=await gitRunner({cwd:state.target.realpath,args:['ls-files','--cached','--others','--exclude-standard','-z']});
106
+ if(inventory?.status!==0||inventory.error||!Buffer.isBuffer(inventory.stdout))throw Error('Verification inventory query failed');
107
+ const raw=new TextDecoder('utf-8',{fatal:true}).decode(inventory.stdout);
108
+ if(raw&&!raw.endsWith('\0'))throw Error('Incomplete verification inventory');
109
+ const index=await gitRunner({cwd:state.target.realpath,args:['ls-files','--stage','-z']});
110
+ if(index?.status!==0||index.error||!Buffer.isBuffer(index.stdout))throw Error('Verification index query failed');
111
+ const indexText=new TextDecoder('utf-8',{fatal:true}).decode(index.stdout),indexRows=[];
112
+ if(indexText&&!indexText.endsWith('\0'))throw Error('Incomplete verification index');
113
+ for(const entry of indexText?indexText.slice(0,-1).split('\0'):[]){const match=/^(100644|100755) ([a-f0-9]{40}|[a-f0-9]{64}) 0\t([\s\S]+)$/.exec(entry);if(!match)throw Error('Verification requires regular stage-0 index entries');indexRows.push({path:path.join(state.target.realpath,match[3]),entry});}
114
+ const names=[...new Set(raw?raw.slice(0,-1).split('\0'):[])].sort(),rows=[];
115
+ for(const name of names){
116
+ if(!name||name.includes('\\')||name.includes('\n')||name.includes('\r')||path.isAbsolute(name)||name.split('/').some(p=>!p||p==='.'||p==='..'))throw Error('Invalid verification path');
117
+ const file=path.join(state.target.realpath,...name.split('/'));
118
+ try{await inspectPath(file);const stat=await fs.lstat(file);if(!stat.isFile())throw Error('Verification requires regular files');rows.push({path:file,mode:stat.mode&0o777,hash:'sha256:'+createHash('sha256').update(await fs.readFile(file)).digest('hex')});}
119
+ catch(error){if(error.code!=='ENOENT')throw error;rows.push({path:file,deleted:true});}
120
+ }
121
+ const tasks={},taskSnapshots={};for(const task of state.tasks){if(!list(task.paths)||!task.paths.every(p=>absolute(p)&&within(state.target.realpath,p)))throw Error('Invalid task paths');taskSnapshots[task.id]={scope:'task',taskId:task.id,target:Object.fromEntries(Object.entries(state.target).filter(([key])=>key!=='head')),paths:task.paths,index:indexRows.filter(row=>task.paths.some(p=>within(p,row.path))),rows:rows.filter(row=>task.paths.some(p=>within(p,row.path)))};tasks[task.id]=fingerprintRequest(taskSnapshots[task.id]);}
122
+ return {tasks,taskSnapshots,final:fingerprintRequest({scope:'final',target:state.target,index:indexRows,rows})};
123
+ }
@@ -0,0 +1,41 @@
1
+ import {createTerminalSession} from './terminal.mjs';
2
+ import {escapeTerminalText} from './render.mjs';
3
+ import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
4
+ import {collectStatus} from '../management/status.mjs';
5
+ import {runSetup,summary} from './setup.mjs';
6
+ import {runWorkflow,workflowRows} from './workflow.mjs';
7
+ const home=['Install / Reconfigure','Change Preset','Workflow','Update Harness','Uninstall Harness','Exit'];
8
+ function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
9
+ function operationRows(prepared){
10
+ if(prepared.kind==='workflow')return['Command: '+prepared.command,'Workspace: '+prepared.cwd,'Workflow: '+prepared.workflowId,'Confirmation does not supply runtime approval evidence.'];
11
+ const rows=['Operation: '+prepared.kind];if(prepared.options)rows.push('Target: '+prepared.options.target,...(prepared.kind==='install'?['Settings: '+summary(prepared.options),'External references must be downloaded after applying.']:[]));
12
+ for(const plan of prepared.plans??[]){rows.push('Target: '+(plan.target?.platform??plan.platform??'harness'),'Destination: '+(plan.target?.home??plan.home??''));for(const key of ['managedPaths','writes','deletes','removals','preserved','preserve']){const values=plan[key];if(Array.isArray(values))for(const item of values)rows.push(key+': '+(typeof item==='string'?item:item.path??item.destination??item.relativePath??'managed file'));}for(const change of plan.globalChanges??[])rows.push('Global: '+change.path+' — '+(change.action??'managed ARC update'));for(const note of plan.summary??[])rows.push(note);}
13
+ if(prepared.kind==='uninstall')rows.push('Remove ARC harness and managed profiles.',prepared.options?.skipGlobal?'Preserve ARC global instruction blocks.':'Remove selected ARC global instruction blocks.','Preserve native concurrency, user files, other skills and workflow audit state.','CLI npm package is retained.');
14
+ if(prepared.kind==='update')rows.push('Installed settings are preserved.','Global CLI replacement ends this session.');
15
+ return rows;
16
+ }
17
+ export function createTuiInterface(session,{input,controller}={}){
18
+ 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;}
19
+ async function menu(title,rows,{selected=0,info=[],requiresConfirmation=false}={}){
20
+ while(true){const offset=Math.max(0,selected-7),visible=rows.slice(offset,offset+8);const screen={title,rows:[...info,...visible],selected:info.length+selected-offset,requiresConfirmation,hint:'Up/Down: select | Enter: continue | Esc: back'+(rows.length>8?' | '+(offset+1)+'-'+Math.min(offset+8,rows.length)+'/'+rows.length:'')};const rendered=session.render(screen);const e=await event();if(e.name==='up')selected=(selected+rows.length-1)%rows.length;else if(e.name==='down')selected=(selected+1)%rows.length;else if(e.name==='back')return null;else if(e.name==='enter'&&(!requiresConfirmation||rendered.applyEnabled))return selected;}
21
+ }
22
+ async function text(title,initial=''){let value=initial;while(true){session.render({title,rows:[value],hint:'Type or paste | Left: delete | Enter: accept | Esc/Backspace: back'});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='left')value=[...value].slice(0,-1).join('');else if(e.name==='back')return null;else if(e.name==='enter')return value;}}
23
+ async function page(title,rows,{requireRead=false}={}){let offset=0;while(true){const size=8,slice=rows.slice(offset,offset+size);const rendered=session.render({title,rows:slice,requiresConfirmation:requireRead,hint:`Rows ${offset+1}-${Math.min(offset+size,rows.length)} of ${rows.length} | Up/Down: scroll | Enter/Esc: back`});const e=await event();if(e.name==='down')offset=Math.min(Math.max(0,rows.length-size),offset+1);else if(e.name==='up')offset=Math.max(0,offset-1);else if(e.name==='back')return false;else if(e.name==='enter'&&(!requireRead||rendered.applyEnabled)){if(requireRead&&offset+size<rows.length){offset=Math.min(rows.length-size,offset+size);}else return true;}}}
24
+ async function review(title,prepared){const rows=operationRows(prepared);while(true){const choice=await menu('Review '+title,['Cancel','Edit','Apply','View change details','Validate (read only)'],{info:rows.slice(0,8),requiresConfirmation:true});if(choice===null||choice===0)return'cancel';if(choice===1)return'edit';if(choice===2){if(rows.length>8){if(!await page('Review all changes',rows,{requireRead:true}))continue;const confirm=await menu('Confirm '+title,['Cancel','Apply'],{info:['All details reviewed. Apply the prepared snapshot?'],requiresConfirmation:true});if(confirm!==1)continue;}return'apply';}if(choice===3)await page('Change details',rows);if(choice===4)await page('Validation',['Prepared plans validated without changes or downloads.','Execution rechecks the reviewed snapshot.','External downloads are checked only after applying.']);}}
25
+ async function busy(action){session.render({title:'Working',rows:['Operation in progress. Please wait.'],hint:'Ctrl+C: interrupt'});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();}}
26
+ return{menu,text,page,review,busy};
27
+ }
28
+ 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.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?['CLI replacement ended this session. Run arc again for further management.']:[])];}
29
+ export async function runTui(options={}){
30
+ 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);
31
+ 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;
32
+ while(true){const statuses=await api.collectStatus(context);const info=statuses.map(s=>s.platform+': '+s.state+(s.diagnostics?.length?' — '+s.diagnostics.map(e=>e.message).join('; '):''));const choice=await ui.menu('ARC Management',home,{selected,info});if(choice===null)continue;selected=choice;if(choice===5)return code;
33
+ try{if(choice===2){await runWorkflow({ui,cwd:context.cwd??process.cwd(),services:options.services,onResult:result=>{code=result.code;if(code===130){finalReport=workflowRows(result);throw interrupted();}}});continue;}
34
+ let prepared;if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:choice===0?'install':'preset',prepare:settings=>api.prepareOperation(context,settings)});
35
+ else if(choice===3){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));const decision=await ui.review('Update Harness',prepared);if(decision!=='apply')prepared=null;}
36
+ else{const target=await ui.menu('Uninstall target',['Codex','Claude','Both targets']);if(target===null)continue;const global=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks']);if(global===null)continue;prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));const decision=await ui.review('Uninstall Harness',prepared);if(decision!=='apply')prepared=null;}
37
+ if(!prepared)continue;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;if(code===130){finalReport=resultRows(result);return 130;}if(result.exitSession){finalReport=resultRows(result);session.render({title:'Result',rows:finalReport,hint:'Session ended'});return code;}await ui.page('Result',resultRows(result));
38
+ }catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;code=error.code==='ARC_CLI_INPUT'?2:1;await ui.page('Operation unavailable',[error.message,'No unreviewed changes were applied.']);}
39
+ }
40
+ }catch(error){if(error.code==='ARC_CANCELLED')return 130;if(error.code==='ARC_TUI_EOF')return code;if(error.code==='ARC_TUI_TERMINAL')return 2;throw error;}finally{signals.removeListener('SIGINT',abort);signals.removeListener('SIGTERM',abort);session?.dispose();if(finalReport)(options.output??process.stdout).write(finalReport.map(row=>escapeTerminalText(row)).join('\n')+'\n');}
41
+ }
@@ -0,0 +1,21 @@
1
+ // Treat all screen content as data. Only terminal.mjs emits terminal commands.
2
+ export function escapeTerminalText(value=''){
3
+ return String(value).replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu,character=>{
4
+ const code=character.codePointAt(0);return code<=255?'\\x'+code.toString(16).padStart(2,'0'):'\\u{'+code.toString(16)+'}';
5
+ });
6
+ }
7
+ function width(character){return /[\p{Mark}]/u.test(character)?0:character.codePointAt(0)>=0x1100?2:1;}
8
+ function fit(text,columns){let used=0,result='';for(const character of text){const size=width(character);if(used+size>columns)return{text:result,complete:false};result+=character;used+=size;}return{text:result,complete:true};}
9
+ export function renderScreen(screen={}, {columns=80,rows=24}={}){
10
+ // Leave the last column unused to avoid terminal autowrap at the right edge.
11
+ columns=Math.max(1,Math.floor(Number(columns)||80)-1);rows=Math.max(1,Math.floor(Number(rows)||24));
12
+ const entries=screen.rows??[],hint=screen.hint??'Up/Down: select | Enter: continue | Esc: back';
13
+ const lines=[escapeTerminalText(screen.title),...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),escapeTerminalText(hint)];
14
+ const fitted=lines.map(line=>fit(line,columns));
15
+ const required=screen.requiredRows??entries.map((_,index)=>index);
16
+ const requiredVisible=required.every(index=>Number.isInteger(index)&&index>=0&&index<entries.length&&index+1<rows-1&&fitted[index+1].complete);
17
+ const applyEnabled=!screen.requiresConfirmation||(requiredVisible&&fitted[0].complete&&lines.length<=rows&&fitted.at(-1).complete);
18
+ const visible=fitted.slice(0,rows).map(line=>line.text);
19
+ if(!applyEnabled)visible[visible.length-1]=fit('Resize terminal to review before applying.',columns).text;
20
+ return{text:visible.join('\n'),applyEnabled};
21
+ }
@@ -0,0 +1,20 @@
1
+ import {parsePositiveLimit} from '../limits.mjs';
2
+ 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}];}));}
3
+ 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};}
4
+ export async function runSetup({ui,statuses,draft,kind,prepare}){
5
+ for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
6
+ while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Reconfigure',['Codex','Claude','Both targets']);if(targetIndex===null)return null;const target=['codex','claude','all'][targetIndex];
7
+ if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
8
+ const choice=await ui.menu('Target settings differ',['Back','Use Codex values for both','Use Claude values for both'],{info:['Codex: '+summary(draft.codex),'Claude: '+summary(draft.claude),'Choosing common values replaces the other target values.']});if(choice===null||choice===0)continue;const source=choice===1?'codex':'claude';draft.codex={...draft[source]};draft.claude={...draft[source]};
9
+ }
10
+ const settings=draft[target==='all'?'codex':target];let editing=true;
11
+ while(editing){const labels=kind==='preset'?['Preset: '+settings.preset,'Review changes']:['Preset: '+settings.preset,'Harness language: '+settings.language,'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Review changes'];const field=await ui.menu('Settings — '+target,labels);if(field===null)break;
12
+ if(field===0){const value=await ui.menu('Performance preset',['Economy','Balanced','Smart'],{selected:['economy','balanced','smart'].indexOf(settings.preset)});if(value!==null)settings.preset=['economy','balanced','smart'][value];}
13
+ else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['English','Korean'],{selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
14
+ else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';const raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',String(settings[key]));if(raw!==null)try{settings[key]=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);}}
15
+ else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
16
+ else{if(target==='all')draft.claude={...settings};const prepared=await prepare({kind,...setupOptions(draft,target)});const action=await ui.review(kind==='preset'?'Change Preset':'Install / Reconfigure',prepared);if(action==='apply'||action===true)return prepared;if(action==='cancel')return null;}
17
+ }
18
+ }
19
+ }
20
+ export function summary(s){return`preset=${s.preset}, language=${s.language}, total=${s.maxTotalSubagents}, concurrent=${s.maxConcurrentSubagents}, global=${!s.skipGlobal}`;}
@@ -0,0 +1,71 @@
1
+ import {StringDecoder} from 'node:string_decoder';
2
+ import {renderScreen} from './render.mjs';
3
+
4
+ /** Events are {name, sequence, ...detail}. Names: up/down/left/right, enter,
5
+ * back, text/paste (text), resize (columns,rows), blocked, eof, interrupt
6
+ * (signal), error (error). Every data chunk gets a sequence; paste is one event.
7
+ * render/discardInput/setBusy invalidate buffered input. While busy all data is
8
+ * discarded. Only one nextEvent waiter is allowed. dispose is idempotent,
9
+ * restores owned terminal state, and settles a waiting reader with eof. Shutdown
10
+ * discards unread events; a chunk is fully decoded before resolving a reader,
11
+ * so an Enter followed by EOF in that chunk cannot escape shutdown.
12
+ */
13
+ export function createTerminalSession({input=process.stdin,output=process.stdout,signals=process}={}){
14
+ if(!input.isTTY||!output.isTTY)throw Object.assign(Error('An interactive terminal is required.'),{code:'ARC_TUI_TERMINAL'});
15
+ const wasRaw=Boolean(input.isRaw),wasFlowing=input.readableFlowing;
16
+ let disposed=false,busy=false,parsing=false,pending='',paste=null,sequence=0,escapeTimer,waiter,queue=[],screen,lastLines=0,applyEnabled=true;
17
+ let decoder=new StringDecoder('utf8'),lastWasCR=false;
18
+ const clearTimer=()=>{clearTimeout(escapeTimer);escapeTimer=undefined;};
19
+ function deliver(name,detail={}){
20
+ const event={name,sequence,...detail};if(waiter&&!parsing){const resolve=waiter;waiter=undefined;resolve(event);}else queue.push(event);
21
+ }
22
+ function discardInput(){clearTimer();pending='';paste=null;decoder=new StringDecoder('utf8');queue=[];}
23
+ function dispose(){
24
+ if(disposed)return;disposed=true;clearTimer();pending='';paste=null;queue=[];
25
+ input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);
26
+ output.removeListener('resize',onResize);output.removeListener('error',onError);signals.removeListener('SIGINT',onSigint);signals.removeListener('SIGTERM',onSigterm);
27
+ try{if(input.setRawMode&&!input.destroyed)input.setRawMode(wasRaw);}finally{
28
+ try{if(wasFlowing!==true)input.pause();}finally{
29
+ try{output.write('\x1b[?2004l\x1b[?25h\n');}finally{if(waiter){const resolve=waiter;waiter=undefined;resolve({name:'eof',sequence});}}
30
+ }
31
+ }
32
+ }
33
+ function finish(name,detail){const resolve=waiter;waiter=undefined;dispose();if(resolve)resolve({name,sequence,...detail});else deliver(name,detail);}
34
+ function onEnd(){if(!disposed)finish('eof');}
35
+ function onError(error){if(!disposed)finish('error',{error});}
36
+ function onSigint(){if(!disposed)finish('interrupt',{signal:'SIGINT'});}
37
+ function onSigterm(){if(!disposed)finish('interrupt',{signal:'SIGTERM'});}
38
+ function draw(){
39
+ const result=renderScreen(screen,{columns:output.columns,rows:output.rows});applyEnabled=result.applyEnabled;
40
+ if(lastLines)output.write(`\x1b[${lastLines}A\r`);
41
+ const lines=result.text.split('\n');
42
+ for(let index=0;index<Math.max(lastLines,lines.length);index++)output.write('\x1b[2K'+(lines[index]??'')+'\n');
43
+ if(lastLines>lines.length)output.write(`\x1b[${lastLines-lines.length}A\r`);
44
+ lastLines=lines.length;return result;
45
+ }
46
+ function render(value){if(disposed)return{applyEnabled:false,text:''};discardInput();screen=value;try{return draw();}catch(error){dispose();throw error;}}
47
+ function onResize(){if(disposed)return;discardInput();try{if(screen)draw();deliver('resize',{columns:output.columns,rows:output.rows});}catch(error){onError(error);}}
48
+ function parse(){
49
+ while(pending&&!disposed){
50
+ if(paste!==null){const end=pending.indexOf('\x1b[201~');if(end<0){const tail=pending.lastIndexOf('\x1b');if(tail<0){paste+=pending;pending='';}else{paste+=pending.slice(0,tail);pending=pending.slice(tail);}return;}paste+=pending.slice(0,end);pending=pending.slice(end+6);deliver('paste',{text:paste});paste=null;continue;}
51
+ if(pending.startsWith('\x1b[200~')){pending=pending.slice(6);paste='';continue;}
52
+ const arrow=/^\x1b(?:\[|O)([ABCD])/.exec(pending);
53
+ if(arrow){pending=pending.slice(arrow[0].length);deliver({A:'up',B:'down',C:'right',D:'left'}[arrow[1]]);continue;}
54
+ if(pending[0]==='\x1b'){
55
+ if(pending==='\x1b'||['\x1b[','\x1bO','\x1b[2','\x1b[20','\x1b[200'].includes(pending)){escapeTimer=setTimeout(()=>{escapeTimer=undefined;pending='';deliver('back');},35);return;}
56
+ // Unknown escape sequences are consumed as text, never executed as keys.
57
+ const unknown=/^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(pending);if(unknown){pending=pending.slice(unknown[0].length);continue;}
58
+ pending=pending.slice(1);deliver('back');continue;
59
+ }
60
+ const character=[...pending][0];pending=pending.slice(character.length);const skipLF=lastWasCR&&character==='\n';lastWasCR=character==='\r';if(skipLF)continue;
61
+ if(character==='\x03'){onSigint();return;}if(character==='\x04'){onEnd();return;}
62
+ if(character==='\r'||character==='\n')deliver(applyEnabled?'enter':'blocked');
63
+ else if(character==='\x7f'||character==='\b')deliver('back');
64
+ else if(!/[\p{Cc}]/u.test(character))deliver('text',{text:character});
65
+ }
66
+ }
67
+ function onData(chunk){if(disposed||busy)return;sequence++;clearTimer();pending+=typeof chunk==='string'?chunk:decoder.write(chunk);parsing=true;try{parse();}finally{parsing=false;if(waiter&&queue.length){const resolve=waiter;waiter=undefined;resolve(queue.shift());}}}
68
+ input.on('data',onData);input.on('end',onEnd);input.on('close',onEnd);input.on('error',onError);output.on('resize',onResize);output.on('error',onError);signals.on('SIGINT',onSigint);signals.on('SIGTERM',onSigterm);
69
+ try{input.setRawMode?.(true);input.resume();output.write('\x1b[?25l\x1b[?2004h');}catch(error){dispose();throw error;}
70
+ return{render,discardInput,dispose,setBusy(value){busy=Boolean(value);discardInput();},nextEvent(){if(queue.length)return Promise.resolve(queue.shift());if(disposed)return Promise.resolve({name:'eof',sequence});if(waiter)return Promise.reject(Error('Only one terminal event reader is allowed.'));return new Promise(resolve=>{waiter=resolve;});}};
71
+ }
@@ -0,0 +1,12 @@
1
+ import * as workflowServices from '../management/workflow.mjs';
2
+ export async function runWorkflow({ui,cwd,services={},onResult=()=>{}}){
3
+ const api={...workflowServices,...services};let workspace=cwd;
4
+ while(true){let observed;try{observed=await api.observeWorkspace(workspace);}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;const selection=await ui.menu('Workflow workspace',['Back','Change workspace'],{info:['Workspace: '+workspace,'Cannot observe this repository. Select a valid repository root.']});if(selection!==1)return;const value=await ui.text('Absolute repository root',workspace);if(value!==null)workspace=value;continue;}workspace=observed.realpath;const selection=await ui.menu('Workflow workspace',['Continue','Change workspace'],{info:['Workspace: '+observed.realpath,'Branch: '+(observed.branch??'(detached)'),'HEAD: '+observed.head]});if(selection===null)return;if(selection===1){const value=await ui.text('Absolute repository root',workspace);if(value!==null)workspace=value;continue;}
5
+ let leave=false;while(!leave){const workflows=await api.listWorkflows(workspace);const pick=await ui.menu('Workflows',['Initialize workflow',...workflows.map(w=>w.id+(w.foreign?' — different target':w.invalid?' — invalid state':` — revision ${w.revision}`))]);if(pick===null)break;
6
+ let workflowId;if(pick===0){workflowId=await ui.text('Workflow ID','');if(workflowId===null)continue;const actor=await ui.text('Actor ID (operator attestation)','');if(actor===null)continue;const messageRef=await ui.text('Target selection message reference','');if(messageRef===null)continue;const contentHash=await ui.text('Target selection content hash (sha256:...)','');if(contentHash===null)continue;await submit('init',{mode:'plan',actor:{id:actor},selection:{messageRef,contentHash}});}
7
+ else{workflowId=workflows[pick-1].id;while(true){const action=await ui.menu('Workflow — '+workflowId,['Refresh status','Record evidence','Prepare task / operation','Record result','Reconcile']);if(action===null)break;const command=['status','record','prepare','result','reconcile'][action];if(command==='status')await submit(command);else{const jsonPath=await ui.text('Absolute regular JSON file (payload stays private)','');if(jsonPath!==null)await submit(command,undefined,jsonPath);}}}
8
+ async function submit(command,input,jsonPath){try{const prepared=await api.prepareWorkflow({cwd:workspace,workflowId,command,input,jsonPath});const review=await ui.review('Workflow '+command,{kind:'workflow',command,cwd:workspace,workflowId});if(review!=='apply'&&review!==true)return;const result=await ui.busy?.(()=>api.executeWorkflow(prepared))??await api.executeWorkflow(prepared);onResult(result);await ui.page('Workflow '+String(result.decision).toUpperCase(),workflowRows(result));}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;await ui.page('Workflow input rejected',['Check absolute paths, JSON schema, revision and matching checkout.','Payload and error details are hidden to protect secrets.']);}}
9
+ }
10
+ }
11
+ }
12
+ export function workflowRows(result){return['Decision: '+result.decision+' (exit '+result.code+')','Workflow: '+result.workflowId,'Revision: '+result.revision,...(result.target?['Workspace: '+result.target.realpath,'HEAD: '+result.target.head]:[]),...(result.ruleIds??[]).map(r=>'Rule: '+r),...(result.tasks??[]).map(t=>'Task: '+t.id+' — '+t.status),...(result.unresolved??[]),...(result.requiredEvidence??[]).map(r=>'Required: '+r),...(result.nextActions??[]).map(r=>'Next: '+r),...(result.error?[result.error]:[])];}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -1,4 +1,2 @@
1
- import {fileURLToPath} from 'node:url';
2
1
  import {runPostinstall} from '../lib/cli/postinstall.mjs';
3
- const packageRoot=fileURLToPath(new URL('../',import.meta.url));
4
- process.exitCode=await runPostinstall({packageRoot,cwd:packageRoot});
2
+ process.exitCode=await runPostinstall({});
@@ -1,6 +1,8 @@
1
+ import {RUNTIME_MODULE_PATHS} from '../lib/installer/runtime-assets.mjs';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import {fileURLToPath} from 'node:url';
5
+ import {COOPERATIVE_RUNTIME_MODULES} from '../lib/runtime/bootstrap.mjs';
4
6
  const args=process.argv.slice(2);const i=args.indexOf('--root');
5
7
  const root=i<0?fileURLToPath(new URL('../',import.meta.url)):path.resolve(args[i+1]);
6
8
  const check=args.includes('--check');
@@ -11,7 +13,10 @@ function safe(filename,directory=false){
11
13
  }
12
14
  function sources(relative){const dir=path.join(root,relative);safe(dir,true);const result=[];for(const entry of fs.readdirSync(dir).sort()){const rel=path.join(relative,entry);const file=path.join(root,rel);safe(file,fs.lstatSync(file).isDirectory());if(fs.lstatSync(file).isDirectory())result.push(...sources(rel));else if(entry.endsWith('.mjs'))result.push(rel);else throw Error(`Unexpected canonical runtime file: ${rel}`);}return result;}
13
15
  try{
14
- safe(root,true);const entries=sources('lib/runtime').map(name=>[path.relative('lib/runtime',name),fs.readFileSync(path.join(root,name))]);
16
+ safe(root,true);const modules=sources('lib/runtime').map(name=>name.split(path.sep).join('/')).sort();
17
+ for(const name of COOPERATIVE_RUNTIME_MODULES)if(!modules.includes(`lib/runtime/${name}`))throw Error(`Missing runtime module: ${name}`);
18
+ if(modules.length!==RUNTIME_MODULE_PATHS.length||modules.some((name,index)=>name!==RUNTIME_MODULE_PATHS[index]))throw Error('Runtime asset inventory does not match canonical modules');
19
+ const entries=modules.map(name=>[name.slice('lib/runtime/'.length),fs.readFileSync(path.join(root,name))]);
15
20
  const policy=path.join(root,'lib/policy/role-policy.json');safe(policy);entries.push(['role-policy.json',fs.readFileSync(policy)]);
16
21
  const outputs=[];for(const platform of ['codex','claude'])for(const [name,bytes]of entries){const filename=path.join(root,platform,'skill/runtime',name);safe(filename);outputs.push({filename,bytes});}
17
22
  let drift=false;for(const {filename,bytes}of outputs){if(!fs.existsSync(filename)||!fs.readFileSync(filename).equals(bytes)){drift=true;if(!check){fs.mkdirSync(path.dirname(filename),{recursive:true});fs.writeFileSync(filename,bytes);}}}
@@ -1,47 +0,0 @@
1
- import readline from 'node:readline';
2
- import {EventEmitter} from 'node:events';
3
- function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
4
- function cancelled(){return Object.assign(new Error('Installation cancelled.'),{code:'ARC_CANCELLED'});}
5
- export async function selectChoices(options,{input=process.stdin,output=process.stdout,targetOnly=false,languageDefault}={}){
6
- if(languageDefault===undefined&&options.target&&(targetOnly||options.preset)||languageDefault!==undefined&&options.language)return options;
7
- if(!input.isTTY||!output.isTTY)throw inputError(targetOnly?'Non-interactive execution requires --target':'Non-interactive execution requires --target and --preset');
8
- const menus=languageDefault!==undefined?[['language',['English','한국어'],['en','ko'],languageDefault==='ko'?1:0]]:[['target',['Claude','Codex','All'],['claude','codex','all'],0],['preset',['Economy','Balanced','Smart'],['economy','balanced','smart'],1]].filter(([key])=>!options[key]&&(!targetOnly||key==='target'));
9
- const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
10
- let menu=0,index=menus[0][3];
11
- const draw=(redraw=false)=>{const [key,labels]=menus[menu];if(redraw)output.write(`\x1b[${labels.length}A\r`);else output.write(`Select ${key==='target'?'an installation target':key==='language'?'an output language':'a performance preset'} [Up/Down]:\n`);output.write(labels.map((label,i)=>`\x1b[2K${i===index?'>':' '} ${label}`).join('\n')+'\n');};
12
- const keys=new EventEmitter();let onKey,onData,onEnd,onError,abort;
13
- try{return await new Promise((resolve,reject)=>{
14
- abort=()=>reject(cancelled());onEnd=abort;onError=reject;
15
- onKey=(str,key={})=>{try{if(key.name==='c'&&key.ctrl||key.name==='d'&&key.ctrl)return abort();if(key.name==='up'||key.name==='down'){index=(index+(key.name==='up'?-1:1)+menus[menu][1].length)%menus[menu][1].length;draw(true);}else if(key.name==='return'||key.name==='enter'){const [name,,values]=menus[menu];options[name]=values[index];output.write('\n');if(++menu===menus.length){keys.removeListener('keypress',onKey);resolve(options);}else{index=menus[menu][3];draw();}}}catch(error){reject(error);}};
16
- keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);
17
- if(input.setRawMode)input.setRawMode(true);input.resume();draw();
18
- });}finally{
19
- keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
20
- for(const [event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
21
- if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
22
- }
23
- }
24
-
25
- export async function selectLimits(options,selections,{input=process.stdin,output=process.stdout}={}){
26
- const{parsePositiveLimit}=await import('../limits.mjs');
27
- const fields=[['maxTotalSubagents','max_total_subagents',100,'total'],['maxConcurrentSubagents','max_concurrent_subagents',5,'concurrent']].filter(([key])=>options[key]===undefined);
28
- if(!fields.length)return options;
29
- const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
30
- let index=0,value='',onKey,onData,onEnd,onError,abort;
31
- const draw=()=>{const[,,fallback,label]=fields[index];output.write(`Maximum ${label} subagents [Default=${fallback}]: `);};
32
- try{return await new Promise((resolve,reject)=>{
33
- abort=()=>reject(cancelled());onEnd=abort;onError=reject;
34
- onKey=(str,key={})=>{if(key.ctrl&&(key.name==='c'||key.name==='d'))return abort();if(key.name==='return'||key.name==='enter'){
35
- const[name,,,label]=fields[index];if(value.trim()){try{options[name]=parsePositiveLimit(value,label);}catch(cause){output.write(`\n${cause.message}\n`);value='';draw();return;}}
36
- if(name==='maxConcurrentSubagents')options.concurrentSelected=true;output.write('\n');value='';if(++index===fields.length){input.removeListener('keypress',onKey);resolve(options);}else draw();
37
- }else if(key.name==='backspace'){value=value.slice(0,-1);output.write('\b \b');}else if(str&&!key.ctrl&&!key.meta){value+=str;output.write(str);}};
38
- onData=chunk=>{for(const character of chunk.toString()){if(index===fields.length)break;const name=character==='\r'||character==='\n'?'return':character==='\x03'?'c':character==='\x04'?'d':character==='\x7f'||character==='\b'?'backspace':undefined;onKey(character,{name,ctrl:character==='\x03'||character==='\x04'});}};
39
- input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
40
- });}finally{
41
- input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
42
- for(const[event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
43
- if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
44
- }
45
- }
46
-
47
- export async function selectLanguage(options,languageDefault,io={}){return selectChoices(options,{...io,languageDefault});}
@@ -1,55 +0,0 @@
1
- export function renderCompletion(shell){
2
- if(shell==='bash')return `_arc() {
3
- local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" command="\${COMP_WORDS[1]}" choices=""
4
- case "$prev" in
5
- --target) [[ $command == -* || $command == preset || $command == uninstall ]] && choices="claude codex all" ;;
6
- --preset) [[ $command == -* ]] && choices="economy balanced smart" ;;
7
- --language) [[ $command == -* ]] && choices="en ko" ;;
8
- *)
9
- if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall completion upgrade"
10
- else
11
- case "$command" in
12
- preset)
13
- if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then choices="economy balanced smart"
14
- else choices="--target --check --dry-run --help"; fi ;;
15
- uninstall) choices="--target --dry-run --skip-global --help --version" ;;
16
- upgrade) choices="--help" ;;
17
- completion) (( COMP_CWORD == 2 )) && choices="zsh bash" ;;
18
- -*) choices="--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version" ;;
19
- esac
20
- fi ;;
21
- esac
22
- COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
23
- }
24
- complete -F _arc arc
25
- `;
26
- if(shell==='zsh')return `_arc() {
27
- local -a candidates
28
- if (( CURRENT == 2 )) && [[ \${words[CURRENT]} != -* ]]; then
29
- candidates=(preset uninstall completion upgrade)
30
- else
31
- case "\${words[CURRENT-1]}" in
32
- --target) [[ \${words[2]} == -* || \${words[2]} == preset || \${words[2]} == uninstall ]] && candidates=(claude codex all) ;;
33
- --preset) [[ \${words[2]} == -* ]] && candidates=(economy balanced smart) ;;
34
- --language) [[ \${words[2]} == -* ]] && candidates=(en ko) ;;
35
- *) case "\${words[2]}" in
36
- preset)
37
- if (( CURRENT == 3 )) && [[ \${words[CURRENT]} != -* ]]; then candidates=(economy balanced smart)
38
- else candidates=(--target --check --dry-run --help); fi ;;
39
- uninstall) candidates=(--target --dry-run --skip-global --help --version) ;;
40
- upgrade) candidates=(--help) ;;
41
- completion) (( CURRENT == 3 )) && candidates=(zsh bash) ;;
42
- -*) candidates=(--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version) ;;
43
- esac ;;
44
- esac
45
- fi
46
- compadd -a candidates
47
- }
48
- if (( ! $+functions[compdef] )); then
49
- autoload -Uz compinit
50
- compinit -D
51
- fi
52
- compdef _arc arc
53
- `;
54
- throw new Error('Unsupported completion shell');
55
- }