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