devloom 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.
Files changed (140) hide show
  1. package/.ai/core.dsl +44 -0
  2. package/.ai/skills.dsl +39 -0
  3. package/.ai/verify.dsl +97 -0
  4. package/.ai/workflow.dsl +81 -0
  5. package/.opencode/themes/devloom-night-owl.json +103 -0
  6. package/GUIDE.md +547 -114
  7. package/README.md +557 -109
  8. package/SECURITY.md +93 -0
  9. package/agents/devloom-developer-flash.md +27 -0
  10. package/agents/devloom-developer-senior.md +28 -0
  11. package/agents/devloom-developer.md +18 -56
  12. package/agents/devloom-documenter-flash.md +24 -0
  13. package/agents/devloom-documenter.md +14 -57
  14. package/agents/devloom-orchestrator.md +232 -356
  15. package/agents/devloom-planner-flash.md +30 -0
  16. package/agents/devloom-planner-senior.md +31 -0
  17. package/agents/devloom-planner.md +32 -0
  18. package/agents/devloom-qa-flash.md +28 -0
  19. package/agents/devloom-qa.md +18 -84
  20. package/agents/devloom-security-senior.md +31 -0
  21. package/agents/devloom-security.md +30 -0
  22. package/agents/devloom-verifier.md +29 -0
  23. package/agents/devloom-vision.md +109 -0
  24. package/agents/devloom-visual-critic.md +98 -0
  25. package/agents/devloom-visual-director.md +80 -0
  26. package/commands/devloom-agents.md +49 -0
  27. package/commands/devloom-auto.md +11 -0
  28. package/commands/devloom-context.md +82 -0
  29. package/commands/devloom-deepseek.md +11 -0
  30. package/commands/devloom-free.md +11 -0
  31. package/commands/devloom-go-economy.md +11 -0
  32. package/commands/devloom-go-flash.md +11 -0
  33. package/commands/devloom-go.md +11 -0
  34. package/commands/devloom-init.md +28 -50
  35. package/commands/devloom-loop-status.md +25 -0
  36. package/commands/devloom-loop.md +36 -0
  37. package/commands/devloom-mimo.md +11 -0
  38. package/commands/devloom-plan.md +13 -0
  39. package/commands/devloom-refresh.md +22 -0
  40. package/commands/devloom-resume.md +30 -49
  41. package/commands/devloom-save.md +69 -0
  42. package/commands/devloom-status.md +9 -18
  43. package/commands/devloom.md +99 -67
  44. package/dist/agents.d.ts +30 -0
  45. package/dist/agents.d.ts.map +1 -0
  46. package/dist/agents.js +139 -0
  47. package/dist/agents.js.map +1 -0
  48. package/dist/bootstrap.d.ts +56 -0
  49. package/dist/bootstrap.d.ts.map +1 -0
  50. package/dist/bootstrap.js +116 -0
  51. package/dist/bootstrap.js.map +1 -0
  52. package/dist/constraints.d.ts +5 -0
  53. package/dist/constraints.d.ts.map +1 -0
  54. package/dist/constraints.js +45 -0
  55. package/dist/constraints.js.map +1 -0
  56. package/dist/context.d.ts +42 -0
  57. package/dist/context.d.ts.map +1 -0
  58. package/dist/context.js +636 -0
  59. package/dist/context.js.map +1 -0
  60. package/dist/guard.d.ts +19 -0
  61. package/dist/guard.d.ts.map +1 -0
  62. package/dist/guard.js +418 -0
  63. package/dist/guard.js.map +1 -0
  64. package/dist/loop.d.ts +64 -0
  65. package/dist/loop.d.ts.map +1 -0
  66. package/dist/loop.js +164 -0
  67. package/dist/loop.js.map +1 -0
  68. package/dist/plugin.d.ts.map +1 -1
  69. package/dist/plugin.js +101 -1
  70. package/dist/plugin.js.map +1 -1
  71. package/dist/tui-agents.d.ts +61 -0
  72. package/dist/tui-agents.d.ts.map +1 -0
  73. package/dist/tui-agents.js +89 -0
  74. package/dist/tui-agents.js.map +1 -0
  75. package/dist/tui.d.ts +6 -0
  76. package/dist/tui.d.ts.map +1 -0
  77. package/dist/tui.js +92 -0
  78. package/dist/tui.js.map +1 -0
  79. package/dist/worktree.d.ts +46 -0
  80. package/dist/worktree.d.ts.map +1 -0
  81. package/dist/worktree.js +272 -0
  82. package/dist/worktree.js.map +1 -0
  83. package/package.json +46 -6
  84. package/patterns/changelog-drafter.md +38 -0
  85. package/patterns/ci-sweeper.md +40 -0
  86. package/patterns/daily-triage.md +37 -0
  87. package/patterns/dependency-sweeper.md +40 -0
  88. package/patterns/design-audit.md +63 -0
  89. package/patterns/issue-triage.md +37 -0
  90. package/patterns/post-merge-cleanup.md +34 -0
  91. package/patterns/pr-babysitter.md +37 -0
  92. package/patterns/registry.yaml +52 -0
  93. package/postinstall.mjs +259 -63
  94. package/project/README.md +29 -0
  95. package/protocol/agent-contracts.md +27 -0
  96. package/protocol/artifact-system.md +53 -0
  97. package/protocol/model-routing.md +185 -0
  98. package/protocol/orchestrator-core.md +43 -0
  99. package/protocol/project-system.md +45 -0
  100. package/protocol/rules.md +18 -0
  101. package/protocol/verification-policy.md +48 -0
  102. package/scripts/loop-run.mjs +205 -0
  103. package/scripts/model-capabilities.mjs +169 -0
  104. package/scripts/plugin-cache.mjs +269 -0
  105. package/scripts/profile.mjs +691 -0
  106. package/scripts/visual-benchmark.mjs +149 -0
  107. package/scripts/worktree.mjs +445 -0
  108. package/skills/build/development.md +33 -0
  109. package/skills/build/live-docs.md +38 -0
  110. package/skills/build/simplify.md +50 -0
  111. package/skills/build/vision-analysis.md +98 -0
  112. package/skills/design/app-design.md +31 -0
  113. package/skills/design/design-grounding.md +30 -0
  114. package/skills/design/design-system.md +27 -0
  115. package/skills/design/game-design.md +35 -0
  116. package/skills/design/motion-design.md +34 -0
  117. package/skills/design/visual-direction.md +32 -0
  118. package/skills/design/web-design.md +33 -0
  119. package/skills/loop/changelog-drafter.md +32 -0
  120. package/skills/loop/ci-sweeper.md +24 -0
  121. package/skills/loop/constraints.md +21 -0
  122. package/skills/loop/dependency-sweeper.md +24 -0
  123. package/skills/loop/design-audit.md +67 -0
  124. package/skills/loop/issue-triage.md +27 -0
  125. package/skills/loop/post-merge-cleanup.md +24 -0
  126. package/skills/loop/pr-babysitter.md +26 -0
  127. package/skills/loop/triage.md +25 -0
  128. package/skills/loop/verifier.md +20 -0
  129. package/skills/meta/skill-discovery.md +23 -0
  130. package/skills/plan/planning.md +27 -0
  131. package/skills/plan/verification-planning.md +40 -0
  132. package/skills/review/security-review.md +63 -0
  133. package/skills/ship/documentation.md +11 -0
  134. package/skills/verify/app-verification.md +19 -0
  135. package/skills/verify/quality-assurance.md +17 -0
  136. package/skills/verify/visual-critique.md +30 -0
  137. package/skills/verify/visual-quality-gate.md +29 -0
  138. package/skills/verify/visual-regression.md +25 -0
  139. package/agents/devloom-analyst.md +0 -83
  140. package/agents/devloom-architect.md +0 -88
@@ -0,0 +1,691 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, cpSync } from "fs"
3
+ import { homedir } from "os"
4
+ import { resolve, dirname } from "path"
5
+ import { fileURLToPath } from "url"
6
+ import { execSync } from "child_process"
7
+ import {
8
+ pluginCacheStatus,
9
+ refreshOpenCodePluginAgentFiles,
10
+ } from "./plugin-cache.mjs"
11
+ import {
12
+ isValidModelId,
13
+ findAvailableCandidate,
14
+ resolveModelCandidate,
15
+ capabilitiesFor,
16
+ modelMeetsRole,
17
+ roleRequiresVision,
18
+ MIMO_V26_FLASH_CANDIDATES,
19
+ MODEL_FAMILIES,
20
+ VISION_FALLBACK_CANDIDATES,
21
+ } from "./model-capabilities.mjs"
22
+
23
+ // Provider-agnostic model helpers re-exported so consumers (commands, tests)
24
+ // can import them from the profile manager as the single entry point.
25
+ export {
26
+ isValidModelId,
27
+ findAvailableCandidate,
28
+ resolveModelCandidate,
29
+ capabilitiesFor,
30
+ modelMeetsRole,
31
+ roleRequiresVision,
32
+ MIMO_V26_FLASH_CANDIDATES,
33
+ MODEL_FAMILIES,
34
+ VISION_FALLBACK_CANDIDATES,
35
+ }
36
+
37
+ const CONFIG_PATH = ".opencode/devloom/config.json"
38
+ const AGENTS_DIR = homedir() + "/.config/opencode/agents"
39
+ const SOURCE_AGENTS_DIR = resolve(dirname(new URL(import.meta.url).pathname), "..", "agents")
40
+
41
+ export const TIERS = {
42
+ senior: {
43
+ orchestrator: "opencode-go/deepseek-v4-flash",
44
+ planner: "opencode-go/glm-5.2",
45
+ developer: "opencode-go/kimi-k3",
46
+ qa: "opencode-go/deepseek-v4-pro",
47
+ verifier: "opencode-go/deepseek-v4-pro",
48
+ security: "opencode-go/glm-5.2",
49
+ documenter: "opencode-go/qwen3.7-plus",
50
+ vision: "opencode-go/glm-5.3-flash",
51
+ "visual-director": "opencode-go/glm-5.2",
52
+ "visual-critic": "opencode-go/glm-5.3-flash"
53
+ },
54
+ standard: {
55
+ orchestrator: "opencode-go/deepseek-v4-flash",
56
+ planner: "opencode-go/qwen3.7-max",
57
+ developer: "opencode-go/glm-5.3-flash",
58
+ qa: "opencode-go/deepseek-v4-flash",
59
+ verifier: "opencode-go/deepseek-v4-flash",
60
+ security: "opencode-go/deepseek-v4-flash",
61
+ documenter: "opencode-go/deepseek-v4-flash",
62
+ vision: "opencode-go/glm-5.3-flash",
63
+ "visual-director": "opencode-go/glm-5.3-flash",
64
+ "visual-critic": "opencode-go/glm-5.3-flash"
65
+ }
66
+ }
67
+
68
+ export const PROFILES = {
69
+ go: {
70
+ orchestrator: "opencode-go/deepseek-v4-flash",
71
+ planner: "opencode-go/qwen3.7-max",
72
+ developer: "opencode-go/glm-5.3-flash",
73
+ qa: "opencode-go/deepseek-v4-pro",
74
+ verifier: "opencode-go/deepseek-v4-pro",
75
+ security: "opencode-go/glm-5.2",
76
+ documenter: "opencode-go/qwen3.7-plus",
77
+ vision: "opencode-go/glm-5.3-flash",
78
+ "visual-director": "opencode-go/glm-5.2",
79
+ "visual-critic": "opencode-go/glm-5.3-flash"
80
+ },
81
+ "go-economy": {
82
+ orchestrator: "opencode-go/deepseek-v4-flash",
83
+ planner: "opencode-go/deepseek-v4-pro",
84
+ developer: "opencode-go/deepseek-v4-pro",
85
+ qa: "opencode-go/deepseek-v4-flash",
86
+ verifier: "opencode-go/deepseek-v4-flash",
87
+ security: "opencode-go/deepseek-v4-pro",
88
+ documenter: "opencode-go/qwen3.7-plus",
89
+ vision: "opencode-go/glm-5.3-flash",
90
+ "visual-director": "opencode-go/deepseek-v4-pro",
91
+ "visual-critic": "opencode-go/glm-5.3-flash"
92
+ },
93
+ deepseek: {
94
+ orchestrator: "opencode-go/deepseek-v4-pro",
95
+ planner: "opencode-go/deepseek-v4-pro",
96
+ developer: "opencode-go/deepseek-v4-pro",
97
+ qa: "opencode-go/deepseek-v4-pro",
98
+ verifier: "opencode-go/deepseek-v4-flash",
99
+ security: "opencode-go/deepseek-v4-pro",
100
+ documenter: "opencode-go/deepseek-v4-flash",
101
+ vision: "opencode-go/deepseek-v4-flash-vision-exp",
102
+ "visual-director": "opencode-go/deepseek-v4-pro",
103
+ "visual-critic": "opencode-go/deepseek-v4-flash-vision-exp"
104
+ },
105
+ glm: {
106
+ orchestrator: "opencode-go/glm-5.3-flash",
107
+ planner: "opencode-go/glm-5.3-flash",
108
+ developer: "opencode-go/glm-5.3-flash",
109
+ qa: "opencode-go/glm-5.3-flash",
110
+ verifier: "opencode-go/glm-5.3-flash",
111
+ security: "opencode-go/glm-5.3-flash",
112
+ documenter: "opencode-go/glm-5.3-flash",
113
+ vision: "opencode-go/glm-5.3-flash",
114
+ "visual-director": "opencode-go/glm-5.3-flash",
115
+ "visual-critic": "opencode-go/glm-5.3-flash"
116
+ },
117
+ "go-flash": {
118
+ orchestrator: "opencode-go/deepseek-v4-flash",
119
+ planner: "opencode-go/deepseek-v4-flash",
120
+ developer: "opencode-go/deepseek-v4-flash",
121
+ qa: "opencode-go/deepseek-v4-flash",
122
+ verifier: "opencode-go/deepseek-v4-flash",
123
+ security: "opencode-go/deepseek-v4-flash",
124
+ documenter: "opencode-go/deepseek-v4-flash",
125
+ vision: "opencode-go/glm-5.3-flash",
126
+ "visual-director": "opencode-go/deepseek-v4-flash",
127
+ "visual-critic": "opencode-go/glm-5.3-flash"
128
+ }
129
+ }
130
+
131
+ export const FREE_CANDIDATES_BY_ROLE = {
132
+ orchestration: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/mimo-v2.5-free", "opencode/muse-spark-1.2-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
133
+ planning: ["opencode/nemotron-3-ultra-free", "opencode/muse-spark-1.3-contributor-free", "opencode/muse-spark-1.2-contributor-free", "opencode/big-pickle", "opencode/nemotron-3.5-lightning-free", "opencode/ling-3.0-flash-fin-free"],
134
+ implementation: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
135
+ verification: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
136
+ documentation: ["opencode/muse-spark-1.3-contributor-free", "opencode/muse-spark-1.2-contributor-free", "opencode/nemotron-3-ultra-free", "opencode/mimo-v2.5-free", "opencode/ling-3.0-flash-fin-free"],
137
+ vision: ["opencode/mimo-v2.5-free", "opencode-go/deepseek-v4-flash-vision-exp", "opencode-go/minimax-m3", "opencode-go/mimo-v2.5-pro"]
138
+ }
139
+
140
+ export const FREE_ROLE_MAP = {
141
+ orchestrator: "orchestration",
142
+ planner: "planning",
143
+ developer: "implementation",
144
+ qa: "verification",
145
+ verifier: "verification",
146
+ security: "verification",
147
+ documenter: "documentation",
148
+ vision: "vision",
149
+ "visual-director": "vision",
150
+ "visual-critic": "vision"
151
+ }
152
+
153
+ export const ALL_AGENTS = Object.keys(FREE_ROLE_MAP)
154
+
155
+ // Profiles that pin every variant model to their profile definition —
156
+ // tier model overrides would silently break that promise, so they are locked.
157
+ export const LOCKED_TIER_PROFILES = new Set(["go-flash", "free", "mimo"])
158
+
159
+ function readJson(path, fallback = null) {
160
+ try { return JSON.parse(readFileSync(path, "utf8")) } catch { return fallback }
161
+ }
162
+
163
+ function writeJson(path, data) {
164
+ const dir = dirname(path)
165
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
166
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n")
167
+ }
168
+
169
+ function run(cmd) {
170
+ try {
171
+ return execSync(cmd, { encoding: "utf8", timeout: 30000 }).trim()
172
+ } catch {
173
+ return ""
174
+ }
175
+ }
176
+
177
+ export function detectAvailableModels() {
178
+ const output = run("opencode models --refresh 2>/dev/null && opencode models 2>/dev/null")
179
+ if (!output) return []
180
+ const lines = output.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("lmstudio/"))
181
+ return [...new Set(lines)]
182
+ }
183
+
184
+ export function hasGoModels(available) {
185
+ return available.some(m => m.startsWith("opencode-go/"))
186
+ }
187
+
188
+ export function getFreeModels(available) {
189
+ return available.filter(m => m.startsWith("opencode/") && m.endsWith("-free") || m === "opencode/big-pickle")
190
+ .concat(available.filter(m => m.startsWith("opencode-go/free-")))
191
+ }
192
+
193
+ export function pickBestFree(available, role) {
194
+ const candidates = FREE_CANDIDATES_BY_ROLE[role] || FREE_CANDIDATES_BY_ROLE.verification
195
+ for (const c of candidates) {
196
+ if (available.includes(c)) return c
197
+ }
198
+ for (const c of candidates) {
199
+ for (const a of available) {
200
+ if (a.startsWith("opencode/") && (a.includes(c.split("/").pop()?.split("-free")[0] || c) || c.includes(a.split("/").pop()?.split("-free")[0] || a))) {
201
+ return a
202
+ }
203
+ }
204
+ }
205
+ const freeModels = getFreeModels(available)
206
+ return freeModels.length > 0 ? freeModels[0] : "opencode/big-pickle"
207
+ }
208
+
209
+ export function resolveFreeProfile(available) {
210
+ const models = {}
211
+ for (const agent of ALL_AGENTS) {
212
+ const role = FREE_ROLE_MAP[agent]
213
+ models[agent] = pickBestFree(available, role)
214
+ }
215
+ return models
216
+ }
217
+
218
+ // Syntax validation is provider-agnostic: any `provider/model` id is valid.
219
+ // Availability stays a separate check against `opencode models` — a model that
220
+ // is well-formed but absent must still fail validation.
221
+ export function validateModels(models, available) {
222
+ const unavailable = []
223
+ for (const [agent, model] of Object.entries(models)) {
224
+ if (!isValidModelId(model)) {
225
+ unavailable.push({ agent, model, reason: "invalid format" })
226
+ continue
227
+ }
228
+ const matches = available.filter(a => a === model)
229
+ if (matches.length === 0) {
230
+ unavailable.push({ agent, model, reason: "not found in available models" })
231
+ }
232
+ }
233
+ return unavailable
234
+ }
235
+
236
+ // Capability routing: roles that require image input (vision, visual-critic)
237
+ // are moved to a vision-capable fallback when the assigned model has no
238
+ // evidence of vision support. Never swaps when no alternative is available —
239
+ // the caller reports the limitation instead of pretending.
240
+ export function enforceVisionRoles(models, available, fallbacks = {}) {
241
+ const result = { ...models }
242
+ const applied = { ...fallbacks }
243
+ for (const agent of Object.keys(result)) {
244
+ if (!roleRequiresVision(agent)) continue
245
+ if (modelMeetsRole(result[agent], agent).ok) continue
246
+ const replacement = findAvailableCandidate(VISION_FALLBACK_CANDIDATES, available)
247
+ if (!replacement) continue
248
+ applied[agent] = { from: result[agent], to: replacement, reason: "vision capability" }
249
+ result[agent] = replacement
250
+ }
251
+ return { result, fallbacks: applied }
252
+ }
253
+
254
+ function containsKey(obj, key) {
255
+ return Object.prototype.hasOwnProperty.call(obj, key)
256
+ }
257
+
258
+ export function applyFallbacks(models, unavailable, available) {
259
+ const result = { ...models }
260
+ const fallbacks = {}
261
+ for (const { agent, model } of unavailable) {
262
+ if (containsKey(FREE_ROLE_MAP, agent)) {
263
+ const role = FREE_ROLE_MAP[agent]
264
+ const fb = pickBestFree(available, role)
265
+ result[agent] = fb
266
+ fallbacks[agent] = { from: model, to: fb }
267
+ } else {
268
+ const fb = pickBestFree(available, "verification")
269
+ result[agent] = fb
270
+ fallbacks[agent] = { from: model, to: fb }
271
+ }
272
+ }
273
+ return { result, fallbacks }
274
+ }
275
+
276
+ function readConfig() {
277
+ return readJson(CONFIG_PATH, {})
278
+ }
279
+
280
+ function writeConfig(config) {
281
+ writeJson(CONFIG_PATH, config)
282
+ }
283
+
284
+ function ensureAgentFiles() {
285
+ if (!existsSync(AGENTS_DIR)) {
286
+ mkdirSync(AGENTS_DIR, { recursive: true })
287
+ }
288
+ const srcFiles = readdirSync(SOURCE_AGENTS_DIR).filter(f => f.startsWith("devloom-") && f.endsWith(".md"))
289
+ const existing = new Set(readdirSync(AGENTS_DIR).filter(f => f.startsWith("devloom-") && f.endsWith(".md")))
290
+ for (const f of srcFiles) {
291
+ if (!existing.has(f)) {
292
+ try { cpSync(resolve(SOURCE_AGENTS_DIR, f), resolve(AGENTS_DIR, f)) } catch {}
293
+ }
294
+ }
295
+ }
296
+
297
+ export const AGENT_VARIANTS = {
298
+ developer: ["developer", "developer-flash", "developer-senior"],
299
+ planner: ["planner", "planner-flash", "planner-senior"],
300
+ qa: ["qa", "qa-flash"],
301
+ verifier: ["verifier"],
302
+ security: ["security", "security-senior"],
303
+ documenter: ["documenter", "documenter-flash"],
304
+ orchestrator: ["orchestrator"],
305
+ vision: ["vision"],
306
+ "visual-director": ["visual-director"],
307
+ "visual-critic": ["visual-critic"]
308
+ }
309
+
310
+ export function applyModelsToAgentFiles(models, profileLabel = "") {
311
+ ensureAgentFiles()
312
+ for (const [agent, model] of Object.entries(models)) {
313
+ const variants = AGENT_VARIANTS[agent] || [agent]
314
+ for (const v of variants) {
315
+ const f = resolve(AGENTS_DIR, `devloom-${v}.md`)
316
+ if (!existsSync(f)) continue
317
+ try {
318
+ let content = readFileSync(f, "utf8")
319
+ content = content.replace(/^model: .*/m, `model: ${model}`)
320
+ if (v === "orchestrator" && profileLabel) {
321
+ content = content.replace(/^(description: "DevLoom Orchestrator:)([^"]*)"/m, (_, prefix, rest) => {
322
+ const cleaned = rest.replace(/\s*\(profile: [^)]*\)/, "")
323
+ return `${prefix}${cleaned} (profile: ${profileLabel})"`
324
+ })
325
+ }
326
+ writeFileSync(f, content)
327
+ } catch {}
328
+ }
329
+ }
330
+ }
331
+
332
+ export function cmdApplyTier(tierName, available) {
333
+ const tier = TIERS[tierName]
334
+ if (!tier) {
335
+ console.error("Unknown tier:", tierName)
336
+ console.error("Valid tiers: senior, standard")
337
+ process.exit(1)
338
+ }
339
+
340
+ const config = readConfig()
341
+ if (LOCKED_TIER_PROFILES.has(config.resolvedProfile)) {
342
+ console.error(`Cannot apply tier "${tierName}" to profile "${config.resolvedProfile}".`)
343
+ console.error(`Profile "${config.resolvedProfile}" locks all models to its profile definition.`)
344
+ console.error("Switch to 'go' or 'go-economy' profile to use tier overrides.")
345
+ process.exit(1)
346
+ }
347
+
348
+ const models = { ...tier }
349
+ const unresolved = validateModels(models, available)
350
+ if (unresolved.length > 0) {
351
+ console.error(`Tier "${tierName}" has unavailable models:`)
352
+ for (const u of unresolved) console.error(` ${u.agent}: ${u.model}`)
353
+ process.exit(1)
354
+ }
355
+
356
+ applyModelsToAgentFiles(models)
357
+
358
+ config.tier = tierName
359
+ config.tierModels = models
360
+ config.models = { ...models }
361
+ writeConfig(config)
362
+
363
+ return models
364
+ }
365
+
366
+ export function cmdSet(profileName) {
367
+ const available = detectAvailableModels()
368
+ const hasGo = hasGoModels(available)
369
+
370
+ if ((profileName === "go" || profileName === "go-economy" || profileName === "deepseek" || profileName === "glm" || profileName === "go-flash") && !hasGo) {
371
+ console.error("No OpenCode Go models detected. Run: opencode /connect to add OpenCode Go.")
372
+ console.error("Fall back to: /devloom-auto or /devloom-free")
373
+ process.exit(1)
374
+ }
375
+
376
+ let resolvedProfile = profileName
377
+ let models = {}
378
+ let fallbacks = {}
379
+ let unavailable = []
380
+
381
+ if (profileName === "go") {
382
+ models = { ...PROFILES.go }
383
+ unavailable = validateModels(models, available)
384
+ if (unavailable.length > 0) {
385
+ const fb = applyFallbacks(models, unavailable, available)
386
+ models = fb.result
387
+ fallbacks = fb.fallbacks
388
+ }
389
+ } else if (profileName === "go-economy") {
390
+ models = { ...PROFILES["go-economy"] }
391
+ unavailable = validateModels(models, available)
392
+ if (unavailable.length > 0) {
393
+ const fb = applyFallbacks(models, unavailable, available)
394
+ models = fb.result
395
+ fallbacks = fb.fallbacks
396
+ }
397
+ } else if (profileName === "free") {
398
+ models = resolveFreeProfile(available)
399
+ unavailable = validateModels(models, available)
400
+ } else if (profileName === "deepseek") {
401
+ models = { ...PROFILES.deepseek }
402
+ unavailable = validateModels(models, available)
403
+ if (unavailable.length > 0) {
404
+ const fb = applyFallbacks(models, unavailable, available)
405
+ models = fb.result
406
+ fallbacks = fb.fallbacks
407
+ }
408
+ } else if (profileName === "glm") {
409
+ models = { ...PROFILES.glm }
410
+ unavailable = validateModels(models, available)
411
+ if (unavailable.length > 0) {
412
+ const fb = applyFallbacks(models, unavailable, available)
413
+ models = fb.result
414
+ fallbacks = fb.fallbacks
415
+ }
416
+ } else if (profileName === "go-flash") {
417
+ models = { ...PROFILES["go-flash"] }
418
+ unavailable = validateModels(models, available)
419
+ if (unavailable.length > 0) {
420
+ const fb = applyFallbacks(models, unavailable, available)
421
+ models = fb.result
422
+ fallbacks = fb.fallbacks
423
+ }
424
+ } else if (profileName === "mimo") {
425
+ // First-class MiMo profile: resolve the V2.6 Flash candidate chain against
426
+ // the live catalog instead of hardcoding a provider prefix.
427
+ const resolved = resolveModelCandidate(MIMO_V26_FLASH_CANDIDATES, available, {
428
+ familyPattern: MODEL_FAMILIES.mimoV26Flash.familyPattern,
429
+ })
430
+ if (!resolved.model) {
431
+ console.error("MiMo V2.6 Flash is not available in this environment.")
432
+ console.error("")
433
+ console.error("Attempted candidates:")
434
+ for (const candidate of resolved.attempted) {
435
+ console.error(` ${candidate} (not found)`)
436
+ }
437
+ const compatible = available.filter((m) => MODEL_FAMILIES.mimoV26Flash.familyPattern.test(m))
438
+ console.error("")
439
+ if (compatible.length > 0) {
440
+ console.error("Available compatible MiMo V2.6 models:")
441
+ for (const m of compatible) console.error(` ${m}`)
442
+ } else {
443
+ const anyMimo = available.filter((m) => /mimo/i.test(m))
444
+ if (anyMimo.length > 0) {
445
+ console.error("Available MiMo models (different version — not used automatically):")
446
+ for (const m of anyMimo) console.error(` ${m}`)
447
+ } else {
448
+ console.error("No MiMo models are available. Run: opencode models")
449
+ }
450
+ }
451
+ console.error("")
452
+ console.error("The mimo profile was NOT activated. No unrelated models were substituted.")
453
+ console.error("Valid profiles: auto, go, go-economy, deepseek, glm, go-flash, mimo, free")
454
+ process.exit(1)
455
+ }
456
+ models = Object.fromEntries(ALL_AGENTS.map((agent) => [agent, resolved.model]))
457
+ if (!resolved.exact) {
458
+ for (const agent of ALL_AGENTS) {
459
+ fallbacks[agent] = {
460
+ from: "mimo-v2.6-flash (candidate chain)",
461
+ to: resolved.model,
462
+ reason: "family fallback",
463
+ }
464
+ }
465
+ }
466
+ } else if (profileName === "auto") {
467
+ if (hasGo) {
468
+ models = { ...PROFILES["go-flash"] }
469
+ resolvedProfile = "go-flash"
470
+ } else {
471
+ models = resolveFreeProfile(available)
472
+ resolvedProfile = "free"
473
+ }
474
+ unavailable = validateModels(models, available)
475
+ if (unavailable.length > 0) {
476
+ const fb = applyFallbacks(models, unavailable, available)
477
+ models = fb.result
478
+ fallbacks = fb.fallbacks
479
+ }
480
+ } else {
481
+ console.error("Unknown profile:", profileName)
482
+ console.error("Valid profiles: auto, go, go-economy, deepseek, glm, go-flash, mimo, free")
483
+ process.exit(1)
484
+ }
485
+
486
+ // Capability routing: keep vision-required roles on vision-capable models.
487
+ const enforced = enforceVisionRoles(models, available, fallbacks)
488
+ models = enforced.result
489
+ fallbacks = enforced.fallbacks
490
+
491
+ const prev = readConfig()
492
+ const config = {
493
+ profile: profileName,
494
+ resolvedProfile,
495
+ models,
496
+ overrides: prev.overrides || {},
497
+ tier: LOCKED_TIER_PROFILES.has(resolvedProfile) ? null : (prev.tier || null),
498
+ tierModels: LOCKED_TIER_PROFILES.has(resolvedProfile) ? null : (prev.tierModels || null),
499
+ availableModelsSnapshot: available,
500
+ fallbacks
501
+ }
502
+
503
+ writeConfig(config)
504
+ applyModelsToAgentFiles(models, config.resolvedProfile ?? config.profile)
505
+
506
+ const cacheStatus = pluginCacheStatus()
507
+ if (cacheStatus === "ok") {
508
+ refreshOpenCodePluginAgentFiles(AGENTS_DIR)
509
+ console.log("OpenCode plugin cache refreshed with the current profile.")
510
+ } else {
511
+ console.log(`Note: DevLoom plugin cache is ${cacheStatus === "missing" ? "missing" : "stale"} — run /devloom-refresh once, then restart opencode to see the profile in the sidebar.`)
512
+ }
513
+ console.log("Restart opencode (or run: opencode --continue) to see the updated profile and agents in the sidebar.")
514
+
515
+ return config
516
+ }
517
+
518
+ export function cmdCurrent() {
519
+ const config = readConfig()
520
+ if (!config || !config.profile) {
521
+ console.log("No DevLoom profile configured yet.")
522
+ console.log("Run: /devloom-init or /devloom-auto")
523
+ return
524
+ }
525
+ console.log(`Profile: ${config.profile}`)
526
+ if (config.tier) {
527
+ console.log(`Tier: ${config.tier}`)
528
+ } else if (LOCKED_TIER_PROFILES.has(config.resolvedProfile)) {
529
+ console.log(`Tier: locked (profile "${config.resolvedProfile}" does not support tier overrides)`)
530
+ }
531
+ console.log("")
532
+ const activeModels = config.tierModels || config.models
533
+ console.log("Resolved models:")
534
+ for (const [agent, model] of Object.entries(activeModels)) {
535
+ const tiered = config.tier && config.models[agent] !== model ? ` (tier: ${model})` : ""
536
+ const overridden = config.overrides && config.overrides[agent] ? " (override)" : ""
537
+ const fallbacked = config.fallbacks && config.fallbacks[agent] ? " (fallback)" : ""
538
+ const base = config.tier ? config.models[agent] : null
539
+ const label = base && base !== model ? `${base} → ${model}` : model
540
+ console.log(` ${agent}: ${label}${tiered}${overridden}${fallbacked}`)
541
+ }
542
+ if (config.unresolved && config.unresolved.length > 0) {
543
+ console.log("")
544
+ console.log("Unavailable models:")
545
+ for (const u of config.unresolved) {
546
+ console.log(` ${u.agent}: ${u.model} (${u.reason})`)
547
+ }
548
+ }
549
+ if (config.fallbacks && Object.keys(config.fallbacks).length > 0) {
550
+ console.log("")
551
+ console.log("Fallbacks applied:")
552
+ for (const [agent, fb] of Object.entries(config.fallbacks)) {
553
+ console.log(` ${agent}: ${fb.from} -> ${fb.to}`)
554
+ }
555
+ }
556
+ }
557
+
558
+ export function cmdValidate() {
559
+ const available = detectAvailableModels()
560
+ const config = readConfig()
561
+ if (!config || !config.models) {
562
+ console.log("No DevLoom configuration to validate.")
563
+ process.exit(1)
564
+ }
565
+ const allUnavailable = []
566
+ for (const [agent, model] of Object.entries(config.models)) {
567
+ if (!available.includes(model)) {
568
+ allUnavailable.push({ agent, model, reason: "not available" })
569
+ }
570
+ }
571
+ if (allUnavailable.length > 0) {
572
+ console.log("Unavailable models:")
573
+ for (const u of allUnavailable) {
574
+ console.log(` ${u.agent}: ${u.model}`)
575
+ }
576
+ process.exit(1)
577
+ } else {
578
+ console.log("All models are available.")
579
+ }
580
+ }
581
+
582
+ export function cmdApply() {
583
+ const config = readConfig()
584
+ if (!config || !config.profile) {
585
+ console.error("No profile configured. Run /devloom-init or /devloom-auto first.")
586
+ process.exit(1)
587
+ }
588
+ const available = detectAvailableModels()
589
+ const models = config.models || {}
590
+ applyModelsToAgentFiles(models, config.resolvedProfile ?? config.profile)
591
+
592
+ config.availableModelsSnapshot = available
593
+ config.requiresReload = true
594
+ writeConfig(config)
595
+
596
+ console.log("Profile applied to agent files.")
597
+ console.log("")
598
+
599
+ const cacheStatus = pluginCacheStatus()
600
+ if (cacheStatus === "ok") {
601
+ refreshOpenCodePluginAgentFiles(AGENTS_DIR)
602
+ console.log("OpenCode plugin cache refreshed with the current profile.")
603
+ } else {
604
+ console.log(`NOTE: the DevLoom plugin cache is ${cacheStatus === "missing" ? "missing (plugin not installed in OpenCode)" : "stale"} —`)
605
+ console.log("the profile will not show in the sidebar until the plugin code is refreshed.")
606
+ console.log("Run: /devloom-refresh (or: npm install -g devloom && opencode plugin devloom --global --force)")
607
+ }
608
+ console.log("")
609
+ console.log("Restart opencode (or run: opencode --continue) to see the updated profile and agents in the sidebar.")
610
+ console.log("")
611
+
612
+ if (existsSync(AGENTS_DIR)) {
613
+ const reopenScript = ".opencode/devloom/reopen.sh"
614
+ writeFileSync(reopenScript, "#!/usr/bin/env bash\nexec opencode --continue\n")
615
+ try { execSync(`chmod +x ${reopenScript}`) } catch {}
616
+
617
+ console.log("If the current OpenCode session does not pick up the new agent models immediately,")
618
+ console.log("continue the same session with:")
619
+ console.log("")
620
+ console.log(" opencode --continue")
621
+ console.log("")
622
+ console.log("Or run:")
623
+ console.log(` bash ${reopenScript}`)
624
+ }
625
+ }
626
+
627
+ export function cmdDetect() {
628
+ const available = detectAvailableModels()
629
+ const hasGo = hasGoModels(available)
630
+
631
+ console.log("Available models:", available.length)
632
+ if (hasGo) {
633
+ console.log("OpenCode Go models detected: yes")
634
+ console.log("Recommended profile: go")
635
+ } else {
636
+ console.log("OpenCode Go models detected: no")
637
+ console.log("Recommended profile: free")
638
+ }
639
+ const freeCount = getFreeModels(available).length
640
+ console.log(`Free models available: ${freeCount}`)
641
+ const mimo = findAvailableCandidate(MIMO_V26_FLASH_CANDIDATES, available)
642
+ console.log(mimo ? `MiMo V2.6 Flash: available (${mimo})` : "MiMo V2.6 Flash: not available")
643
+ }
644
+
645
+ function main() {
646
+ const args = process.argv.slice(2)
647
+ const command = args[0] || ""
648
+
649
+ switch (command) {
650
+ case "set":
651
+ if (!args[1]) { console.error("Usage: devloom profile set <auto|go|go-economy|deepseek|glm|go-flash|mimo|free>"); process.exit(1) }
652
+ cmdSet(args[1])
653
+ console.log(`DevLoom profile changed to: ${args[1]}`)
654
+ if (args[1] !== "free") {
655
+ console.log(`Resolved profile: ${args[1]}`)
656
+ }
657
+ break
658
+ case "current":
659
+ cmdCurrent()
660
+ break
661
+ case "validate":
662
+ cmdValidate()
663
+ break
664
+ case "apply":
665
+ cmdApply()
666
+ break
667
+ case "detect":
668
+ cmdDetect()
669
+ break
670
+ case "tier":
671
+ if (!args[1]) { console.error("Usage: devloom profile tier <senior|balanced|junior>"); process.exit(1) }
672
+ if (!["senior","standard"].includes(args[1])) { console.error("Invalid tier. Valid: senior, standard"); process.exit(1) }
673
+ const available = detectAvailableModels()
674
+ cmdApplyTier(args[1], available)
675
+ console.log(`DevLoom tier changed to: ${args[1]}`)
676
+ break
677
+ default:
678
+ console.log("DevLoom Profile Manager")
679
+ console.log("")
680
+ console.log("Commands:")
681
+ console.log(" devloom profile set <auto|go|go-economy|deepseek|glm|go-flash|mimo|free>")
682
+ console.log(" devloom profile tier <senior|standard>")
683
+ console.log(" devloom profile current")
684
+ console.log(" devloom profile validate")
685
+ console.log(" devloom profile apply")
686
+ console.log(" devloom profile detect")
687
+ }
688
+ }
689
+
690
+ const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false
691
+ if (isMain) main()