@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.
- package/MANIFEST.sha256 +86 -88
- package/claude/agents/arc-implementer.md +32 -42
- package/claude/agents/arc-planner.md +33 -47
- package/claude/agents/arc-qa.md +28 -44
- package/claude/agents/arc-reviewer.md +28 -41
- package/claude/agents/arc-scouter.md +26 -38
- package/claude/skill/SKILL.md +41 -59
- package/claude/skill/models.toml +11 -11
- package/claude/skill/references/arc-visual-companion.md +19 -24
- package/claude/skill/references/brainstorming.md +111 -42
- package/claude/skill/references/commit.md +38 -70
- package/claude/skill/references/coordination.md +77 -126
- package/claude/skill/references/git-recovery.md +17 -35
- package/claude/skill/references/handoff-prep.md +91 -103
- package/claude/skill/references/handoff.md +110 -208
- package/claude/skill/references/limits.md +19 -31
- package/claude/skill/references/planning.md +93 -244
- package/claude/skill/references/superpowers-provenance.json +3 -3
- package/claude/skill/references/verification.md +105 -261
- package/claude/skill/runtime/adapters/claude.mjs +10 -9
- package/claude/skill/runtime/adapters/codex.mjs +7 -8
- package/claude/skill/runtime/bootstrap.mjs +3 -2
- package/claude/skill/runtime/capabilities.mjs +12 -0
- package/claude/skill/runtime/cooperative-schema.mjs +3 -2
- package/claude/skill/runtime/cooperative.mjs +52 -4
- package/claude/skill/runtime/evaluate.mjs +115 -2
- package/claude/skill/runtime/identity.mjs +22 -0
- package/claude/skill/runtime/role-policy.json +18 -18
- package/claude/skill/runtime/schema.mjs +19 -0
- package/claude/skill/scripts/model_config.py +20 -84
- package/codex/agents/arc-implementer.toml +34 -30
- package/codex/agents/arc-planner.toml +27 -23
- package/codex/agents/arc-qa.toml +24 -15
- package/codex/agents/arc-reviewer.toml +25 -16
- package/codex/agents/arc-scouter.toml +20 -26
- package/codex/skill/SKILL.md +42 -57
- package/codex/skill/models.toml +13 -13
- package/codex/skill/references/arc-visual-companion.md +21 -27
- package/codex/skill/references/brainstorming.md +111 -42
- package/codex/skill/references/commit.md +35 -81
- package/codex/skill/references/coordination.md +76 -104
- package/codex/skill/references/git-recovery.md +29 -51
- package/codex/skill/references/handoff-prep.md +76 -134
- package/codex/skill/references/handoff-worktree.md +32 -66
- package/codex/skill/references/handoff.md +111 -271
- package/codex/skill/references/limits.md +21 -34
- package/codex/skill/references/planning.md +125 -299
- package/codex/skill/references/superpowers-provenance.json +3 -3
- package/codex/skill/references/verification.md +90 -272
- package/codex/skill/runtime/adapters/claude.mjs +10 -9
- package/codex/skill/runtime/adapters/codex.mjs +7 -8
- package/codex/skill/runtime/bootstrap.mjs +3 -2
- package/codex/skill/runtime/capabilities.mjs +12 -0
- package/codex/skill/runtime/cooperative-schema.mjs +3 -2
- package/codex/skill/runtime/cooperative.mjs +52 -4
- package/codex/skill/runtime/evaluate.mjs +115 -2
- package/codex/skill/runtime/identity.mjs +22 -0
- package/codex/skill/runtime/role-policy.json +18 -18
- package/codex/skill/runtime/schema.mjs +19 -0
- package/codex/skill/scripts/model_config.py +25 -6
- package/lib/installer/global-config.mjs +2 -1
- package/lib/installer/manifest.mjs +1 -1
- package/lib/installer/plan.mjs +6 -8
- package/lib/installer/preset.mjs +7 -4
- package/lib/installer/uninstall.mjs +2 -2
- package/lib/management/operations.mjs +4 -2
- package/lib/management/update.mjs +3 -3
- package/lib/models/catalog-anthropic.mjs +46 -0
- package/lib/models/catalog.mjs +62 -0
- package/lib/models/codex-catalog.mjs +103 -0
- package/lib/models/config.mjs +19 -11
- package/lib/models/index.mjs +1 -1
- package/lib/models/profiles.mjs +1 -40
- package/lib/policy/render.mjs +1 -5
- package/lib/policy/role-policy.json +18 -18
- package/lib/runtime/adapters/claude.mjs +10 -9
- package/lib/runtime/adapters/codex.mjs +7 -8
- package/lib/runtime/bootstrap.mjs +3 -2
- package/lib/runtime/capabilities.mjs +12 -0
- package/lib/runtime/cooperative-schema.mjs +3 -2
- package/lib/runtime/cooperative.mjs +52 -4
- package/lib/runtime/evaluate.mjs +115 -2
- package/lib/runtime/identity.mjs +22 -0
- package/lib/runtime/schema.mjs +19 -0
- package/lib/tui/app.mjs +26 -16
- package/lib/tui/custom-preset.mjs +122 -0
- package/lib/tui/render.mjs +3 -2
- package/lib/tui/setup.mjs +65 -12
- package/package.json +1 -2
- package/claude/skill/hooks/git-policy.json +0 -208
- package/claude/skill/hooks/role-git-guard.mjs +0 -18
- package/claude/skill/hooks/runtime-gate.mjs +0 -12
- package/codex/skill/hooks/runtime-gate.mjs +0 -10
- package/lib/policy/claude-hook.mjs +0 -80
- package/scripts/probe-runtime-hooks.mjs +0 -55
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
import tomllib
|
|
@@ -77,10 +78,14 @@ PRESETS = ("economy", "balanced", "smart")
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def validate_config(config: dict, path: Path) -> dict:
|
|
80
|
-
if not isinstance(config, dict) or
|
|
81
|
-
raise _error(path, "settings", "update to version 1 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if not isinstance(config, dict) or type(config.get("version")) is not int or config["version"] not in (1, 2):
|
|
82
|
+
raise _error(path, "settings", "update to version 1 or 2 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
83
|
+
has_custom = "custom" in config
|
|
84
|
+
allowed = {"version", "preset", "roles"} | ({"custom"} if config["version"] == 2 and has_custom else set())
|
|
85
|
+
if set(config) != allowed:
|
|
86
|
+
raise _error(path, "settings", "update to version 1 or 2 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
|
|
87
|
+
if not isinstance(config["preset"], str) or config["preset"] not in (*PRESETS, "custom") or (config["preset"] == "custom" and not has_custom):
|
|
88
|
+
raise _error(path, "preset", "must select an available preset")
|
|
84
89
|
roles = config["roles"]
|
|
85
90
|
if not isinstance(roles, dict) or set(roles) != set(ROLE_FILES):
|
|
86
91
|
raise _error(path, "roles", "must define exactly the five ARC roles")
|
|
@@ -89,24 +94,38 @@ def validate_config(config: dict, path: Path) -> dict:
|
|
|
89
94
|
raise _error(path, f"roles.{role}", "must contain exactly economy, balanced and smart")
|
|
90
95
|
for preset, values in variants.items():
|
|
91
96
|
_settings_for_profile(values, f"{role}.{preset}", path)
|
|
92
|
-
|
|
97
|
+
if has_custom:
|
|
98
|
+
custom = config["custom"]
|
|
99
|
+
if not isinstance(custom, dict) or set(custom) != set(ROLE_FILES):
|
|
100
|
+
raise _error(path, "custom", "must define exactly the five ARC roles")
|
|
101
|
+
for role, values in custom.items():
|
|
102
|
+
_settings_for_profile(values, f"custom.{role}", path)
|
|
103
|
+
result = copy.deepcopy(config)
|
|
104
|
+
result["version"] = 2
|
|
105
|
+
return result
|
|
93
106
|
|
|
94
107
|
|
|
95
108
|
def load_settings(config_path: Path) -> dict[str, RoleSettings]:
|
|
96
109
|
"""Validate every preset and return the active role settings."""
|
|
97
110
|
_, _, config = _parse_file(config_path, "settings")
|
|
98
111
|
config = validate_config(config, config_path)
|
|
112
|
+
if config["preset"] == "custom":
|
|
113
|
+
return config["custom"]
|
|
99
114
|
return {role: dict(variants[config["preset"]]) for role, variants in config["roles"].items()}
|
|
100
115
|
|
|
101
116
|
|
|
102
117
|
def serialize_settings(config: dict) -> bytes:
|
|
103
118
|
"""Serialize a complete unified configuration without losing inactive presets."""
|
|
104
119
|
config = validate_config(config, Path("models.toml"))
|
|
105
|
-
lines = ["version =
|
|
120
|
+
lines = ["version = 2", f'preset = {json.dumps(config["preset"])}']
|
|
106
121
|
for role in ROLE_FILES:
|
|
107
122
|
for preset in PRESETS:
|
|
108
123
|
lines.extend(["", f"[roles.{role}.{preset}]"])
|
|
109
124
|
lines.extend(f"{key} = {json.dumps(config['roles'][role][preset][key], ensure_ascii=False)}" for key in _MODEL_FIELDS)
|
|
125
|
+
if "custom" in config:
|
|
126
|
+
for role in ROLE_FILES:
|
|
127
|
+
lines.extend(["", f"[custom.{role}]"])
|
|
128
|
+
lines.extend(f"{key} = {json.dumps(config['custom'][role][key], ensure_ascii=False)}" for key in _MODEL_FIELDS)
|
|
110
129
|
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
111
130
|
|
|
112
131
|
|
|
@@ -38,5 +38,6 @@ export function renderWaitSettings(original){
|
|
|
38
38
|
try{if(!isDeepStrictEqual(parseToml(updated),expected))return manual('automatic edit could not be verified');}catch{return manual('automatic edit could not be verified');}
|
|
39
39
|
return{content:Buffer.from(updated),action:'set ARC wait settings'};
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
export function agentsBlockBody(home,userHome,language='en',platform='codex'){const skill=home+'/skills/arc/SKILL.md',within=skill.startsWith(userHome+'/'),shown=within?'~/'+skill.slice(userHome.length+1):skill;return '- '+languagePolicy(language)+'\n'+'- For requests requiring substantive multi-step research, analysis, or implementation, configuration, or test work, and for design, spec, plan, and brainstorming requests (these are NOT standalone document writing), load and follow `'+shown+'`; keep following it in relevant follow-up turns of the same chat without re-reading it unless it changed.'+(within?" Resolve `~` to the current user's home directory.":'')+' ARC subagents (depth 2) follow their role profile and brief instead and do not load `SKILL.md`.\n'+'- Design and brainstorming drafts and first complete spec and plan drafts are authored only by the Planner; route drafting skills to the Planner. Orchestrator reviews and directly revises the same spec or plan when needed; a missing or failed first draft does not transfer initial authorship. New product decisions outside approved scope require user input first.\n'+
|
|
42
43
|
'- For repository implementation, configuration, or test work, Orchestrator owns coordination and acceptance; assign file edits and other work to the other roles according to their defined responsibilities. Orchestrator must not edit implementation, configuration, or test-code files directly, except under the ARC Orchestrator direct path for small, low-risk tasks.\n';}
|
|
@@ -40,7 +40,7 @@ export function verifyPayload({packageRoot,safeFs,payloadMode='auto'}){
|
|
|
40
40
|
for(const platform of ['codex','claude'])referenceTrees[platform]=validateFlatReferenceFiles(files,platform);
|
|
41
41
|
for(const{destination}of REFERENCE_PATH_MAP)if(!referenceTrees.codex.entries.get(destination).bytes.equals(referenceTrees.claude.entries.get(destination).bytes))throw Error('Imported platform parity mismatch: '+destination);
|
|
42
42
|
for(const name of listed.keys())if(!files.has(name))throw Error(`Manifest entry is missing: ${name}`);
|
|
43
|
-
const required=['
|
|
43
|
+
const required=['lib/policy/role-policy.json','lib/policy/render.mjs','scripts/update-role-policy.mjs','lib/limits.mjs','lib/installer/limits.mjs','lib/installer/runtime-config.mjs','README.md','install.command','package.json','bin/arc.mjs','lib/cli/root.mjs','lib/tui/app.mjs','lib/tui/terminal.mjs','lib/tui/render.mjs','lib/tui/setup.mjs','lib/management/operations.mjs','lib/management/status.mjs','lib/management/update.mjs','lib/management/workflow.mjs','lib/cli/postinstall.mjs','scripts/postinstall.mjs','lib/cli/upgrade.mjs','bin/arc-upgrade.mjs','lib/installer/shell-completion.mjs','lib/installer/manifest.mjs','lib/installer/targets.mjs','lib/installer/global-config.mjs','lib/installer/plan.mjs','lib/safe-fs/index.mjs','lib/safe-fs/native.mjs','lib/models/index.mjs','lib/models/config.mjs','lib/models/profiles.mjs','vendor/toml/LICENSE','vendor/toml/package.json','vendor/toml/provenance.json','vendor/toml/dist/index.js','prebuilds/index.json'];
|
|
44
44
|
required.push('lib/installer/runtime-assets.mjs',...RUNTIME_MODULE_PATHS);
|
|
45
45
|
for(const platform of ['codex','claude'])required.push(`${platform}/skill/runtime/role-policy.json`,...RUNTIME_MODULE_PATHS.map(name=>`${platform}/skill/runtime/${name.slice('lib/runtime/'.length)}`));
|
|
46
46
|
for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
|
package/lib/installer/plan.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {installClaudeHook} from '../policy/claude-hook.mjs';
|
|
2
|
-
import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
|
|
3
1
|
import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
|
|
4
2
|
import{resolveLimits}from'../limits.mjs';import{parseLimits,serializeLimits}from'./limits.mjs';import{readRuntimeConcurrency,renderCodexConcurrency,renderClaudeConcurrency}from'./runtime-config.mjs';import{assertPlanSnapshot}from'./transaction.mjs';
|
|
5
3
|
import{validateModelConfig}from'../models/config.mjs';
|
|
6
|
-
import
|
|
4
|
+
import{assertCatalogRoles}from'../models/catalog.mjs';
|
|
5
|
+
import path from'node:path';import{loadModelConfig,applyPreset,applyCustomRoles,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
|
|
7
6
|
function fail(message){throw new TargetConfigurationError(message);}
|
|
8
7
|
export function previewLimits({target,safeFs}){
|
|
9
8
|
const snapshot=new Map(),filename=path.join(target.home,'skills/arc/limits.toml');
|
|
@@ -17,10 +16,9 @@ export function previewLanguage({target,safeFs}){
|
|
|
17
16
|
let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
|
|
18
17
|
return{existing,snapshot};
|
|
19
18
|
}
|
|
20
|
-
function renderModelAssets({payload,target,preset,modelConfig,language}){
|
|
19
|
+
function renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog}){
|
|
21
20
|
const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
|
|
22
|
-
|
|
23
|
-
if(target.platform==='claude'){try{const script=payload.files.get('claude/skill/hooks/role-git-guard.mjs'),policy=payload.files.get('claude/skill/hooks/git-policy.json');if(!Buffer.isBuffer(script)||!script.length||!Buffer.isBuffer(policy))throw Error('Missing hook assets');validateGitPolicy(JSON.parse(policy.toString('utf8')));for(const[name,bytes]of profiles)profiles.set(name,installClaudeHook(bytes,path.join(target.home,'skills/arc')));}catch(cause){fail(`Invalid Claude Git guard: ${cause.message}`);}}
|
|
21
|
+
let config;if(modelConfig!==undefined)config=validateModelConfig(modelConfig);else{const base=loadModelConfig(input.models,target.platform);if(preset==='custom'){assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});config=applyCustomRoles(base,customRoles);}else {if(customRoles!==undefined)assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});config=applyPreset(customRoles!==undefined?applyCustomRoles(base,customRoles):preservedCustomRoles!==undefined?applyCustomRoles(base,preservedCustomRoles):base,preset);}}if(config.platform!==target.platform)fail('Model config platform does not match target');const profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
|
|
24
22
|
return{profiles,modelBytes};
|
|
25
23
|
}
|
|
26
24
|
function renderSkillPayload({payload,target,language,modelBytes,limits}){
|
|
@@ -48,14 +46,14 @@ function renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurre
|
|
|
48
46
|
}
|
|
49
47
|
return globalChanges;
|
|
50
48
|
}
|
|
51
|
-
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
|
|
49
|
+
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig,customRoles,preservedCustomRoles,verifiedCatalog}){
|
|
52
50
|
validateLanguage(language);
|
|
53
51
|
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
54
52
|
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
55
53
|
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
56
54
|
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
57
55
|
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
58
|
-
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language});
|
|
56
|
+
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog});
|
|
59
57
|
const skill=renderSkillPayload({payload,target,language,modelBytes,limits});
|
|
60
58
|
const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
|
|
61
59
|
for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
|
package/lib/installer/preset.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
3
|
-
import {loadModelConfig,applyPreset,serializeModelConfig,resolveModelSettings,renderProfiles} from '../models/index.mjs';
|
|
3
|
+
import {loadModelConfig,applyPreset,applyCustomRoles,serializeModelConfig,resolveModelSettings,renderProfiles} from '../models/index.mjs';
|
|
4
4
|
import {assertPlanSnapshot,applyTransaction} from './transaction.mjs';
|
|
5
|
+
import {assertCatalogRoles} from '../models/catalog.mjs';
|
|
5
6
|
import {validatePlanSet} from './plan.mjs';
|
|
6
|
-
export function buildPresetPlan({target,preset,safeFs}){
|
|
7
|
+
export function buildPresetPlan({target,preset,safeFs,customRoles,verifiedCatalog}){
|
|
7
8
|
const snapshot=new Map(),modelsPath=path.join(target.home,'skills/arc/models.toml'),managedPaths=[modelsPath,...target.profileNames.map(name=>path.join(target.home,'agents',name))];
|
|
8
9
|
const read=filename=>{const found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});if(!found.exists)throw new TargetConfigurationError(`Installed ARC file missing: ${filename}`);return found.bytes;};
|
|
9
|
-
const originalModels=read(modelsPath),config=loadModelConfig(originalModels,target.platform)
|
|
10
|
+
const originalModels=read(modelsPath),config=loadModelConfig(originalModels,target.platform);
|
|
11
|
+
if(preset==='custom'||customRoles!==undefined)assertCatalogRoles({platform:target.platform,roles:customRoles,catalog:verifiedCatalog});
|
|
12
|
+
const next=customRoles!==undefined?applyPreset(applyCustomRoles(config,customRoles),preset):applyPreset(config,preset),originalProfiles=new Map(target.profileNames.map((name,i)=>[name,read(managedPaths[i+1])]));
|
|
10
13
|
const profiles=renderProfiles(originalProfiles,resolveModelSettings(next)),specs=[];
|
|
11
|
-
if(config.preset!==preset)specs.push({path:modelsPath,bytes:Buffer.from(serializeModelConfig(next))});
|
|
14
|
+
if(config.preset!==preset||(customRoles!==undefined&&(!config.custom||Object.keys(next.custom).some(role=>Object.keys(next.custom[role]).some(key=>config.custom[role]?.[key]!==next.custom[role][key])))))specs.push({path:modelsPath,bytes:Buffer.from(serializeModelConfig(next))});
|
|
12
15
|
for(const[name,bytes]of profiles)if(!bytes.equals(originalProfiles.get(name)))specs.push({path:path.join(target.home,'agents',name),bytes});
|
|
13
16
|
return{target,preset,snapshot,managedPaths,externalPaths:[],specs};
|
|
14
17
|
}
|
|
@@ -4,7 +4,7 @@ import {SKILLS,snapshotSkill} from './superpowers.mjs';
|
|
|
4
4
|
import {validatePlanSet} from './plan.mjs';
|
|
5
5
|
import {removeManagedBlock} from './global-config.mjs';
|
|
6
6
|
import {attachShellCompletionCleanup} from './shell-completion.mjs';
|
|
7
|
-
export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
7
|
+
export function buildUninstallPlan({target,safeFs,skipGlobal=false,removeCompletion=true}){
|
|
8
8
|
const snapshot=new Map(),removals=[],managedPaths=[path.join(target.home,'skills/arc'),...target.profileNames.map(name=>path.join(target.home,'agents',name))],globalChanges=[];
|
|
9
9
|
for(const filename of managedPaths){const directory=filename===managedPaths[0];const found=inspectPath(safeFs,filename,{kind:directory?'directory':'file',tree:directory,allowLinks:directory,snapshot});if(found.exists)removals.push(filename);}
|
|
10
10
|
const linkedSkills=new Set(),arc=managedPaths[0];
|
|
@@ -16,7 +16,7 @@ export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
|
16
16
|
for(const skill of linkedSkills){const root=arc+'/references/superpowers/'+skill,directory=safeFs.openRoot(root);try{if(!sameIdentity(snapshot.get(root),directory.stat()))throw new TargetConfigurationError('Removal reference root changed');snapshotSkill(directory,root,[]);}finally{directory.close();}}
|
|
17
17
|
if(!skipGlobal){const filename=target.instructionPath??target.globalPaths[0],found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});managedPaths.push(filename);if(found.exists){const removed=removeManagedBlock(found.bytes);if(removed.changed)globalChanges.push({path:filename,content:removed.content});}}
|
|
18
18
|
const plan={target,skipGlobal,snapshot,removals,managedPaths,externalPaths:[],globalChanges,summary:[target.platform==='codex'?'Codex config.toml wait settings and native concurrency key are preserved.':'Claude settings.json native concurrency key is preserved.']};
|
|
19
|
-
attachShellCompletionCleanup([plan],{safeFs});return plan;
|
|
19
|
+
if(removeCompletion)attachShellCompletionCleanup([plan],{safeFs});return plan;
|
|
20
20
|
}
|
|
21
21
|
import {assertPlanSnapshot,applyUninstallTransaction} from './transaction.mjs';
|
|
22
22
|
export async function uninstallTargets(plans,{safeFs,signal,hooks={}}){
|
|
@@ -19,12 +19,13 @@ export async function createManagementContext(options={}){
|
|
|
19
19
|
export async function prepareOperation(context,options,{selections=[]}={}){
|
|
20
20
|
if(options.kind==='update'){const{prepareUpdate}=await import('./update.mjs');return prepareUpdate(context);}
|
|
21
21
|
if(!['install','preset','uninstall'].includes(options.kind))throw Object.assign(Error('Unknown management operation'),{code:'ARC_CLI_INPUT'});
|
|
22
|
+
if(options.kind==='uninstall')options={...options,removeCli:options.removeCli!==false};
|
|
22
23
|
const platforms=options.target==='all'?['codex','claude']:[options.target],payload=options.kind==='install'?verifyPayload(context):null;
|
|
23
|
-
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
24
|
+
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs,customRoles:options.customRoles,verifiedCatalog:options.verifiedCatalog});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal,removeCompletion:options.removeCli!==false});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,customRoles:options.customRoles,preservedCustomRoles:options.preservedCustomRoles,verifiedCatalog:options.verifiedCatalog,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
24
25
|
if(options.kind==='install')attachShellCompletionCleanup(plans,context);
|
|
25
26
|
// Uninstall plans initially share the completion snapshot; its executor assigns one owner.
|
|
26
27
|
if(options.kind!=='uninstall')validatePlanSet(plans);
|
|
27
|
-
const globalArc=options.kind==='uninstall'?await(context.inspectGlobalArc??inspectGlobalArc)({env:context.env,cwd:context.cwd,runNpm:context.runNpm}):undefined;
|
|
28
|
+
const globalArc=options.kind==='uninstall'&&options.removeCli!==false?await(context.inspectGlobalArc??inspectGlobalArc)({env:context.env,cwd:context.cwd,runNpm:context.runNpm}):undefined;
|
|
28
29
|
return{kind:options.kind,plans,options:{...options},...(globalArc?{globalArc}:{})};
|
|
29
30
|
}
|
|
30
31
|
export function operationResult(result,successStatuses=['installed']){return{...result,code:result.interrupted?130:result.results.every(item=>successStatuses.includes(item.status)&&!item.error)?0:1,exitSession:false};}
|
|
@@ -34,6 +35,7 @@ export async function executeOperation(context,prepared){
|
|
|
34
35
|
if(prepared.kind==='install')return operationResult(await installTargets(prepared.plans,options));
|
|
35
36
|
if(prepared.kind==='preset')return operationResult(await switchPresetTargets(prepared.plans,options),['updated','unchanged']);
|
|
36
37
|
if(prepared.kind==='uninstall'){
|
|
38
|
+
if(prepared.options?.removeCli===false)return operationResult(await uninstallTargets(prepared.plans,options),['uninstalled']);
|
|
37
39
|
let result;
|
|
38
40
|
try{
|
|
39
41
|
const current=await(context.inspectGlobalArc??inspectGlobalArc)({context:prepared.globalArc,env:context.env,cwd:context.cwd,runNpm:context.runNpm});
|
|
@@ -12,7 +12,7 @@ export async function runtime(deps){
|
|
|
12
12
|
return{...context,...state,...upgrade,...npm,...deps,safeFs};
|
|
13
13
|
}
|
|
14
14
|
export function report(result,output,error){
|
|
15
|
-
for(const item of result.results){output.write(item.platform+': '+item.status+'
|
|
15
|
+
for(const item of result.results){output.write(item.platform+': '+item.status+'\n');for(const filename of item.residualPaths??[])output.write('Residual path: '+filename+'\n');if(item.error)error.write('Error: '+item.error.message+'\n');}
|
|
16
16
|
if(result.interrupted)return 130;
|
|
17
17
|
if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\n');return 0;}
|
|
18
18
|
error.write('Harness update did not complete. Run arc again and choose Install / Update to retry the harness update.\n');return 1;
|
|
@@ -35,18 +35,18 @@ export async function runChild(request,io={},deps={}){
|
|
|
35
35
|
export async function prepareUpdate(context){
|
|
36
36
|
const root=context.packageRoot??packageRoot,pkg=identity(root),api=await runtime(context),cwd=fs.realpathSync(context.cwd??process.cwd()),env=context.env??process.env;
|
|
37
37
|
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:context.userHome??os.homedir(),env,cwd});
|
|
38
|
-
if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
|
|
39
38
|
const npmContext=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
|
|
40
39
|
if(!['global','npx'].includes(npmContext.kind))throw configurationError('Run arc from a verified global ARC installation or npx');
|
|
41
40
|
let latestVersion=null,latestError;
|
|
42
41
|
try{const latest=await api.resolveLatest({context:npmContext,env,cwd,signal:context.signal});if(!/^\d+\.\d+\.\d+$/.test(latest.version))throw Error('Invalid latest ARC version');latestVersion=latest.version;}catch(error){if(['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(error.code))throw error;latestError=error;}
|
|
43
|
-
return{kind:'update',root,pkg,cwd,env,state,npmContext,currentVersion:pkg.version,latestVersion,canUpgrade:latestVersion!==null&&newer(latestVersion,pkg.version),latestError};
|
|
42
|
+
return{kind:'update',root,pkg,cwd,env,state,npmContext,currentVersion:pkg.version,latestVersion,canUpgrade:state.targets.length>0&&latestVersion!==null&&newer(latestVersion,pkg.version),latestError};
|
|
44
43
|
}
|
|
45
44
|
function newer(left,right){const a=left.split('.').map(BigInt),b=right.split('.').map(BigInt);for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]>b[i];}return false;}
|
|
46
45
|
|
|
47
46
|
export async function executeUpdate(context,prepared){
|
|
48
47
|
let packageStarted=false;const messages=[];try{
|
|
49
48
|
const api=await runtime(context),{root,pkg,cwd,env,state}=prepared;
|
|
49
|
+
if(!state?.targets?.length)throw configurationError('No installed ARC harness found');
|
|
50
50
|
if(!prepared.canUpgrade||typeof prepared.latestVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(prepared.latestVersion)||!newer(prepared.latestVersion,prepared.currentVersion))throw configurationError('No newer ARC version is available');
|
|
51
51
|
if(identity(root).version!==pkg.version||pkg.version!==prepared.currentVersion)throw configurationError('ARC package changed after checking its version');
|
|
52
52
|
const{assertPlanSnapshot}=await import('../installer/transaction.mjs');for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs:api.safeFs});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const API = 'https://api.anthropic.com/v1/models';
|
|
2
|
+
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
3
|
+
const unavailable = (code, message) => ({ status: 'unavailable', code, message });
|
|
4
|
+
|
|
5
|
+
export async function discoverAnthropicCatalog({ apiKey, fetchImpl = globalThis.fetch, signal } = {}) {
|
|
6
|
+
if (typeof apiKey !== 'string' || !apiKey.trim() || /[\r\n]/.test(apiKey)) return unavailable('missing_api_key', 'Anthropic API key is unavailable.');
|
|
7
|
+
if (typeof fetchImpl !== 'function') return unavailable('unavailable', 'Anthropic catalog is unavailable.');
|
|
8
|
+
const pairs = [], unavailableModels = [], seenPairs = new Set(), seenModels = new Set(), supportedModels = new Set(), cursors = new Set();
|
|
9
|
+
let cursor;
|
|
10
|
+
try {
|
|
11
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber++) {
|
|
12
|
+
if (signal?.aborted) return unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
13
|
+
const url = new URL(API);
|
|
14
|
+
if (cursor) url.searchParams.set('after_id', cursor);
|
|
15
|
+
const response = await fetchImpl(url, {
|
|
16
|
+
method: 'GET', redirect: 'manual', signal,
|
|
17
|
+
headers: { 'anthropic-version': '2026-10-21', Authorization: `Bearer ${apiKey}` },
|
|
18
|
+
});
|
|
19
|
+
if (!response?.ok) return unavailable('http_error', 'Anthropic catalog is unavailable.');
|
|
20
|
+
if (response.url) {
|
|
21
|
+
const actual = new URL(response.url);
|
|
22
|
+
if (actual.origin !== url.origin || actual.pathname !== url.pathname) return unavailable('http_error', 'Anthropic catalog is unavailable.');
|
|
23
|
+
}
|
|
24
|
+
const page = await response.json();
|
|
25
|
+
if (!page || !Array.isArray(page.data) || !page.data.length || typeof page.has_more !== 'boolean') throw Error('invalid page');
|
|
26
|
+
for (const model of page.data) {
|
|
27
|
+
if (!model || typeof model.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/@+\-]*$/.test(model.id) || model.id.includes(apiKey)) throw Error('invalid model');
|
|
28
|
+
const capability = model.capabilities?.effort;
|
|
29
|
+
const supported = capability?.supported === true ? EFFORTS.filter(effort => capability[effort]?.supported === true) : [];
|
|
30
|
+
if (!supported.length && !seenModels.has(model.id)) unavailableModels.push({ model: model.id, reason: 'Effort capability is unavailable.' });
|
|
31
|
+
seenModels.add(model.id);
|
|
32
|
+
for (const effort of supported) {
|
|
33
|
+
supportedModels.add(model.id);
|
|
34
|
+
const key = JSON.stringify([model.id, effort]);
|
|
35
|
+
if (!seenPairs.has(key)) { seenPairs.add(key); pairs.push({ model: model.id, effort }); }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!page.has_more) return { status: 'ready', catalog: { platform: 'claude', pairs, unavailable: unavailableModels.filter(item => !supportedModels.has(item.model)) } };
|
|
39
|
+
if (typeof page.last_id !== 'string' || !page.last_id || page.last_id !== page.data.at(-1).id || cursors.has(page.last_id)) throw Error('invalid cursor');
|
|
40
|
+
cursor = page.last_id; cursors.add(cursor);
|
|
41
|
+
}
|
|
42
|
+
throw Error('too many pages');
|
|
43
|
+
} catch {
|
|
44
|
+
return signal?.aborted ? unavailable('aborted', 'Catalog lookup was cancelled.') : unavailable('invalid_response', 'Anthropic catalog is unavailable.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { effortField, validateRoles, ModelValidationError } from './config.mjs';
|
|
2
|
+
import { discoverAnthropicCatalog } from './catalog-anthropic.mjs';
|
|
3
|
+
export { discoverAnthropicCatalog } from './catalog-anthropic.mjs';
|
|
4
|
+
export { listCodexModelCapabilities } from './codex-catalog.mjs';
|
|
5
|
+
|
|
6
|
+
const unavailable = (code, message) => ({ status: 'unavailable', code, message });
|
|
7
|
+
const validText = value => typeof value === 'string' && value.length > 0 && value === value.trim() && !/[\p{Cc}\u2028\u2029]/u.test(value);
|
|
8
|
+
|
|
9
|
+
function codexCatalog(models, secrets) {
|
|
10
|
+
if (!Array.isArray(models) || !models.length) throw Error('invalid catalog');
|
|
11
|
+
const pairs = [], missing = [], seenPairs = new Set(), seenModels = new Set(), supportedModels = new Set();
|
|
12
|
+
for (const item of models) {
|
|
13
|
+
if (!item || !validText(item.model) || secrets.some(secret => item.model.includes(secret)) || !Array.isArray(item.efforts) || !item.efforts.every(value => validText(value) && !secrets.some(secret => value.includes(secret)))) throw Error('invalid model');
|
|
14
|
+
if (!item.efforts.length && !seenModels.has(item.model)) missing.push({ model: item.model, reason: 'Effort capability is unavailable.' });
|
|
15
|
+
seenModels.add(item.model);
|
|
16
|
+
for (const effort of item.efforts) {
|
|
17
|
+
supportedModels.add(item.model);
|
|
18
|
+
const key = JSON.stringify([item.model, effort]);
|
|
19
|
+
if (!seenPairs.has(key)) { seenPairs.add(key); pairs.push({ model: item.model, effort }); }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { platform: 'codex', pairs, unavailable: missing.filter(item => !supportedModels.has(item.model)) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function discoverModelCatalog({ platform, env = process.env, fetchImpl = globalThis.fetch, listCodexModels, signal, timeoutMs = 10000 } = {}) {
|
|
26
|
+
if (!['codex', 'claude'].includes(platform)) return unavailable('unsupported_platform', 'Catalog is unavailable.');
|
|
27
|
+
if (signal?.aborted) return unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
28
|
+
if (platform === 'codex' && typeof listCodexModels !== 'function') return unavailable('unavailable', 'Codex catalog is unavailable.');
|
|
29
|
+
const controller = new AbortController(); let timedOut = false, resolveAbort;
|
|
30
|
+
const abortPromise = new Promise(resolve => { resolveAbort = resolve; });
|
|
31
|
+
const onAbort = () => { controller.abort(); resolveAbort(); };
|
|
32
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
const timer = setTimeout(() => { timedOut = true; onAbort(); }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 10000);
|
|
34
|
+
const operation = Promise.resolve().then(async () => platform === 'codex'
|
|
35
|
+
? { status: 'ready', catalog: codexCatalog(await listCodexModels({ env, signal: controller.signal }), [env?.OPENAI_API_KEY, env?.ANTHROPIC_API_KEY].filter(value => typeof value === 'string' && value.length)) }
|
|
36
|
+
: discoverAnthropicCatalog({ apiKey: env?.ANTHROPIC_API_KEY, fetchImpl, signal: controller.signal }));
|
|
37
|
+
const settled = operation.then(value => ({ value }), () => ({ failed: true }));
|
|
38
|
+
try {
|
|
39
|
+
const first = await Promise.race([settled, abortPromise.then(() => ({ aborted: true }))]);
|
|
40
|
+
if (first.aborted) {
|
|
41
|
+
if (platform === 'codex') {
|
|
42
|
+
let graceTimer;
|
|
43
|
+
await Promise.race([settled, new Promise(resolve => { graceTimer = setTimeout(resolve, 2100); })]);
|
|
44
|
+
clearTimeout(graceTimer);
|
|
45
|
+
}
|
|
46
|
+
return timedOut ? unavailable('timeout', 'Catalog lookup timed out.') : unavailable('aborted', 'Catalog lookup was cancelled.');
|
|
47
|
+
}
|
|
48
|
+
return first.failed ? unavailable('unavailable', 'Catalog is unavailable.') : first.value;
|
|
49
|
+
} finally { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function assertCatalogRoles({ platform, roles, catalog } = {}) {
|
|
53
|
+
const validRoles = validateRoles(roles, platform);
|
|
54
|
+
if (!catalog || catalog.platform !== platform || !Array.isArray(catalog.pairs)) throw new ModelValidationError('Catalog is unavailable.');
|
|
55
|
+
const pairs = new Set();
|
|
56
|
+
for (const pair of catalog.pairs) {
|
|
57
|
+
if (!pair || !validText(pair.model) || !validText(pair.effort)) throw new ModelValidationError('Catalog is invalid.');
|
|
58
|
+
pairs.add(JSON.stringify([pair.model, pair.effort]));
|
|
59
|
+
}
|
|
60
|
+
const effort = effortField(platform);
|
|
61
|
+
if (Object.values(validRoles).some(value => !pairs.has(JSON.stringify([value.model, value[effort]])))) throw new ModelValidationError('Roles contain a pair absent from the catalog.');
|
|
62
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
|
|
5
|
+
const ENV_KEYS = ['PATH', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'SYSTEMROOT', 'WINDIR', 'TMPDIR', 'TEMP', 'TMP', 'CODEX_HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME'];
|
|
6
|
+
const validText = value => typeof value === 'string' && value.length > 0 && value === value.trim() && !/[\p{Cc}\u2028\u2029]/u.test(value);
|
|
7
|
+
const failure = () => new Error('Codex catalog is unavailable.');
|
|
8
|
+
|
|
9
|
+
export async function listCodexModelCapabilities({ env = process.env, cwd, spawnImpl = spawn, signal, timeoutMs = 10000 } = {}) {
|
|
10
|
+
if (typeof cwd !== 'string' || !path.isAbsolute(cwd) || path.normalize(cwd) !== cwd || typeof spawnImpl !== 'function' || signal?.aborted) throw failure();
|
|
11
|
+
const childEnv = Object.fromEntries(ENV_KEYS.filter(key => typeof env?.[key] === 'string').map(key => [key, env[key]]));
|
|
12
|
+
const secrets = [env?.OPENAI_API_KEY, env?.ANTHROPIC_API_KEY].filter(value => typeof value === 'string' && value.length);
|
|
13
|
+
const containsSecret = value => secrets.some(secret => value.includes(secret));
|
|
14
|
+
let child;
|
|
15
|
+
try { child = spawnImpl('codex', ['app-server'], { cwd, env: childEnv, stdio: ['pipe', 'pipe', 'pipe'], shell: false }); }
|
|
16
|
+
catch { throw failure(); }
|
|
17
|
+
if (!child?.stdin || !child?.stdout || !child?.stderr || typeof child.on !== 'function' || typeof child.kill !== 'function') throw failure();
|
|
18
|
+
|
|
19
|
+
let closed = false, interrupted = false, cancelled = false, nextId = 0, buffer = '';
|
|
20
|
+
const pending = new Map(), decoder = new StringDecoder('utf8');
|
|
21
|
+
let resolveClosed;
|
|
22
|
+
const closedPromise = new Promise(resolve => { resolveClosed = resolve; });
|
|
23
|
+
const rejectPending = () => { interrupted = true; for (const entry of pending.values()) entry.reject(failure()); pending.clear(); };
|
|
24
|
+
const onClose = () => { closed = true; resolveClosed(); rejectPending(); };
|
|
25
|
+
const onError = () => rejectPending();
|
|
26
|
+
const onData = chunk => {
|
|
27
|
+
buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
28
|
+
if (buffer.length > 1048576) { rejectPending(); return; }
|
|
29
|
+
let newline;
|
|
30
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
31
|
+
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
|
32
|
+
if (!line.trim()) continue;
|
|
33
|
+
let message; try { message = JSON.parse(line); } catch { rejectPending(); return; }
|
|
34
|
+
if (message?.id === undefined) continue;
|
|
35
|
+
const entry = pending.get(message.id);
|
|
36
|
+
if (!entry) { rejectPending(); return; }
|
|
37
|
+
pending.delete(message.id);
|
|
38
|
+
if (message.error || !Object.hasOwn(message, 'result')) entry.reject(failure()); else entry.resolve(message.result);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
child.on('close', onClose); child.on('error', onError);
|
|
42
|
+
child.stdout.on('data', onData); child.stdout.on('error', onError); child.stdin.on('error', onError); child.stderr.on('error', onError); child.stderr.resume();
|
|
43
|
+
const onAbort = () => { cancelled = true; rejectPending(); };
|
|
44
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
if (signal?.aborted) onAbort();
|
|
46
|
+
const timer = setTimeout(onAbort, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 10000);
|
|
47
|
+
const send = value => { if (interrupted || closed) throw failure(); child.stdin.write(JSON.stringify(value) + '\n'); };
|
|
48
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
49
|
+
if (interrupted || closed) { reject(failure()); return; }
|
|
50
|
+
const id = ++nextId; pending.set(id, { resolve, reject });
|
|
51
|
+
try { send({ id, method, params }); } catch { pending.delete(id); reject(failure()); }
|
|
52
|
+
});
|
|
53
|
+
let result, error;
|
|
54
|
+
try {
|
|
55
|
+
const initialized = await request('initialize', { clientInfo: { name: 'arc', title: 'ARC', version: '1.0.0' } });
|
|
56
|
+
if (!initialized || typeof initialized !== 'object' || Array.isArray(initialized) || !validText(initialized.userAgent)) throw failure();
|
|
57
|
+
send({ method: 'initialized', params: {} });
|
|
58
|
+
const models = new Map(), cursors = new Set(); let cursor;
|
|
59
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber++) {
|
|
60
|
+
const page = await request('model/list', { limit: 100, includeHidden: true, ...(cursor ? { cursor } : {}) });
|
|
61
|
+
if (interrupted) throw failure();
|
|
62
|
+
if (!page || !Array.isArray(page.data) || !page.data.length || !(page.nextCursor === null || validText(page.nextCursor))) throw failure();
|
|
63
|
+
for (const descriptor of page.data) {
|
|
64
|
+
if (descriptor?.hidden === true) continue;
|
|
65
|
+
if (!descriptor || !validText(descriptor.model) || containsSecret(descriptor.model) || !Array.isArray(descriptor.supportedReasoningEfforts)) throw failure();
|
|
66
|
+
let efforts = models.get(descriptor.model);
|
|
67
|
+
if (!efforts) { efforts = []; models.set(descriptor.model, efforts); }
|
|
68
|
+
for (const option of descriptor.supportedReasoningEfforts) {
|
|
69
|
+
if (!validText(option?.reasoningEffort) || containsSecret(option.reasoningEffort)) throw failure();
|
|
70
|
+
if (!efforts.includes(option.reasoningEffort)) efforts.push(option.reasoningEffort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (page.nextCursor === null) { result = [...models].map(([model, efforts]) => ({ model, efforts })); break; }
|
|
74
|
+
if (cursors.has(page.nextCursor)) throw failure();
|
|
75
|
+
cursors.add(page.nextCursor); cursor = page.nextCursor;
|
|
76
|
+
}
|
|
77
|
+
if (!result) throw failure();
|
|
78
|
+
} catch { error = failure(); }
|
|
79
|
+
finally {
|
|
80
|
+
rejectPending();
|
|
81
|
+
try { child.stdin.end(); } catch { error = failure(); }
|
|
82
|
+
if (!closed) { try { child.kill('SIGTERM'); } catch { error = failure(); } }
|
|
83
|
+
const wait = async ms => { if (closed) return true; let timerId; const timeout = new Promise(resolve => { timerId = setTimeout(() => resolve(false), ms); }); const value = await Promise.race([closedPromise.then(() => true), timeout]); clearTimeout(timerId); return value; };
|
|
84
|
+
if (!await wait(1000)) {
|
|
85
|
+
try { child.kill('SIGKILL'); } catch { error = failure(); }
|
|
86
|
+
if (!await wait(1000)) error = failure();
|
|
87
|
+
}
|
|
88
|
+
if (!closed) {
|
|
89
|
+
// A failed close may still emit late errors; keep only a guard until close.
|
|
90
|
+
const guard = () => {};
|
|
91
|
+
const streams = [child.stdin, child.stdout, child.stderr];
|
|
92
|
+
child.on('error', guard); for (const stream of streams) stream.on('error', guard);
|
|
93
|
+
child.once('close', () => { child.removeListener('error', guard); for (const stream of streams) stream.removeListener('error', guard); });
|
|
94
|
+
for (const stream of streams) stream.destroy();
|
|
95
|
+
}
|
|
96
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
97
|
+
if (cancelled) error = failure();
|
|
98
|
+
child.stdout.removeListener('data', onData); child.stdout.removeListener('error', onError); child.stdin.removeListener('error', onError); child.stderr.removeListener('error', onError);
|
|
99
|
+
child.removeListener('close', onClose); child.removeListener('error', onError);
|
|
100
|
+
}
|
|
101
|
+
if (error) throw error;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
package/lib/models/config.mjs
CHANGED
|
@@ -5,8 +5,7 @@ export class ModelValidationError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
export function fail(message) { throw new ModelValidationError(message); }
|
|
7
7
|
export function platformOf(config) {
|
|
8
|
-
if (config?.platform
|
|
9
|
-
if (config?.platform === 'claude' && config.version === 1) return 'claude';
|
|
8
|
+
if (['codex', 'claude'].includes(config?.platform) && [1, 2].includes(config.version)) return config.platform;
|
|
10
9
|
fail('Unsupported model schema version');
|
|
11
10
|
}
|
|
12
11
|
export function effortField(platform) {
|
|
@@ -30,29 +29,38 @@ export function validateRoles(roles, platform) {
|
|
|
30
29
|
}
|
|
31
30
|
export const PRESETS = Object.freeze(['economy', 'balanced', 'smart']);
|
|
32
31
|
export function validateModelConfig(config) {
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const platform = platformOf(config);
|
|
33
|
+
const hasCustom = Object.hasOwn(config, 'custom');
|
|
34
|
+
exactKeys(config, ['platform', 'version', 'preset', 'roles', ...(hasCustom && config.version === 2 ? ['custom'] : [])], 'model config');
|
|
35
|
+
if (![...PRESETS, 'custom'].includes(config.preset) || (config.preset === 'custom' && !hasCustom)) fail('Unsupported performance preset or missing custom roles');
|
|
35
36
|
exactKeys(config.roles, ROLES, 'roles');
|
|
36
37
|
for (const role of ROLES) exactKeys(config.roles[role], PRESETS, `roles.${role}`);
|
|
37
38
|
for (const preset of PRESETS) validateRoles(Object.fromEntries(ROLES.map(role => [role, config.roles[role][preset]])), platform);
|
|
38
|
-
|
|
39
|
+
if (hasCustom) validateRoles(config.custom, platform);
|
|
40
|
+
return { ...structuredClone(config), version: 2 };
|
|
39
41
|
}
|
|
40
42
|
export function resolveModelSettings(config) {
|
|
41
43
|
const validated = validateModelConfig(config);
|
|
42
|
-
return { platform: validated.platform, roles: Object.fromEntries(ROLES.map(role => [role, validated.roles[role][validated.preset]])) };
|
|
44
|
+
return { platform: validated.platform, roles: validated.preset === 'custom' ? validated.custom : Object.fromEntries(ROLES.map(role => [role, validated.roles[role][validated.preset]])) };
|
|
43
45
|
}
|
|
44
46
|
export function parseToml(text) {
|
|
45
47
|
try { return parse(typeof text === 'string' ? text : new TextDecoder('utf-8', { fatal: true }).decode(text), { integersAsBigInt: true }); }
|
|
46
48
|
catch (cause) { throw new ModelValidationError(`Invalid TOML: ${cause.message}`, { cause }); }
|
|
47
49
|
}
|
|
48
50
|
export function loadModelConfig(text, platform) {
|
|
49
|
-
effortField(platform); const config = parseToml(text);
|
|
50
|
-
if (config.version !== 1n) fail(`Expected ${platform} integer schema version 1; reinstall or update models.toml`);
|
|
51
|
-
|
|
51
|
+
effortField(platform); const config = parseToml(text);
|
|
52
|
+
if (config.version !== 1n && config.version !== 2n) fail(`Expected ${platform} integer schema version 1 or 2; reinstall or update models.toml`);
|
|
53
|
+
const hasCustom = Object.hasOwn(config, 'custom');
|
|
54
|
+
exactKeys(config, ['version', 'preset', 'roles', ...(hasCustom && config.version === 2n ? ['custom'] : [])], 'model file (reinstall or update legacy role-only settings)');
|
|
55
|
+
return validateModelConfig({ ...config, platform, version: Number(config.version) });
|
|
52
56
|
}
|
|
53
|
-
export function serializeModelConfig(config) { const {
|
|
57
|
+
export function serializeModelConfig(config) { const { platform, ...file } = validateModelConfig(config); return stringify(file); }
|
|
54
58
|
export function applyPreset(config, preset) {
|
|
55
59
|
const validated = validateModelConfig(config);
|
|
56
|
-
if (!PRESETS.includes(preset)) fail('Unsupported performance preset');
|
|
60
|
+
if (![...PRESETS, 'custom'].includes(preset) || (preset === 'custom' && !validated.custom)) fail('Unsupported performance preset or missing custom roles');
|
|
57
61
|
return { ...validated, preset };
|
|
58
62
|
}
|
|
63
|
+
export function applyCustomRoles(config, roles) {
|
|
64
|
+
const validated = validateModelConfig(config);
|
|
65
|
+
return { ...validated, preset: 'custom', custom: validateRoles(roles, validated.platform) };
|
|
66
|
+
}
|
package/lib/models/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { loadModelConfig, applyPreset, serializeModelConfig, resolveModelSettings, ModelValidationError } from './config.mjs';
|
|
1
|
+
export { loadModelConfig, applyPreset, applyCustomRoles, serializeModelConfig, resolveModelSettings, ModelValidationError } from './config.mjs';
|
|
2
2
|
export { renderProfiles } from './profiles.mjs';
|
package/lib/models/profiles.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {validateClaudeHookCommand} from '../policy/claude-hook.mjs';
|
|
2
1
|
import { parseToml, exactKeys, validateRoles, effortField, ROLES, fail } from './config.mjs';
|
|
3
2
|
const tools = {
|
|
4
3
|
arc_scouter: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch'],
|
|
@@ -74,43 +73,6 @@ function renderCodex(text, role, settings) {
|
|
|
74
73
|
if (checked.name !== role || Object.entries(settings).some(([key, value]) => checked[key] !== value)) fail('Rendered Codex profile does not match settings');
|
|
75
74
|
return Buffer.from(output);
|
|
76
75
|
}
|
|
77
|
-
// Validate the consumed hook subset without rewriting any hook/user bytes.
|
|
78
|
-
// The two ARC entries are exact contracts; user command entries remain data.
|
|
79
|
-
function validateClaudeHooks(lines){
|
|
80
|
-
const managed=new Map();let event;const events=new Set();
|
|
81
|
-
if(lines[0]!=='hooks:')fail('Unsupported managed Claude hooks');
|
|
82
|
-
for(let i=1;i<lines.length;){
|
|
83
|
-
const heading=/^ (PreToolUse|PostToolUse|SubagentStart):$/.exec(lines[i]);
|
|
84
|
-
if(heading){event=heading[1];if(events.has(event))fail('Duplicate Claude hook event');events.add(event);i++;continue;}
|
|
85
|
-
if(!event)fail('Unsupported Claude hook event');
|
|
86
|
-
const matcher=/^ - matcher: (.+)$/.exec(lines[i]);
|
|
87
|
-
let parsedMatcher,command;
|
|
88
|
-
try{
|
|
89
|
-
if(!matcher)throw Error('Missing matcher');
|
|
90
|
-
parsedMatcher=matcher[1].startsWith('"')?JSON.parse(matcher[1]):matcher[1];
|
|
91
|
-
if(typeof parsedMatcher!=='string'||!parsedMatcher||/[\r\n\x00]/.test(parsedMatcher)||(!matcher[1].startsWith('"')&&!/^[A-Za-z][A-Za-z0-9_|]*$/.test(parsedMatcher)))throw Error('Invalid matcher');
|
|
92
|
-
if(lines[i+1]!==' hooks:'||lines[i+2]!==' - type: command'||!lines[i+3]?.startsWith(' command: '))throw Error('Unsupported command entry');
|
|
93
|
-
command=JSON.parse(lines[i+3].slice(' command: '.length));
|
|
94
|
-
if(typeof command!=='string'||!command||/[\r\n\x00]/.test(command))throw Error('Invalid command');
|
|
95
|
-
}catch{fail('Unsupported managed Claude hooks');}
|
|
96
|
-
let isManaged=false;
|
|
97
|
-
try{validateClaudeHookCommand(command);isManaged=true;}catch{}
|
|
98
|
-
const hasFailure=lines[i+4]?.startsWith(' onFailure:');
|
|
99
|
-
if(isManaged){
|
|
100
|
-
if(lines[i+4]!==' onFailure: block')fail('Unsupported managed Claude failure behavior');
|
|
101
|
-
const encoded=/"([A-Za-z0-9+/]+={0,2})"$/.exec(command)[1];
|
|
102
|
-
const url=new URL(Buffer.from(encoded,'base64').toString('utf8'));const file=url.pathname.split('/').at(-1);
|
|
103
|
-
const expected=file==='runtime-gate.mjs'?'"*"':'Bash';
|
|
104
|
-
if(event!=='PreToolUse'||matcher[1]!==expected||!hasFailure||managed.has(file))fail('Malformed or duplicate managed Claude hooks');
|
|
105
|
-
managed.set(file,url.href.slice(0,url.href.lastIndexOf('/')+1));
|
|
106
|
-
}else{
|
|
107
|
-
if(hasFailure&&![' onFailure: continue',' onFailure: block'].includes(lines[i+4]))fail('Unsupported Claude failure behavior');
|
|
108
|
-
if(/runtime-gate\.mjs|role-git-guard\.mjs/.test(command))fail('Unrecognized managed Claude hook command');
|
|
109
|
-
}
|
|
110
|
-
i+=hasFailure?5:4;
|
|
111
|
-
}
|
|
112
|
-
if(managed.size!==1||!managed.has('role-git-guard.mjs'))fail('Missing or mismatched managed Claude hooks');
|
|
113
|
-
}
|
|
114
76
|
function renderClaude(text, role, settings) {
|
|
115
77
|
const lines = linesOf(text); if (lines[0]?.replace(/\r?\n$/, '') !== '---') fail('Missing opening Claude frontmatter');
|
|
116
78
|
const fields = {}, indices = {}; let current, closed = false;
|
|
@@ -119,7 +81,6 @@ function renderClaude(text, role, settings) {
|
|
|
119
81
|
if(line==='hooks:') {
|
|
120
82
|
if(Object.hasOwn(fields,'hooks'))fail('Duplicate Claude hooks');
|
|
121
83
|
let end=i+1;while(end<lines.length&&/^[ \t]/.test(lines[end]))end++;
|
|
122
|
-
validateClaudeHooks(lines.slice(i,end).map(x=>x.replace(/\r?\n$/,'')));
|
|
123
84
|
fields.hooks=true;indices.hooks=i;i=end-1;current='hooks';continue;
|
|
124
85
|
}
|
|
125
86
|
const item = /^ - ([A-Za-z][A-Za-z0-9]*)$/.exec(line);
|
|
@@ -130,7 +91,7 @@ function renderClaude(text, role, settings) {
|
|
|
130
91
|
if (key === 'tools') { if (value !== undefined) fail('Claude tools must be an explicit list'); fields.tools = []; }
|
|
131
92
|
else { if (!value || value !== value.trim() || /[\t\x00{}\[\]#&*!|>"']/.test(value) || value.includes(': ')) fail('Claude frontmatter requires plain scalar values'); fields[key] = value; }
|
|
132
93
|
}
|
|
133
|
-
if (!closed || !['name', 'description', 'model', 'effort', 'tools'
|
|
94
|
+
if (!closed || !['name', 'description', 'model', 'effort', 'tools'].every(key => Object.hasOwn(fields, key)) || fields.name !== role.replace('_', '-') || JSON.stringify(fields.tools) !== JSON.stringify(tools[role])) fail('Invalid Claude profile identity or role tools');
|
|
134
95
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:/@+\-]*$/.test(fields.model) || !['low','medium','high','xhigh','max'].includes(fields.effort)) fail('Invalid Claude profile model or effort');
|
|
135
96
|
for (const key of ['model', 'effort']) { const index = indices[key], eol = /\r?\n$/.exec(lines[index])?.[0] ?? ''; lines[index] = `${key}: ${settings[key]}${eol}`; }
|
|
136
97
|
return Buffer.from(lines.join(''));
|