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 +28 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +1 -1
- package/scripts/discover.mjs +79 -13
- package/skills/adaptive-director/SKILL.md +1 -1
- package/skills/adaptive-director/data/registry.json +74 -21
- package/skills/adaptive-director/scripts/discover-models.mjs +198 -0
- package/skills/adaptive-director/scripts/route.mjs +145 -43
package/README.md
CHANGED
|
@@ -174,8 +174,10 @@ adaptive-director help # Show usage
|
|
|
174
174
|
|
|
175
175
|
## Core Principles
|
|
176
176
|
|
|
177
|
-
- **
|
|
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
package/scripts/cli.mjs
CHANGED
package/scripts/discover.mjs
CHANGED
|
@@ -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
|
|
93
|
-
|
|
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
|
|
96
|
-
const projectLanes = readFleet(projectPath)
|
|
148
|
+
const installed = Object.keys(lanes).length > 0 || hasAgentsSkills || hasSkillLock
|
|
97
149
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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:
|
|
132
|
-
available:
|
|
133
|
-
version:
|
|
134
|
-
skillPath:
|
|
135
|
-
hasSkill:
|
|
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.
|
|
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.
|
|
6
|
-
"gpt-5.
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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": { "
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
if (!modelId) return
|
|
132
|
+
function resolveModelAlias(registry, modelId) {
|
|
133
|
+
if (!modelId) return 'unknown'
|
|
101
134
|
const lower = modelId.toLowerCase()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
118
|
-
if (req.min_coding
|
|
119
|
-
if (req.min_review
|
|
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'
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
gemini: 'gemini-
|
|
137
|
-
opencode: '
|
|
138
|
-
aider: '
|
|
139
|
-
cursor: 'claude-
|
|
140
|
-
cline: 'claude-
|
|
141
|
-
copilot: 'gpt-
|
|
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 = ['
|
|
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.
|
|
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
|
|
193
|
-
|
|
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:
|
|
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: '
|
|
303
|
+
return { agent: priorityList[0] ?? 'codex', model: null, effort, execution: 'native' }
|
|
202
304
|
}
|
|
203
305
|
|
|
204
306
|
function findLaneForPhase(lanes, phase) {
|