@yunhozz/arc 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/MANIFEST.sha256 +31 -27
  2. package/claude/agents/arc-planner.md +3 -3
  3. package/claude/skill/SKILL.md +2 -2
  4. package/claude/skill/models.toml +11 -11
  5. package/claude/skill/references/brainstorming.md +111 -42
  6. package/claude/skill/references/coordination.md +3 -3
  7. package/claude/skill/references/planning.md +6 -6
  8. package/claude/skill/references/superpowers-provenance.json +3 -3
  9. package/claude/skill/runtime/role-policy.json +3 -3
  10. package/claude/skill/scripts/model_config.py +17 -5
  11. package/codex/agents/arc-planner.toml +3 -3
  12. package/codex/skill/SKILL.md +2 -2
  13. package/codex/skill/models.toml +13 -13
  14. package/codex/skill/references/brainstorming.md +111 -42
  15. package/codex/skill/references/coordination.md +3 -3
  16. package/codex/skill/references/planning.md +6 -6
  17. package/codex/skill/references/superpowers-provenance.json +3 -3
  18. package/codex/skill/runtime/role-policy.json +3 -3
  19. package/codex/skill/scripts/model_config.py +25 -6
  20. package/lib/installer/global-config.mjs +1 -1
  21. package/lib/installer/plan.mjs +6 -5
  22. package/lib/installer/preset.mjs +7 -4
  23. package/lib/management/operations.mjs +1 -1
  24. package/lib/models/catalog-anthropic.mjs +46 -0
  25. package/lib/models/catalog.mjs +62 -0
  26. package/lib/models/codex-catalog.mjs +103 -0
  27. package/lib/models/config.mjs +19 -11
  28. package/lib/models/index.mjs +1 -1
  29. package/lib/policy/role-policy.json +3 -3
  30. package/lib/tui/app.mjs +15 -9
  31. package/lib/tui/custom-preset.mjs +122 -0
  32. package/lib/tui/render.mjs +3 -2
  33. package/lib/tui/setup.mjs +62 -6
  34. package/package.json +1 -1
@@ -24,7 +24,7 @@ If explicitly requested probe+implementation is fully specified, classify the wh
24
24
 
25
25
  Initial design/brainstorming may precede target choice. Formal spec/plan includes complete pre-save drafts returned only in chat. Before that, Orchestrator records actual absolute workspace/repository, branch/detached HEAD/HEAD, changes/ownership, explicit choice/preparation and in-target absolute document paths. Planner drafts for verified target; Orchestrator saves there. Missing/mismatched evidence stops affected drafting/saving. Planned paths prove no actual preparation. Initial preparation never starts Implementer without full handoff.
26
26
 
27
- Small-task direct edits follow only [handoff](handoff.md#orchestrator-direct-path-for-small-tasks), stopping/reclassifying changed scope/risk. Otherwise follow this reference. `brainstorming`/`writing-plans` instruct existing roles, not extra agents; ARC routing/approval overrides alternate executors. Missing required skills block their stage. Integration means save/route/request approval, never author.
27
+ Small-task direct edits follow only [handoff](handoff.md#orchestrator-direct-path-for-small-tasks), stopping/reclassifying changed scope/risk. Otherwise follow this reference. `brainstorming`/`writing-plans` instruct existing roles, not extra agents; ARC routing/approval overrides alternate executors. Missing required skills block their stage. Integration includes saving, reviewing and directly revising the same spec or plan after the complete Planner first draft; design/brainstorming retain their Planner drafting path.
28
28
 
29
29
  For Korean requests/briefs, Planner uses Korean titles/prose unless another language is requested, preserving identifiers, skill/API names, code, paths, commands and literals. Orchestrator preserves agreed language when saving.
30
30
 
@@ -36,10 +36,10 @@ Planner reports initial/revision progress only to Orchestrator: brief stage/feed
36
36
  1. Preflight relevant policy/references/profiles/tests/parity/manifest/conflicts. Apply formal-target and coordination provenance/input/local-skill gates, including required paths, approved bounded read alternatives and return conditions. Missing inputs/conflicts hold drafting. Read recovery grants no preparation/check/write/approval authority.
37
37
  2. Maintain a compact ledger of original requirements/IDs, approved/open decisions, exclusions and scope deltas. Consolidate intervening user changes into one brief delta. Separate approvals, path/line-verified facts, assumptions and open decisions.
38
38
  3. Send one complete stage-specific initial brief: prepared target/HEAD/ownership, scope/exclusions/depth/dependencies/shared contracts/criteria/checks/evidence/holds; preceding approved versions, paths, current behavior and connected source/generated/runtime/installer/docs, policy conflicts/resolution direction and exact deliverable. Visual work includes approved sources/versions/criteria/open interpretations.
39
- 4. Planner returns complete proportional draft/self-review; Orchestrator integrates/saves/reviews/requests approval without authoring missing content. Review once with the fixed checklist: scope/evidence; targets/ownership/paths; policy/conflicts; task boundaries/dependencies/criteria; checks/evidence/holds; roles/approval/commit boundaries; completeness. Finish before one consolidated correction covering every substantive omission with criterion/defect/evidence/expected fix. Use deltas/`unchanged`; never split by omission.
40
- 5. Further calls after correction need approved material scope/evidence changes or concrete unmet checklist items plus meaningful new input/criterion progress. State changed input/target criterion/expected artifact/progress. Existing-scope omissions need no renewed scope approval. Repeated unmet criteria without new input/evidence/progress hold the stage; report criterion/observation/exact unblock decision. Do not repeat resolved questions or count rewording/agent replacement as progress.
39
+ 4. Planner returns the complete proportional first draft and self-review. Orchestrator saves and reviews it against the fixed checklist: scope/evidence; targets/ownership/paths; policy/conflicts; task boundaries/dependencies/criteria; checks/evidence/holds; roles/approval/commit boundaries; completeness. For spec or plan, Orchestrator directly revises the same document, including substantive omissions, before requesting approval. New product decisions outside approved scope require user input first; Orchestrator then revises the document. Missing or failed first drafts stop the stage and do not transfer initial authorship.
40
+ 5. For design and brainstorming, finish the checklist before one consolidated correction covering every substantive omission with criterion/defect/evidence/expected fix. Use deltas/`unchanged`; never split by omission. Further calls after correction need approved material scope/evidence changes or concrete unmet checklist items plus meaningful new input/criterion progress. State changed input/target criterion/expected artifact/progress. Existing-scope omissions need no renewed scope approval. Repeated unmet criteria without new input/evidence/progress hold the stage; report criterion/observation/exact unblock decision. Do not repeat resolved questions or count rewording/agent replacement as progress.
41
41
 
42
- Allow initial draft and one consolidated correction. Reformatting/re-expression/save/patch errors belong to Orchestrator, not Planner calls. Separate substantive work uses normal gates. Design/spec/plan stages remain separately ordered; spec approval precedes dependent planning. Add no fixed call cap/profile count condition or incomplete-draft acceptance.
42
+ For design and brainstorming, allow initial draft and one consolidated correction. Reformatting/re-expression/save/patch errors belong to Orchestrator, not Planner calls. Separate substantive work uses normal gates. Design/spec/plan stages remain separately ordered; spec approval precedes dependent planning. Add no fixed call cap/profile count condition or incomplete-draft acceptance.
43
43
 
44
44
  <a id="design-and-planning"></a>
45
45
  <a id="design-path-and-plan-depth"></a>
@@ -55,7 +55,7 @@ Record depth separately from risk. Risk controls checks/review, not design paths
55
55
 
56
56
  If Bounded requires written spec, record necessity/scope using approved short design as basis. Planner returns proportional full spec/self-review; Orchestrator saves/reviews/obtains individual approval before dependent plan/preparation. Short design approval never substitutes. This applies recorded judgment, not new spec necessity. Required plans alone imply no written spec.
57
57
 
58
- Only Planner drafts. Never substitute Orchestrator-authored specs/plans. Return substantive omissions; unavailable/failed Planner stops work rather than transferring authorship. Design approval does not transfer ownership.
58
+ Only Planner authors the first complete spec or plan draft and self-review. Orchestrator reviews and directly revises the same document before approval. Unavailable/failed Planner stops first drafting rather than transferring authorship. Design approval does not transfer initial authorship.
59
59
 
60
60
  <a id="top-level-plan-tasks"></a>
61
61
  ## Top-level plan tasks
@@ -151,7 +151,7 @@ Generated from lib/policy/role-policy.json; do not edit this managed section man
151
151
  - [review.significant-plan] Significant/high-risk plan work follows Implementer → required fresh QA → Orchestrator acceptance and focused commit for each task. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is not a per-task or per-commit gate. Preserve required checks and necessary finding rechecks.
152
152
  - [review.final-plan-recheck] Final overall Reviewer findings on plan work, including behavior findings, follow Implementer → Reviewer without QA reassignment until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before the initial final review and outside this exception.
153
153
  - [planning.plan-amend] Mechanical plan progress records during approved execution need no new user approval. Changes to goals, scope, criteria, method, risk, dependencies or required verification need revised-version approval and preparation before affected work. In both cases Orchestrator amends the original planning-document commit at a safe boundary, with no separate document or fixup commit. Preserve actual approval chronology, recoverable old/new content, hashes, versions and SHA mappings; never authorize past work retroactively. Rewrite only unpublished, unshared local linear history with clear ownership and no merges. Unknown publication/ownership, unexpected state or replay conflicts stop rewriting; no force push or fallback document commit. Preserve implementation task boundaries and revalidate affected evidence. Follow planning, handoff-prep, commit and verification for the full contract.
154
- - [planning.planner-brief] Follow planning.md's Planner drafting preparation and consolidated feedback procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Allow the initial draft and one consolidated correction. Send one complete stage-specific initial brief. Planner returns a complete proportional draft and self-review. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Hold repeated unmet criteria without new input, evidence or progress; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.
154
+ - [planning.planner-brief] Follow planning.md's Planner drafting preparation and review procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Send one complete stage-specific initial brief. Planner returns a complete proportional first draft and self-review. Orchestrator reviews and directly revises the same spec or plan when needed. A missing or failed first draft does not transfer initial authorship to Orchestrator. New product decisions outside approved scope require user input before Orchestrator revises the document. For design and brainstorming, allow the initial draft and one consolidated correction. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Repeated unmet criteria without new input, evidence or progress hold the stage; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.
155
155
  - [platform.codex] Role restrictions are prompt-level contracts, not a filesystem sandbox. ARC supplies no native observer hook or role-specific Git guard. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Implementer and QA need shell for approved checks.
156
156
  [ARC role policy: contract end]
157
157
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "repository": "https://github.com/obra/superpowers",
4
- "revision": "bb92a77741419a4ab5f06e711a283343f1ada0c3",
5
- "fetchedAt": "2026-10-17T07:50:01.077Z",
4
+ "revision": "2ce8aa15d05bd57213e6f746f953aafb7ded7a8b",
5
+ "fetchedAt": "2026-10-29T17:10:27.098Z",
6
6
  "skills": [
7
7
  "brainstorming",
8
8
  "writing-plans",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "files": {
12
12
  "LICENSE": "a37e0e9697144819e1d965176ac4ae5bc3fa02d11e7812036bbcadf6dafe2400",
13
- "brainstorming/SKILL.md": "6ee10d1481b8a2580d5336ff66c5c08acc9d4b251b13bbc3bfd31be99c261d43",
13
+ "brainstorming/SKILL.md": "61a7ef918652108c76a3fd01e02dd92d39996c6b46d4cb2819cbd03f6c45e46a",
14
14
  "brainstorming/builder-check-prompt.md": "36d51a4723d6f0a3ce5811a0ea3b3a8424f07533947e1355f5d8cb8cce40c55e",
15
15
  "brainstorming/scripts/frame-template.html": "6a8a4e58bd6a44b904e2e3c57de774481d909204597e1498de53f1b2fecc4c4e",
16
16
  "brainstorming/scripts/helper.js": "43c6d69954a46ec34a2a262bcc62a9a7e83e839c739f199cb72646d397c686e3",
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts only to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation remains limited to the existing small, low-risk path. Use a fresh Implementer per top-level task and a fresh QA per task requiring QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and assignment context remain valid, default same-task corrections to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only for invalid/unavailable context or a changed task boundary, except that an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the same task. Before Implementer reuse, refresh actual workspace, branch/HEAD and change state. QA reruns reproduction and invalidated related checks and reports actor, target state, outcomes and limits. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
153
+ "text": "Assign design/brainstorming drafts and first complete spec/plan drafts only to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves and reviews complete spec/plan drafts and directly revises the same document when needed. Missing or failed first drafts do not transfer initial authorship. New product decisions outside approved scope require user input before revision. Orchestrator owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation remains limited to the existing small, low-risk path. Use a fresh Implementer per top-level task and a fresh QA per task requiring QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and assignment context remain valid, default same-task corrections to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only for invalid/unavailable context or a changed task boundary, except that an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the same task. Before Implementer reuse, refresh actual workspace, branch/HEAD and change state. QA reruns reproduction and invalidated related checks and reports actor, target state, outcomes and limits. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts with dependencies, criteria, holds and verification needs in the response. During initial drafting and revisions, report progress only to Orchestrator: briefly state the stage, feedback being applied, actual blocker or next step, without draft prose or excerpts. Return the complete draft and self-review for review and correction. Raw responses may appear in app/tool surfaces; prompt policy cannot hide them. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting when actual prepared-target evidence is missing. Route decisions to Orchestrator.",
193
+ "text": "Return design/brainstorming drafts and complete first spec/plan drafts with dependencies, criteria, holds and verification needs in the response. During initial drafting and revisions, report progress only to Orchestrator: briefly state the stage, feedback being applied, actual blocker or next step, without draft prose or excerpts. Return the complete first draft and self-review for Orchestrator review; Orchestrator directly revises the same spec or plan when needed. Raw responses may appear in app/tool surfaces; prompt policy cannot hide them. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting when actual prepared-target evidence is missing. Route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -372,7 +372,7 @@
372
372
  "preceding-approval"
373
373
  ],
374
374
  "authority": "orchestrator",
375
- "text": "Follow planning.md's Planner drafting preparation and consolidated feedback procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Allow the initial draft and one consolidated correction. Send one complete stage-specific initial brief. Planner returns a complete proportional draft and self-review. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Hold repeated unmet criteria without new input, evidence or progress; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.",
375
+ "text": "Follow planning.md's Planner drafting preparation and review procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Send one complete stage-specific initial brief. Planner returns a complete proportional first draft and self-review. Orchestrator reviews and directly revises the same spec or plan when needed. A missing or failed first draft does not transfer initial authorship to Orchestrator. New product decisions outside approved scope require user input before Orchestrator revises the document. For design and brainstorming, allow the initial draft and one consolidated correction. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Repeated unmet criteria without new input, evidence or progress hold the stage; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.",
376
376
  "controls": {
377
377
  "codex": "prompt",
378
378
  "claude": "prompt"
@@ -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 set(config) != {"version", "preset", "roles"} or type(config["version"]) is not int or config["version"] != 1:
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
- if not isinstance(config["preset"], str) or config["preset"] not in PRESETS:
83
- raise _error(path, "preset", "must be economy, balanced or smart")
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
- return config
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 = 1", f'preset = {json.dumps(config["preset"])}']
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
 
@@ -39,5 +39,5 @@ export function renderWaitSettings(original){
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, spec, plan, and brainstorming drafts are authored ONLY by the Planner. Orchestrator MUST NEVER author them; route drafting skills to the Planner.\n'+
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'+
43
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';}
@@ -1,7 +1,8 @@
1
1
  import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
2
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';
3
3
  import{validateModelConfig}from'../models/config.mjs';
4
- import path from'node:path';import{loadModelConfig,applyPreset,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
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';
5
6
  function fail(message){throw new TargetConfigurationError(message);}
6
7
  export function previewLimits({target,safeFs}){
7
8
  const snapshot=new Map(),filename=path.join(target.home,'skills/arc/limits.toml');
@@ -15,9 +16,9 @@ export function previewLanguage({target,safeFs}){
15
16
  let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
16
17
  return{existing,snapshot};
17
18
  }
18
- function renderModelAssets({payload,target,preset,modelConfig,language}){
19
+ function renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog}){
19
20
  const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
20
- const config=modelConfig===undefined?applyPreset(loadModelConfig(input.models,target.platform),preset):validateModelConfig(modelConfig);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));
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));
21
22
  return{profiles,modelBytes};
22
23
  }
23
24
  function renderSkillPayload({payload,target,language,modelBytes,limits}){
@@ -45,14 +46,14 @@ function renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurre
45
46
  }
46
47
  return globalChanges;
47
48
  }
48
- 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}){
49
50
  validateLanguage(language);
50
51
  if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
51
52
  const languageState=languagePreview??previewLanguage({target,safeFs});
52
53
  if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
53
54
  const preview=limitsPreview??previewLimits({target,safeFs});
54
55
  limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
55
- const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language});
56
+ const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language,customRoles,preservedCustomRoles,verifiedCatalog});
56
57
  const skill=renderSkillPayload({payload,target,language,modelBytes,limits});
57
58
  const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
58
59
  for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
@@ -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),next=applyPreset(config,preset),originalProfiles=new Map(target.profileNames.map((name,i)=>[name,read(managedPaths[i+1])]));
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
  }
@@ -21,7 +21,7 @@ export async function prepareOperation(context,options,{selections=[]}={}){
21
21
  if(!['install','preset','uninstall'].includes(options.kind))throw Object.assign(Error('Unknown management operation'),{code:'ARC_CLI_INPUT'});
22
22
  if(options.kind==='uninstall')options={...options,removeCli:options.removeCli!==false};
23
23
  const platforms=options.target==='all'?['codex','claude']:[options.target],payload=options.kind==='install'?verifyPayload(context):null;
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});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,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)});});
25
25
  if(options.kind==='install')attachShellCompletionCleanup(plans,context);
26
26
  // Uninstall plans initially share the completion snapshot; its executor assigns one owner.
27
27
  if(options.kind!=='uninstall')validatePlanSet(plans);
@@ -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
+ }
@@ -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 === 'codex' && config.version === 1) return 'codex';
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
- exactKeys(config, ['platform', 'version', 'preset', 'roles'], 'model config'); const platform = platformOf(config);
34
- if (!PRESETS.includes(config.preset)) fail('Unsupported performance preset');
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
- return structuredClone(config);
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); exactKeys(config, ['version', 'preset', 'roles'], 'model file (reinstall or update legacy role-only settings)');
50
- if (config.version !== 1n) fail(`Expected ${platform} integer schema version 1; reinstall or update models.toml`);
51
- return validateModelConfig({ platform, version: 1, preset: config.preset, roles: config.roles });
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 { version, preset, roles } = validateModelConfig(config); return stringify({ version, preset, roles }); }
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
+ }
@@ -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';
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts only to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation remains limited to the existing small, low-risk path. Use a fresh Implementer per top-level task and a fresh QA per task requiring QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and assignment context remain valid, default same-task corrections to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only for invalid/unavailable context or a changed task boundary, except that an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the same task. Before Implementer reuse, refresh actual workspace, branch/HEAD and change state. QA reruns reproduction and invalidated related checks and reports actor, target state, outcomes and limits. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
153
+ "text": "Assign design/brainstorming drafts and first complete spec/plan drafts only to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves and reviews complete spec/plan drafts and directly revises the same document when needed. Missing or failed first drafts do not transfer initial authorship. New product decisions outside approved scope require user input before revision. Orchestrator owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation remains limited to the existing small, low-risk path. Use a fresh Implementer per top-level task and a fresh QA per task requiring QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and assignment context remain valid, default same-task corrections to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only for invalid/unavailable context or a changed task boundary, except that an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the same task. Before Implementer reuse, refresh actual workspace, branch/HEAD and change state. QA reruns reproduction and invalidated related checks and reports actor, target state, outcomes and limits. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts with dependencies, criteria, holds and verification needs in the response. During initial drafting and revisions, report progress only to Orchestrator: briefly state the stage, feedback being applied, actual blocker or next step, without draft prose or excerpts. Return the complete draft and self-review for review and correction. Raw responses may appear in app/tool surfaces; prompt policy cannot hide them. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting when actual prepared-target evidence is missing. Route decisions to Orchestrator.",
193
+ "text": "Return design/brainstorming drafts and complete first spec/plan drafts with dependencies, criteria, holds and verification needs in the response. During initial drafting and revisions, report progress only to Orchestrator: briefly state the stage, feedback being applied, actual blocker or next step, without draft prose or excerpts. Return the complete first draft and self-review for Orchestrator review; Orchestrator directly revises the same spec or plan when needed. Raw responses may appear in app/tool surfaces; prompt policy cannot hide them. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting when actual prepared-target evidence is missing. Route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -372,7 +372,7 @@
372
372
  "preceding-approval"
373
373
  ],
374
374
  "authority": "orchestrator",
375
- "text": "Follow planning.md's Planner drafting preparation and consolidated feedback procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Allow the initial draft and one consolidated correction. Send one complete stage-specific initial brief. Planner returns a complete proportional draft and self-review. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Hold repeated unmet criteria without new input, evidence or progress; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.",
375
+ "text": "Follow planning.md's Planner drafting preparation and review procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Send one complete stage-specific initial brief. Planner returns a complete proportional first draft and self-review. Orchestrator reviews and directly revises the same spec or plan when needed. A missing or failed first draft does not transfer initial authorship to Orchestrator. New product decisions outside approved scope require user input before Orchestrator revises the document. For design and brainstorming, allow the initial draft and one consolidated correction. Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Further calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress. State changed input, target criterion, expected artifact and expected progress. Repeated unmet criteria without new input, evidence or progress hold the stage; report the criterion, observed evidence and exact unblock input/decision. Do not split omissions, repeat resolved items or count rewording/agent replacement as progress. Do not invoke Planner only for reformatting, re-expression, save errors or patch-application errors; Orchestrator handles these. Evaluate separate substantive work under the normal Planner gate. Preserve separate sequential design/spec/plan approvals. Add no fixed call cap or profile call-count conditions, and never accept an incomplete draft. Keep user progress brief and based on Planner status without relaying draft content. At approval, present the integrated, saved and reviewed Markdown without repeating its body in chat.",
376
376
  "controls": {
377
377
  "codex": "prompt",
378
378
  "claude": "prompt"