adaptive-director-skill 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -174,8 +174,10 @@ adaptive-director help # Show usage
174
174
 
175
175
  ## Core Principles
176
176
 
177
- - **Routing Priority:** User overrides Delegate fleet lanes Capability registry Default fallback.
177
+ - **Dynamic Discovery:** Discovers installed host CLIs and inspects locally available models at runtime rather than assuming static pairings.
178
+ - **Routing Priority:** User overrides → Delegate fleet lanes → Dynamic Host Discovery → Capability registry → Default fallback.
178
179
  - **Independent Review:** The coder never reviews its own work.
180
+ - **Verification Dimension:** Independent verification requires dedicated verification capabilities (`min_verification: 3`).
179
181
  - **Max Reasoning Opt-in:** `max` effort is locked by default; requires `--allow-max`.
180
182
  - **Runaway Loop Protection:** Maximum 1 automated fix cycle before alerting the user.
181
183
  - **Context Isolation:** Each agent receives only a self-contained brief on disk (`.adaptive-director/runs/`), preventing context window bloat.
@@ -183,6 +185,31 @@ adaptive-director help # Show usage
183
185
 
184
186
  ---
185
187
 
188
+ ## Dynamic Model Discovery & Heuristics
189
+
190
+ Adaptive Director does **not** rely on rigid, hardcoded host-to-model pairings. Real-world model availability depends on local CLI versions, host configurations, and account subscriptions.
191
+
192
+ ### Routing Pipeline
193
+
194
+ ```text
195
+ Detect Host CLI
196
+
197
+ Discover Locally Available Models (cache / config / env)
198
+
199
+ Resolve Aliases (e.g. 'astra' → 'gpt-6-astra')
200
+
201
+ Intersect with Registry Capabilities & Phase Requirements
202
+
203
+ Score Compatible Candidates for Current Phase
204
+
205
+ Route to Optimal Candidate (with Graceful Fallback)
206
+ ```
207
+
208
+ > [!NOTE]
209
+ > **Heuristic Calibration Disclaimer:** Capability scores (1–5) in `data/registry.json` represent internal routing heuristics and priors calibrated for phase allocation. They are **not** vendor laboratory benchmarks or absolute leaderboards.
210
+
211
+ ---
212
+
186
213
  ## Configuration (`~/.adaptive-director/config.json`)
187
214
 
188
215
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adaptive-director-skill",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Adaptive Director Skill — Skill-first adaptive multi-agent orchestration for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/cli.mjs CHANGED
@@ -18,7 +18,7 @@ const commands = {
18
18
  };
19
19
 
20
20
  if (command === 'help' || command === '--help' || command === '-h') {
21
- console.log(`Adaptive Director Skill CLI v1.0.0
21
+ console.log(`Adaptive Director Skill CLI v1.1.0
22
22
 
23
23
  Usage:
24
24
  adaptive-director <command>
@@ -16,6 +16,7 @@ import { execFileSync, execSync } from 'node:child_process'
16
16
  import { existsSync, readFileSync } from 'node:fs'
17
17
  import { join } from 'node:path'
18
18
  import { homedir } from 'node:os'
19
+ import { discoverHostModels } from '../skills/adaptive-director/scripts/discover-models.mjs'
19
20
 
20
21
  // ─── Known agents ─────────────────────────────────────────────────────────────
21
22
 
@@ -88,19 +89,83 @@ function readFleet(path) {
88
89
  }
89
90
  }
90
91
 
92
+ function readJsonConfig(path) {
93
+ if (!existsSync(path)) return null
94
+ try {
95
+ const raw = JSON.parse(readFileSync(path, 'utf8'))
96
+ if (raw.lanes && typeof raw.lanes === 'object') {
97
+ const result = {}
98
+ for (const [lane, def] of Object.entries(raw.lanes)) {
99
+ result[lane] = {
100
+ agent: def.implementer ?? def.agent,
101
+ model: def.model ?? null,
102
+ effort: def.effort ?? def.variant ?? null,
103
+ implementer: def.implementer ?? def.agent,
104
+ }
105
+ }
106
+ return Object.keys(result).length > 0 ? result : null
107
+ }
108
+ } catch {}
109
+ return null
110
+ }
111
+
91
112
  function discoverDelegate() {
92
- const globalPath = join(homedir(), '.delegate', 'fleet.yaml')
93
- const projectPath = join(process.cwd(), '.delegate', 'fleet.yaml')
113
+ const paths = [
114
+ join(process.cwd(), '.delegate', 'fleet.yaml'),
115
+ join(homedir(), '.delegate', 'fleet.yaml'),
116
+ join(process.cwd(), '.delegate', 'config.json'),
117
+ join(homedir(), '.config', 'delegate-skills', 'config.json'),
118
+ ]
119
+
120
+ let lanes = {}
121
+ for (const p of paths) {
122
+ if (p.endsWith('.json')) {
123
+ const jsonLanes = readJsonConfig(p)
124
+ if (jsonLanes) lanes = { ...lanes, ...jsonLanes }
125
+ } else {
126
+ const fleetLanes = readFleet(p)
127
+ if (fleetLanes) lanes = { ...lanes, ...fleetLanes }
128
+ }
129
+ }
130
+
131
+ // Check if delegate-skills is installed on disk
132
+ const agentsSkillsDir = join(homedir(), '.agents', 'skills')
133
+ const codexSkillsDir = join(homedir(), '.codex', 'skills')
134
+ const hasAgentsSkills = existsSync(join(agentsSkillsDir, 'delegate-setup')) ||
135
+ existsSync(join(agentsSkillsDir, 'codex-delegate')) ||
136
+ existsSync(join(agentsSkillsDir, 'agy-delegate')) ||
137
+ existsSync(join(codexSkillsDir, 'delegate-review-loop'))
138
+
139
+ let hasSkillLock = false
140
+ const skillLockPath = join(homedir(), '.agents', '.skill-lock.json')
141
+ if (existsSync(skillLockPath)) {
142
+ try {
143
+ const lockContent = readFileSync(skillLockPath, 'utf8')
144
+ if (lockContent.includes('delegate-skills')) hasSkillLock = true
145
+ } catch {}
146
+ }
94
147
 
95
- const globalLanes = readFleet(globalPath)
96
- const projectLanes = readFleet(projectPath)
148
+ const installed = Object.keys(lanes).length > 0 || hasAgentsSkills || hasSkillLock
97
149
 
98
- if (!globalLanes && !projectLanes) {
99
- return { installed: false, lanes: {} }
150
+ // If installed but no explicit fleet lanes file configured, synthesize lanes from installed delegate skills
151
+ if (installed && Object.keys(lanes).length === 0 && hasAgentsSkills) {
152
+ if (existsSync(join(agentsSkillsDir, 'codex-delegate'))) {
153
+ lanes.feature = { agent: 'codex', implementer: 'codex' }
154
+ lanes.implement = { agent: 'codex', implementer: 'codex' }
155
+ lanes.fix = { agent: 'codex', implementer: 'codex' }
156
+ }
157
+ if (existsSync(join(agentsSkillsDir, 'agy-delegate'))) {
158
+ lanes.plan = { agent: 'agy', implementer: 'agy' }
159
+ lanes.review = { agent: 'agy', implementer: 'agy' }
160
+ lanes.verify = { agent: 'agy', implementer: 'agy' }
161
+ }
100
162
  }
101
163
 
102
- const merged = { ...(globalLanes ?? {}), ...(projectLanes ?? {}) }
103
- return { installed: true, lanes: merged }
164
+ return {
165
+ installed,
166
+ lanes,
167
+ source: hasSkillLock ? 'amElnagdy/delegate-skills' : (hasAgentsSkills ? 'local-skills' : (Object.keys(lanes).length > 0 ? 'fleet-config' : null))
168
+ }
104
169
  }
105
170
 
106
171
  // ─── Main ─────────────────────────────────────────────────────────────────────
@@ -128,11 +193,12 @@ function discover() {
128
193
  }
129
194
 
130
195
  agents[desc.id] = {
131
- installed: true,
132
- available: version !== null,
133
- version: version ?? 'unknown',
134
- skillPath: detectedSkillPath,
135
- hasSkill: detectedSkillPath ? existsSync(join(detectedSkillPath, 'adaptive-director')) || existsSync(join(detectedSkillPath, 'Adaptive-Director')) || existsSync(join(detectedSkillPath, 'adaptive-director-skill')) : false
196
+ installed: true,
197
+ available: version !== null,
198
+ version: version ?? 'unknown',
199
+ skillPath: detectedSkillPath,
200
+ hasSkill: detectedSkillPath ? existsSync(join(detectedSkillPath, 'adaptive-director')) || existsSync(join(detectedSkillPath, 'Adaptive-Director')) || existsSync(join(detectedSkillPath, 'adaptive-director-skill')) : false,
201
+ availableModels: discoverHostModels(desc.id),
136
202
  }
137
203
  }
138
204
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Adaptive multi-agent director for coding tasks. Use this skill whenever the user
5
5
  asks to implement features, fix complex bugs, refactor architecture, or coordinate
6
6
  multiple coding agents across planning, implementation, independent review, and verification phases.
7
- version: 1.0.0
7
+ version: 1.1.0
8
8
  ---
9
9
 
10
10
  # Adaptive Director Skill
@@ -1,32 +1,85 @@
1
1
  {
2
+ "meta": {
3
+ "score_source": "adaptive-director-heuristic",
4
+ "confidence": "maintainer-default",
5
+ "score_type": "routing-prior",
6
+ "description": "Capability scores (1-5) are internal routing heuristics calibrated by Adaptive Director for phase allocation, not official vendor laboratory benchmarks."
7
+ },
2
8
  "models": {
3
- "gpt-6-astra": { "planning": 5, "coding": 5, "review": 5 },
4
- "gpt-5.6-sol": { "planning": 5, "coding": 5, "review": 5 },
5
- "gpt-5.5": { "planning": 5, "coding": 5, "review": 4 },
6
- "gpt-5.4-mini": { "planning": 3, "coding": 4, "review": 3 },
7
- "o3": { "planning": 5, "coding": 5, "review": 4 },
8
- "o3-mini": { "planning": 5, "coding": 5, "review": 4 },
9
- "o1": { "planning": 5, "coding": 4, "review": 4 },
10
- "gpt-4o": { "planning": 3, "coding": 4, "review": 3 },
11
- "gpt-4o-mini": { "planning": 2, "coding": 3, "review": 2 },
12
- "claude-3-7-sonnet": { "planning": 5, "coding": 5, "review": 5 },
13
- "claude-3-5-sonnet": { "planning": 5, "coding": 5, "review": 4 },
14
- "claude-3-opus": { "planning": 5, "coding": 4, "review": 5 },
15
- "claude-3-5-haiku": { "planning": 2, "coding": 3, "review": 2 },
16
- "deepseek-r1": { "planning": 5, "coding": 4, "review": 4 },
17
- "deepseek-v3": { "planning": 3, "coding": 4, "review": 3 },
18
- "gemini-2-5-pro": { "planning": 5, "coding": 4, "review": 4 },
19
- "gemini-2-0-pro": { "planning": 5, "coding": 4, "review": 4 },
20
- "gemini-2-0-flash": { "planning": 2, "coding": 3, "review": 2 },
21
- "codex-default": { "planning": 5, "coding": 5, "review": 4 },
22
- "unknown": { "planning": 1, "coding": 1, "review": 1 }
9
+ "gpt-6-astra": { "planning": 5, "coding": 5, "review": 5, "verification": 5 },
10
+ "gpt-5.6-sol": { "planning": 5, "coding": 5, "review": 5, "verification": 4 },
11
+ "gpt-5.6-terra": { "planning": 4, "coding": 5, "review": 4, "verification": 4 },
12
+ "gpt-5.6-luna": { "planning": 3, "coding": 4, "review": 3, "verification": 3 },
13
+ "gpt-oss-120b": { "planning": 3, "coding": 3, "review": 3, "verification": 3 },
14
+ "gpt-5.5": { "planning": 5, "coding": 5, "review": 4, "verification": 4 },
15
+ "gpt-5.4-mini": { "planning": 3, "coding": 4, "review": 3, "verification": 3 },
16
+ "o3": { "planning": 5, "coding": 5, "review": 4, "verification": 4 },
17
+ "o3-mini": { "planning": 5, "coding": 5, "review": 4, "verification": 4 },
18
+ "o1": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
19
+ "gpt-4o": { "planning": 3, "coding": 4, "review": 3, "verification": 3 },
20
+ "gpt-4o-mini": { "planning": 2, "coding": 3, "review": 2, "verification": 2 },
21
+
22
+ "claude-fable-5-1": { "planning": 5, "coding": 5, "review": 5, "verification": 5 },
23
+ "claude-opus-5": { "planning": 5, "coding": 5, "review": 5, "verification": 5 },
24
+ "claude-sonnet-5": { "planning": 4, "coding": 5, "review": 4, "verification": 4 },
25
+ "claude-haiku-4-5": { "planning": 3, "coding": 4, "review": 3, "verification": 3 },
26
+ "claude-sonnet-4-6": { "planning": 5, "coding": 5, "review": 5, "verification": 4 },
27
+ "claude-3-7-sonnet": { "planning": 5, "coding": 5, "review": 5, "verification": 4 },
28
+ "claude-3-5-sonnet": { "planning": 5, "coding": 5, "review": 4, "verification": 4 },
29
+ "claude-3-opus": { "planning": 5, "coding": 4, "review": 5, "verification": 4 },
30
+ "claude-3-5-haiku": { "planning": 2, "coding": 3, "review": 2, "verification": 2 },
31
+
32
+ "gemini-3.8-flash": { "planning": 4, "coding": 4, "review": 4, "verification": 4 },
33
+ "gemini-3-8-flash": { "planning": 4, "coding": 4, "review": 4, "verification": 4 },
34
+ "gemini-3.1-pro": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
35
+ "gemini-3-1-pro": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
36
+ "gemini-2-5-pro": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
37
+ "gemini-2-0-pro": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
38
+ "gemini-2-0-flash": { "planning": 2, "coding": 3, "review": 2, "verification": 2 },
39
+
40
+ "glm-5.3": { "planning": 5, "coding": 5, "review": 4, "verification": 5 },
41
+ "glm-5-3": { "planning": 5, "coding": 5, "review": 4, "verification": 5 },
42
+ "glm-5.3-flash": { "planning": 4, "coding": 5, "review": 4, "verification": 5 },
43
+ "glm-5-3-flash": { "planning": 4, "coding": 5, "review": 4, "verification": 5 },
44
+ "glm-5.2": { "planning": 4, "coding": 5, "review": 4, "verification": 4 },
45
+
46
+ "deepseek-v4-pro": { "planning": 5, "coding": 5, "review": 4, "verification": 4 },
47
+ "deepseek-flash": { "planning": 4, "coding": 4, "review": 4, "verification": 4 },
48
+ "deepseek-r1": { "planning": 5, "coding": 4, "review": 4, "verification": 4 },
49
+ "deepseek-v3": { "planning": 3, "coding": 4, "review": 3, "verification": 3 },
50
+
51
+ "codex-default": { "planning": 5, "coding": 5, "review": 5, "verification": 5 },
52
+ "unknown": { "planning": 1, "coding": 1, "review": 1, "verification": 1 }
53
+ },
54
+ "aliases": {
55
+ "astra": "gpt-6-astra",
56
+ "gpt-6": "gpt-6-astra",
57
+ "sol": "gpt-5.6-sol",
58
+ "terra": "gpt-5.6-terra",
59
+ "luna": "gpt-5.6-luna",
60
+ "fable": "claude-fable-5-1",
61
+ "fable-5.1": "claude-fable-5-1",
62
+ "deepseek-v4-flash": "deepseek-flash",
63
+ "deepseek-v4-flash-vision-exp": "deepseek-flash",
64
+ "composer-2.5": "claude-sonnet-5",
65
+ "gemini-3.8-flash-high": "gemini-3.8-flash",
66
+ "gemini-3.8-flash-medium": "gemini-3.8-flash",
67
+ "gemini-3.8-flash-low": "gemini-3.8-flash",
68
+ "gemini-3.7-flash-high": "gemini-3.8-flash",
69
+ "gemini-3.7-flash-medium": "gemini-3.8-flash",
70
+ "gemini-3.7-flash-low": "gemini-3.8-flash",
71
+ "gemini-3.1-pro-high": "gemini-3.1-pro",
72
+ "gemini-3.1-pro-low": "gemini-3.1-pro",
73
+ "claude-opus-4-6-thinking": "claude-3-opus",
74
+ "gpt-oss-120b-medium": "gpt-oss-120b",
75
+ "gpt-reserve": "gpt-6-astra"
23
76
  },
24
77
  "phase_requirements": {
25
78
  "plan": { "min_planning": 3 },
26
79
  "implement": { "min_coding": 2 },
27
80
  "review": { "min_review": 3 },
28
81
  "fix": { "min_coding": 2 },
29
- "verify": { "min_planning": 2 }
82
+ "verify": { "min_verification": 3 }
30
83
  },
31
84
  "effort_table": {
32
85
  "conservative": {
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * discover-models.mjs
4
+ * ───────────────────
5
+ * Dynamic runtime host and model discovery for Adaptive Director.
6
+ * Detects installed agent CLIs and queries locally configured/cached models.
7
+ * Zero external dependencies. Ultra-fast (< 15ms).
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'node:fs'
11
+ import { join, delimiter } from 'node:path'
12
+ import { homedir } from 'node:os'
13
+
14
+ export const KNOWN_AGENTS = [
15
+ { id: 'agy', bins: ['agy'], skillPaths: ['.gemini/antigravity/builtin/skills', '.gemini/antigravity/skills'] },
16
+ { id: 'codex', bins: ['codex'], skillPaths: ['.codex/skills'] },
17
+ { id: 'claude', bins: ['claude'], skillPaths: ['.claude/skills', '.config/claude/skills'] },
18
+ { id: 'gemini', bins: ['gemini'], skillPaths: ['.gemini/skills'] },
19
+ { id: 'opencode', bins: ['opencode'], skillPaths: ['.opencode/skills'] },
20
+ { id: 'aider', bins: ['aider'], skillPaths: ['.aider/skills'] },
21
+ { id: 'cursor', bins: ['cursor-agent', 'cursor'], skillPaths: ['.cursor/skills'] },
22
+ { id: 'cline', bins: ['cline'], skillPaths: ['.cline/skills'] },
23
+ { id: 'copilot', bins: ['copilot', 'github-copilot-cli'], skillPaths: ['.copilot/skills'] },
24
+ ]
25
+
26
+ export function isBinaryInPath(bin) {
27
+ const pathDirs = (process.env.PATH || '').split(delimiter)
28
+ const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : ['']
29
+ for (const dir of pathDirs) {
30
+ if (!dir) continue
31
+ for (const ext of exts) {
32
+ try {
33
+ const candidate = join(dir, bin + ext)
34
+ if (existsSync(candidate)) return true
35
+ } catch {
36
+ continue
37
+ }
38
+ }
39
+ }
40
+ return false
41
+ }
42
+
43
+ export function isAgentInstalled(agentId, userConfig = {}) {
44
+ if (userConfig.hosts?.[agentId]?.installed !== undefined) {
45
+ return Boolean(userConfig.hosts[agentId].installed)
46
+ }
47
+ const meta = KNOWN_AGENTS.find(a => a.id === agentId)
48
+ if (!meta) return false
49
+
50
+ // Check if binary is in PATH
51
+ for (const bin of meta.bins) {
52
+ if (isBinaryInPath(bin)) return true
53
+ }
54
+
55
+ return false
56
+ }
57
+
58
+ export function discoverHostModels(agentId, userConfig = {}) {
59
+ // 1. Explicit user config override takes precedence
60
+ const explicit = userConfig.hosts?.[agentId]?.models || userConfig.hosts?.[agentId]?.available_models
61
+ if (Array.isArray(explicit) && explicit.length > 0) {
62
+ return explicit
63
+ }
64
+
65
+ const models = new Set()
66
+
67
+ switch (agentId) {
68
+ case 'codex': {
69
+ // Check .codex/models_cache.json
70
+ const cachePath = join(homedir(), '.codex', 'models_cache.json')
71
+ if (existsSync(cachePath)) {
72
+ try {
73
+ const raw = JSON.parse(readFileSync(cachePath, 'utf8'))
74
+ const list = Array.isArray(raw) ? raw : (Array.isArray(raw.models) ? raw.models : Object.keys(raw.models || {}))
75
+ for (const item of list) {
76
+ const slug = typeof item === 'string' ? item : (item.slug || item.id || item.model || item.name)
77
+ if (slug && !slug.toLowerCase().includes('review') && !slug.toLowerCase().includes('auto')) {
78
+ models.add(slug)
79
+ }
80
+ }
81
+ } catch {}
82
+ }
83
+ // Check .codex/config.toml
84
+ const tomlPath = join(homedir(), '.codex', 'config.toml')
85
+ if (existsSync(tomlPath)) {
86
+ try {
87
+ const text = readFileSync(tomlPath, 'utf8')
88
+ for (const line of text.split('\n')) {
89
+ const parts = line.split('=')
90
+ if (parts.length >= 2 && parts[0].trim() === 'model') {
91
+ const val = parts[1].trim().replace(/^['"]|['"]$/g, '')
92
+ if (val) models.add(val)
93
+ }
94
+ }
95
+ } catch {}
96
+ }
97
+ if (process.env.CODEX_MODEL) models.add(process.env.CODEX_MODEL.trim())
98
+ break
99
+ }
100
+
101
+ case 'agy': {
102
+ // Known models provided natively by Antigravity / Gemini ecosystem
103
+ const knownAgy = [
104
+ 'gemini-3.8-flash',
105
+ 'gemini-3.1-pro',
106
+ 'claude-sonnet-4-6',
107
+ 'claude-opus-4-6-thinking',
108
+ 'gpt-oss-120b',
109
+ 'gemini-2-5-pro'
110
+ ]
111
+ for (const m of knownAgy) models.add(m)
112
+ if (process.env.AGY_MODEL) models.add(process.env.AGY_MODEL.trim())
113
+ if (process.env.ANTIGRAVITY_MODEL) models.add(process.env.ANTIGRAVITY_MODEL.trim())
114
+ break
115
+ }
116
+
117
+ case 'claude': {
118
+ const conf = join(homedir(), '.claude', 'config.json')
119
+ if (existsSync(conf)) {
120
+ try {
121
+ const d = JSON.parse(readFileSync(conf, 'utf8'))
122
+ if (d.model) models.add(d.model)
123
+ } catch {}
124
+ }
125
+ if (process.env.CLAUDE_MODEL) models.add(process.env.CLAUDE_MODEL.trim())
126
+ if (process.env.ANTHROPIC_MODEL) models.add(process.env.ANTHROPIC_MODEL.trim())
127
+ break
128
+ }
129
+
130
+ case 'gemini': {
131
+ const conf = join(homedir(), '.gemini', 'config.json')
132
+ if (existsSync(conf)) {
133
+ try {
134
+ const d = JSON.parse(readFileSync(conf, 'utf8'))
135
+ if (d.model) models.add(d.model)
136
+ } catch {}
137
+ }
138
+ if (process.env.GEMINI_MODEL) models.add(process.env.GEMINI_MODEL.trim())
139
+ break
140
+ }
141
+
142
+ case 'opencode': {
143
+ const conf = join(homedir(), '.config', 'opencode', 'config.json')
144
+ if (existsSync(conf)) {
145
+ try {
146
+ const d = JSON.parse(readFileSync(conf, 'utf8'))
147
+ if (d.model) models.add(d.model)
148
+ } catch {}
149
+ }
150
+ if (process.env.OPENCODE_MODEL) models.add(process.env.OPENCODE_MODEL.trim())
151
+ break
152
+ }
153
+
154
+ case 'cursor': {
155
+ if (process.env.CURSOR_MODEL) models.add(process.env.CURSOR_MODEL.trim())
156
+ break
157
+ }
158
+
159
+ case 'aider': {
160
+ const conf = join(homedir(), '.aider.conf.yml')
161
+ if (existsSync(conf)) {
162
+ try {
163
+ const text = readFileSync(conf, 'utf8')
164
+ for (const line of text.split('\n')) {
165
+ const parts = line.split(':')
166
+ if (parts.length >= 2 && parts[0].trim() === 'model') {
167
+ const val = parts.slice(1).join(':').trim()
168
+ if (val) models.add(val)
169
+ }
170
+ }
171
+ } catch {}
172
+ }
173
+ if (process.env.AIDER_MODEL) models.add(process.env.AIDER_MODEL.trim())
174
+ break
175
+ }
176
+
177
+ case 'copilot': {
178
+ if (process.env.COPILOT_MODEL) models.add(process.env.COPILOT_MODEL.trim())
179
+ break
180
+ }
181
+ }
182
+
183
+ return Array.from(models)
184
+ }
185
+
186
+ export function discoverAllInstalledAgents(userConfig = {}) {
187
+ const result = {}
188
+ for (const agent of KNOWN_AGENTS) {
189
+ const installed = isAgentInstalled(agent.id)
190
+ if (installed) {
191
+ result[agent.id] = {
192
+ installed: true,
193
+ availableModels: discoverHostModels(agent.id, userConfig),
194
+ }
195
+ }
196
+ }
197
+ return result
198
+ }
@@ -32,6 +32,7 @@ import { existsSync, readFileSync } from 'node:fs'
32
32
  import { join, dirname, resolve } from 'node:path'
33
33
  import { homedir } from 'node:os'
34
34
  import { fileURLToPath } from 'node:url'
35
+ import { discoverAllInstalledAgents } from './discover-models.mjs'
35
36
 
36
37
  const __filename = fileURLToPath(import.meta.url)
37
38
  const __dirname = dirname(__filename)
@@ -70,79 +71,125 @@ function loadDelegateLanes() {
70
71
  const paths = [
71
72
  join(process.cwd(), '.delegate', 'fleet.yaml'),
72
73
  join(homedir(), '.delegate', 'fleet.yaml'),
74
+ join(process.cwd(), '.delegate', 'config.json'),
75
+ join(homedir(), '.config', 'delegate-skills', 'config.json'),
73
76
  ]
74
77
 
78
+ const lanes = {}
79
+
75
80
  for (const p of paths) {
76
81
  if (!existsSync(p)) continue
77
82
  try {
78
- const raw = readFileSync(p, 'utf8')
79
- const lanes = {}
80
- let current = null
81
- for (const line of raw.split('\n')) {
82
- const laneMatch = line.match(/^ (\w[\w-]*):\s*$/)
83
- const implMatch = line.match(/^\s+implementer:\s*(.+)$/)
84
- const modelMatch = line.match(/^\s+model:\s*(.+)$/)
85
- const effortMatch = line.match(/^\s+effort:\s*(.+)$/)
86
- if (laneMatch) { current = laneMatch[1]; lanes[current] = {} }
87
- if (current && implMatch) lanes[current].agent = implMatch[1].trim()
88
- if (current && modelMatch) lanes[current].model = modelMatch[1].trim()
89
- if (current && effortMatch) lanes[current].effort = effortMatch[1].trim()
83
+ if (p.endsWith('.json')) {
84
+ const raw = JSON.parse(readFileSync(p, 'utf8'))
85
+ if (raw.lanes && typeof raw.lanes === 'object') {
86
+ for (const [lane, def] of Object.entries(raw.lanes)) {
87
+ lanes[lane] = {
88
+ agent: def.implementer ?? def.agent,
89
+ model: def.model ?? null,
90
+ effort: def.effort ?? def.variant ?? null,
91
+ implementer: def.implementer ?? def.agent,
92
+ }
93
+ }
94
+ }
95
+ } else {
96
+ const raw = readFileSync(p, 'utf8')
97
+ let current = null
98
+ for (const line of raw.split('\n')) {
99
+ const laneMatch = line.match(/^ (\w[\w-]*):\s*$/)
100
+ const implMatch = line.match(/^\s+implementer:\s*(.+)$/)
101
+ const modelMatch = line.match(/^\s+model:\s*(.+)$/)
102
+ const effortMatch = line.match(/^\s+effort:\s*(.+)$/)
103
+ if (laneMatch) { current = laneMatch[1]; lanes[current] = {} }
104
+ if (current && implMatch) lanes[current].agent = implMatch[1].trim()
105
+ if (current && modelMatch) lanes[current].model = modelMatch[1].trim()
106
+ if (current && effortMatch) lanes[current].effort = effortMatch[1].trim()
107
+ }
90
108
  }
91
- return lanes
92
109
  } catch { continue }
93
110
  }
94
- return {}
111
+
112
+ // If no explicit lanes found, check installed delegate skills
113
+ if (Object.keys(lanes).length === 0) {
114
+ const agentsSkillsDir = join(homedir(), '.agents', 'skills')
115
+ if (existsSync(join(agentsSkillsDir, 'codex-delegate'))) {
116
+ lanes.feature = { agent: 'codex', implementer: 'codex' }
117
+ lanes.implement = { agent: 'codex', implementer: 'codex' }
118
+ lanes.fix = { agent: 'codex', implementer: 'codex' }
119
+ }
120
+ if (existsSync(join(agentsSkillsDir, 'agy-delegate'))) {
121
+ lanes.plan = { agent: 'agy', implementer: 'agy' }
122
+ lanes.review = { agent: 'agy', implementer: 'agy' }
123
+ lanes.verify = { agent: 'agy', implementer: 'agy' }
124
+ }
125
+ }
126
+
127
+ return lanes
95
128
  }
96
129
 
97
130
  // ─── Scoring ──────────────────────────────────────────────────────────────────
98
131
 
99
- function modelScore(registry, modelId) {
100
- if (!modelId) return registry.models['unknown'] ?? { planning: 1, coding: 1, review: 1 }
132
+ function resolveModelAlias(registry, modelId) {
133
+ if (!modelId) return 'unknown'
101
134
  const lower = modelId.toLowerCase()
102
- const withHyphen = lower.replace(/\./g, '-')
103
- const withDot = lower.replace(/-/g, '.')
104
- return (
105
- registry.models[modelId] ??
106
- registry.models[lower] ??
135
+ return registry.aliases?.[modelId] ?? registry.aliases?.[lower] ?? modelId
136
+ }
137
+
138
+ function modelScore(registry, modelId) {
139
+ if (!modelId) return registry.models['unknown'] ?? { planning: 1, coding: 1, review: 1, verification: 1 }
140
+ const resolved = resolveModelAlias(registry, modelId)
141
+ const rLower = resolved.toLowerCase()
142
+ const withHyphen = rLower.replace(/\./g, '-')
143
+ const withDot = rLower.replace(/-/g, '.')
144
+ const base = (
145
+ registry.models[resolved] ??
146
+ registry.models[rLower] ??
107
147
  registry.models[withHyphen] ??
108
148
  registry.models[withDot] ??
109
149
  registry.models['unknown'] ??
110
- { planning: 1, coding: 1, review: 1 }
150
+ { planning: 1, coding: 1, review: 1, verification: 1 }
111
151
  )
152
+ return {
153
+ ...base,
154
+ verification: base.verification ?? base.review ?? base.planning ?? 1,
155
+ }
112
156
  }
113
157
 
114
158
  function meetsRequirement(registry, modelId, phase) {
115
159
  const score = modelScore(registry, modelId)
116
160
  const req = registry.phase_requirements?.[phase] ?? {}
117
- if (req.min_planning && score.planning < req.min_planning) return false
118
- if (req.min_coding && score.coding < req.min_coding) return false
119
- if (req.min_review && score.review < req.min_review) return false
161
+ if (req.min_planning && score.planning < req.min_planning) return false
162
+ if (req.min_coding && score.coding < req.min_coding) return false
163
+ if (req.min_review && score.review < req.min_review) return false
164
+ if (req.min_verification && score.verification < req.min_verification) return false
120
165
  return true
121
166
  }
122
167
 
123
168
  // Phase-relevant score for ranking
124
169
  function phaseScore(registry, modelId, phase) {
125
170
  const s = modelScore(registry, modelId)
126
- if (phase === 'plan' || phase === 'review' || phase === 'verify') return s.planning + s.review
171
+ if (phase === 'plan' || phase === 'review') return s.planning + s.review
172
+ if (phase === 'verify') return s.verification + s.planning
127
173
  return s.coding
128
174
  }
129
175
 
130
- // ─── Agent → representative model mapping ─────────────────────────────────────
176
+ // ─── Agent → representative model default hint ───────────────────────────────
177
+ // Default preference hint used when dynamic discovery is unavailable or unconfigured.
131
178
 
132
179
  const AGENT_MODEL_MAP = {
133
- claude: 'claude-3-7-sonnet',
134
- agy: 'claude-3-7-sonnet',
135
- codex: 'gpt-5.6-sol',
136
- gemini: 'gemini-2-5-pro',
137
- opencode: 'o3-mini',
138
- aider: 'deepseek-r1',
139
- cursor: 'claude-3-7-sonnet',
140
- cline: 'claude-3-7-sonnet',
141
- copilot: 'gpt-4o',
180
+ agy: 'gemini-3.8-flash',
181
+ codex: 'gpt-6-astra',
182
+ claude: 'claude-fable-5-1',
183
+ gemini: 'gemini-3.8-flash',
184
+ opencode: 'gpt-5.6-sol',
185
+ aider: 'glm-5.3',
186
+ cursor: 'claude-sonnet-5',
187
+ cline: 'claude-sonnet-5',
188
+ copilot: 'gpt-5.6-terra',
142
189
  }
143
190
 
144
191
  // Fallback priority order (best-to-acceptable)
145
- const AGENT_PRIORITY_FOR_PLANNING = ['claude', 'agy', 'gemini', 'opencode', 'cursor', 'cline', 'copilot', 'aider', 'codex']
192
+ const AGENT_PRIORITY_FOR_PLANNING = ['agy', 'claude', 'gemini', 'opencode', 'cursor', 'cline', 'copilot', 'aider', 'codex']
146
193
  const AGENT_PRIORITY_FOR_CODING = ['codex', 'aider', 'opencode', 'cursor', 'cline', 'claude', 'agy', 'copilot', 'gemini']
147
194
 
148
195
  // ─── Main routing logic ───────────────────────────────────────────────────────
@@ -183,22 +230,77 @@ function route(input) {
183
230
  }
184
231
  }
185
232
 
186
- // ── 3. Built-in registry: best available agent for phase ─────────────────
233
+ // ── 3. Dynamic Host & Model Discovery Pipeline ───────────────────────────
234
+ // Detect host → Detect actually available models → resolve aliases → intersect with registry → score compatible candidates → route → fallback
187
235
  const priorityList = (phase === 'plan' || phase === 'review' || phase === 'verify')
188
236
  ? AGENT_PRIORITY_FOR_PLANNING
189
237
  : AGENT_PRIORITY_FOR_CODING
190
238
 
239
+ const installedAgents = discoverAllInstalledAgents(userConfig)
240
+
241
+ // Collect candidate options from installed agents
242
+ const eligibleCandidates = []
243
+
244
+ for (const agentId of priorityList) {
245
+ if (!installedAgents[agentId]) continue
246
+
247
+ const availableModels = installedAgents[agentId].availableModels
248
+ if (Array.isArray(availableModels) && availableModels.length > 0) {
249
+ for (const rawModel of availableModels) {
250
+ const canonicalId = resolveModelAlias(registry, rawModel)
251
+ if (meetsRequirement(registry, canonicalId, phase)) {
252
+ eligibleCandidates.push({
253
+ agent: agentId,
254
+ model: canonicalId,
255
+ score: phaseScore(registry, canonicalId, phase),
256
+ source: 'dynamic_discovery',
257
+ })
258
+ }
259
+ }
260
+ } else {
261
+ // Installed agent but no local model cache found: check default hint
262
+ const hintModel = AGENT_MODEL_MAP[agentId] ?? 'unknown'
263
+ const canonicalId = resolveModelAlias(registry, hintModel)
264
+ if (meetsRequirement(registry, canonicalId, phase)) {
265
+ eligibleCandidates.push({
266
+ agent: agentId,
267
+ model: canonicalId,
268
+ score: phaseScore(registry, canonicalId, phase),
269
+ source: 'default_hint',
270
+ })
271
+ }
272
+ }
273
+ }
274
+
275
+ // If eligible candidates were found among installed agents:
276
+ if (eligibleCandidates.length > 0) {
277
+ // Sort by:
278
+ // 1. Agent priority index in priorityList (lower index = higher priority)
279
+ // 2. Model score descending
280
+ eligibleCandidates.sort((a, b) => {
281
+ const pA = priorityList.indexOf(a.agent)
282
+ const pB = priorityList.indexOf(b.agent)
283
+ if (pA !== pB) return pA - pB
284
+ return b.score - a.score
285
+ })
286
+
287
+ const best = eligibleCandidates[0]
288
+ const effort = computeEffort(registry, budget, phase, allowMax)
289
+ return { agent: best.agent, model: best.model, effort, execution: 'native' }
290
+ }
291
+
292
+ // ── 4. Fallback (if no installed agent meets requirement) ─────────────────
191
293
  for (const agentId of priorityList) {
192
- const modelId = AGENT_MODEL_MAP[agentId] ?? 'unknown'
193
- if (meetsRequirement(registry, modelId, phase)) {
294
+ const hintModel = AGENT_MODEL_MAP[agentId] ?? 'unknown'
295
+ const canonicalId = resolveModelAlias(registry, hintModel)
296
+ if (meetsRequirement(registry, canonicalId, phase)) {
194
297
  const effort = computeEffort(registry, budget, phase, allowMax)
195
- return { agent: agentId, model: modelId, effort, execution: 'native' }
298
+ return { agent: agentId, model: canonicalId, effort, execution: 'native' }
196
299
  }
197
300
  }
198
301
 
199
- // ── 4. Fallback ──────────────────────────────────────────────────────────
200
302
  const effort = computeEffort(registry, budget, phase, allowMax)
201
- return { agent: 'claude', model: null, effort, execution: 'native' }
303
+ return { agent: priorityList[0] ?? 'codex', model: null, effort, execution: 'native' }
202
304
  }
203
305
 
204
306
  function findLaneForPhase(lanes, phase) {