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,169 @@
1
+ // Provider-agnostic model identity, candidate resolution, and capability hints
2
+ // for DevLoom profiles. Pure module: no fs, no child_process, no secrets.
3
+ //
4
+ // Design rules:
5
+ // - Model IDs are conceptually `provider/model` (the model segment may itself
6
+ // contain slashes, e.g. `lmstudio/google/gemma-4-26b`). Any provider is valid;
7
+ // availability always comes from `opencode models` at the call site.
8
+ // - MODEL_FAMILIES is the authoritative capability source for known families.
9
+ // `capabilitiesFor()` falls back to conservative name heuristics for unknown
10
+ // models — hints only, never a substitute for availability validation.
11
+ // - Nothing here is MiMo-specific; MiMo is simply the first family registered.
12
+
13
+ /** Conceptual model families with candidate chains + capability declarations. */
14
+ export const MODEL_FAMILIES = {
15
+ mimoV26Flash: {
16
+ id: "mimoV26Flash",
17
+ label: "MiMo V2.6 Flash",
18
+ // Provider prefix is NOT assumed — resolution picks the first available.
19
+ candidates: [
20
+ "mimo/mimo-v2.6-flash",
21
+ "xiaomi/mimo-v2.6-flash",
22
+ "opencode-go/mimo-v2.6-flash",
23
+ ],
24
+ familyPattern: /(^|\/)mimo-v2\.6/i,
25
+ capabilities: {
26
+ vision: true,
27
+ multimodal: true,
28
+ coding: "high",
29
+ reasoning: "high",
30
+ toolUse: true,
31
+ longContext: true,
32
+ speed: "fast",
33
+ costClass: "low",
34
+ orchestration: true,
35
+ verification: "high",
36
+ },
37
+ },
38
+ }
39
+
40
+ /** MiMo V2.6 Flash candidate chain (re-exported for profile manager + tests). */
41
+ export const MIMO_V26_FLASH_CANDIDATES = MODEL_FAMILIES.mimoV26Flash.candidates
42
+
43
+ /**
44
+ * Vision-capable fallbacks used when a vision-required role (vision,
45
+ * visual-critic) resolves to a text-only model. Ordered by existing DevLoom
46
+ * convention: Go multimodal models first, free-tier vision model last.
47
+ */
48
+ export const VISION_FALLBACK_CANDIDATES = [
49
+ "opencode-go/qwen3.6-plus",
50
+ "opencode-go/glm-5.3-flash",
51
+ "opencode-go/deepseek-v4-flash-vision-exp",
52
+ "opencode-go/minimax-m3",
53
+ "opencode/mimo-v2.5-free",
54
+ ]
55
+
56
+ /** Roles whose assigned model must accept image input. */
57
+ const VISION_REQUIRED_ROLES = new Set(["vision", "visual-critic"])
58
+
59
+ const PROVIDER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
60
+ const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._+/-]*$/
61
+
62
+ /**
63
+ * True for any well-formed OpenCode model identifier of the form
64
+ * `provider/model` (the model segment may contain further slashes).
65
+ * Availability is a separate concern — see findAvailableCandidate().
66
+ */
67
+ export function isValidModelId(id) {
68
+ if (typeof id !== "string") return false
69
+ if (!id || id.length > 256 || /\s/.test(id)) return false
70
+ const slash = id.indexOf("/")
71
+ if (slash <= 0 || slash === id.length - 1) return false
72
+ const provider = id.slice(0, slash)
73
+ const model = id.slice(slash + 1)
74
+ if (!PROVIDER_RE.test(provider)) return false
75
+ if (!MODEL_RE.test(model)) return false
76
+ if (model.startsWith("/") || model.endsWith("/")) return false
77
+ return true
78
+ }
79
+
80
+ /** First candidate present in the available list, or null. */
81
+ export function findAvailableCandidate(candidates, available) {
82
+ if (!Array.isArray(candidates) || !Array.isArray(available)) return null
83
+ for (const candidate of candidates) {
84
+ if (available.includes(candidate)) return candidate
85
+ }
86
+ return null
87
+ }
88
+
89
+ /**
90
+ * Resolve a candidate chain against the available model list.
91
+ *
92
+ * Returns { model, attempted, exact, fallback }:
93
+ * - exact hit on any declared candidate → exact: true
94
+ * - optional opts.familyPattern permits a same-family fallback (e.g.
95
+ * mimo-v2.6-flash → mimo-v2.6-pro) reported with exact: false so callers
96
+ * can surface "requested vs resolved" transparently
97
+ * - otherwise model: null — callers must fail loudly, never pretend
98
+ */
99
+ export function resolveModelCandidate(candidates, available, opts = {}) {
100
+ const attempted = Array.isArray(candidates) ? [...candidates] : []
101
+ const list = Array.isArray(available) ? available : []
102
+ const exact = findAvailableCandidate(attempted, list)
103
+ if (exact) return { model: exact, attempted, exact: true, fallback: null }
104
+ if (opts.familyPattern instanceof RegExp) {
105
+ const family = list.find((m) => isValidModelId(m) && opts.familyPattern.test(m))
106
+ if (family) return { model: family, attempted, exact: false, fallback: family }
107
+ }
108
+ return { model: null, attempted, exact: false, fallback: null }
109
+ }
110
+
111
+ function familyCapabilities(modelId) {
112
+ for (const family of Object.values(MODEL_FAMILIES)) {
113
+ if (family.candidates.includes(modelId)) return { ...family.capabilities }
114
+ if (family.familyPattern instanceof RegExp && family.familyPattern.test(modelId)) {
115
+ return { ...family.capabilities }
116
+ }
117
+ }
118
+ return null
119
+ }
120
+
121
+ /**
122
+ * Capability hints for a model id. Authoritative for registered families;
123
+ * conservative heuristics otherwise. Callers must not treat `vision: false`
124
+ * from the heuristic path as proof a model cannot see images — it only means
125
+ * DevLoom has no evidence, which is the safe direction for routing decisions.
126
+ */
127
+ export function capabilitiesFor(modelId) {
128
+ const id = typeof modelId === "string" ? modelId : ""
129
+ const known = familyCapabilities(id)
130
+ if (known) return known
131
+ const caps = {
132
+ vision: false,
133
+ multimodal: false,
134
+ coding: "medium",
135
+ reasoning: "medium",
136
+ toolUse: true,
137
+ longContext: false,
138
+ speed: "medium",
139
+ costClass: "unknown",
140
+ orchestration: false,
141
+ verification: "medium",
142
+ }
143
+ // Known multimodal markers observed in OpenCode catalogs.
144
+ if (/vision|minimax-m3|qwen3\.6|mimo-v2\.[56]|glm-5\.3/i.test(id)) {
145
+ caps.vision = true
146
+ caps.multimodal = true
147
+ }
148
+ if (/-free$|big-pickle/.test(id)) caps.costClass = "free"
149
+ if (/flash/i.test(id)) caps.speed = "fast"
150
+ if (/pro|max|kimi-k3|glm-5\.2$/.test(id)) {
151
+ caps.reasoning = "high"
152
+ caps.coding = "high"
153
+ }
154
+ if (/kimi-k2\.7-code|kimi-k3|qwen3\.7|longcat|glm-5\.3$/.test(id)) caps.longContext = true
155
+ return caps
156
+ }
157
+
158
+ export function roleRequiresVision(role) {
159
+ return VISION_REQUIRED_ROLES.has(role)
160
+ }
161
+
162
+ /** Whether a model satisfies a role's capability requirements. */
163
+ export function modelMeetsRole(modelId, role) {
164
+ if (!roleRequiresVision(role)) return { ok: true, missing: [] }
165
+ const caps = capabilitiesFor(modelId)
166
+ const missing = []
167
+ if (!caps.vision) missing.push("vision")
168
+ return { ok: missing.length === 0, missing }
169
+ }
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ // Shared helpers for keeping the OpenCode plugin cache in sync with the
3
+ // installed DevLoom package.
4
+ //
5
+ // OpenCode installs npm plugins into `~/.cache/opencode/packages/<spec>` with
6
+ // `ignoreScripts: true`, so the package `postinstall` never runs there and the
7
+ // cached copy can silently go stale. These helpers refresh the cached package
8
+ // files (dist, agents, commands, ...) directly from a package source directory.
9
+ import {
10
+ existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync,
11
+ copyFileSync, rmSync, statSync,
12
+ } from "fs"
13
+ import { resolve, dirname, join } from "path"
14
+ import { fileURLToPath } from "url"
15
+ import { homedir, platform } from "os"
16
+ import { execFileSync } from "node:child_process"
17
+
18
+ export function getOpenCodeCacheDir(_platform = platform(), _homedir = homedir(), _env = process.env) {
19
+ if (_platform === "win32") {
20
+ return resolve(_env.LOCALAPPDATA ?? _homedir, "cache", "opencode")
21
+ }
22
+ if (_platform === "darwin") {
23
+ const lib = resolve(_homedir, "Library", "Caches", "opencode")
24
+ const legacy = resolve(_env.XDG_CACHE_HOME ?? join(_homedir, ".cache"), "opencode")
25
+ // Some opencode builds keep using ~/.cache/opencode on macOS; prefer the
26
+ // platform path unless only the legacy one exists on disk.
27
+ if (!existsSync(lib) && existsSync(legacy)) return legacy
28
+ return lib
29
+ }
30
+ return resolve(_env.XDG_CACHE_HOME ?? join(_homedir, ".cache"), "opencode")
31
+ }
32
+
33
+ // Names of OpenCode plugin cache entries that contain a `devloom` package.
34
+ // Both `devloom` (legacy) and `devloom@latest` (current config spec) exist in
35
+ // the wild; refreshing both is harmless and covers every loader path.
36
+ export function openCodePluginEntries(cacheDir) {
37
+ const packagesDir = join(cacheDir, "packages")
38
+ if (!existsSync(packagesDir)) return []
39
+ let entries = []
40
+ try {
41
+ entries = readdirSync(packagesDir)
42
+ } catch {
43
+ return []
44
+ }
45
+ return entries.filter((entry) => {
46
+ try {
47
+ return existsSync(join(packagesDir, entry, "node_modules", "devloom"))
48
+ } catch {
49
+ return false
50
+ }
51
+ })
52
+ }
53
+
54
+ function copyDir(src, dest) {
55
+ if (existsSync(dest) && !statSync(src).isDirectory()) {
56
+ rmSync(dest, { recursive: true, force: true })
57
+ }
58
+ const stat = statSync(src)
59
+ if (stat.isDirectory()) {
60
+ mkdirSync(dest, { recursive: true })
61
+ for (const name of readdirSync(src)) {
62
+ copyDir(join(src, name), join(dest, name))
63
+ }
64
+ } else {
65
+ mkdirSync(dirname(dest), { recursive: true })
66
+ copyFileSync(src, dest)
67
+ }
68
+ }
69
+
70
+ // Rebuild dist/ from source before it is copied into the OpenCode plugin cache,
71
+ // so a refresh never deploys stale compiled code. Runs the package's declared
72
+ // `build` script (npm run build) when present. Best-effort: never throws —
73
+ // failures are reported in the returned summary and the caller proceeds with
74
+ // whatever dist already exists.
75
+ export function buildPackageDist(sourceDir, exec = execFileSync) {
76
+ const summary = { built: false, skipped: "", errors: [] }
77
+ try {
78
+ const pkg = JSON.parse(readFileSync(join(sourceDir, "package.json"), "utf8"))
79
+ const build = pkg.scripts && typeof pkg.scripts.build === "string" ? pkg.scripts.build : ""
80
+ if (!build) {
81
+ summary.skipped = "no build script in package.json"
82
+ return summary
83
+ }
84
+ exec("npm", ["run", "build"], { cwd: sourceDir, stdio: "ignore", timeout: 120_000 })
85
+ summary.built = true
86
+ } catch (err) {
87
+ summary.errors.push(`build failed: ${err && typeof err.message === "string" ? err.message : String(err)}`)
88
+ }
89
+ return summary
90
+ }
91
+
92
+ // Copy the current package (dist, agents, commands, ... per package.json
93
+ // `files` plus package.json itself) into every existing OpenCode plugin cache
94
+ // entry. Best-effort: never throws, reports per-entry results.
95
+ export function refreshOpenCodePluginCache(sourceDir, cacheDir = getOpenCodeCacheDir(), exec = execFileSync) {
96
+ const summary = { refreshed: [], skipped: [], errors: [], build: null }
97
+ // Root-cause guard against the "status ok but stale" class of bugs: the
98
+ // refresh pipeline must ship freshly built code, not whatever dist happens
99
+ // to sit in the source tree. Rebuild before copying.
100
+ const build = buildPackageDist(sourceDir, exec)
101
+ summary.build = build
102
+ if (build.errors.length > 0) summary.errors.push(...build.errors)
103
+ let roots = []
104
+ try {
105
+ const pkg = JSON.parse(readFileSync(join(sourceDir, "package.json"), "utf8"))
106
+ roots = Array.isArray(pkg.files) ? pkg.files : []
107
+ } catch (err) {
108
+ summary.errors.push(`cannot read package.json at ${sourceDir}: ${err.message}`)
109
+ return summary
110
+ }
111
+
112
+ const entries = openCodePluginEntries(cacheDir)
113
+ if (entries.length === 0) {
114
+ summary.skipped.push(cacheDir)
115
+ return summary
116
+ }
117
+
118
+ const toCopy = [...roots, "package.json"]
119
+ for (const entry of entries) {
120
+ const destPkg = join(cacheDir, "packages", entry, "node_modules", "devloom")
121
+ let copied = 0
122
+ for (const root of toCopy) {
123
+ const src = join(sourceDir, root)
124
+ if (!existsSync(src)) continue
125
+ try {
126
+ copyDir(src, join(destPkg, root))
127
+ copied++
128
+ } catch (err) {
129
+ summary.errors.push(`${entry}/${root}: ${err.message}`)
130
+ }
131
+ }
132
+ if (copied > 0) summary.refreshed.push(entry)
133
+ }
134
+ return summary
135
+ }
136
+
137
+ // Sync the installed agent files into the cached plugin package so a profile
138
+ // change is reflected even in the cache copy. Best-effort, never throws.
139
+ export function refreshOpenCodePluginAgentFiles(agentsDir, cacheDir = getOpenCodeCacheDir()) {
140
+ const summary = { refreshed: [], skipped: [], errors: [] }
141
+ const entries = openCodePluginEntries(cacheDir)
142
+ if (entries.length === 0) {
143
+ summary.skipped.push(cacheDir)
144
+ return summary
145
+ }
146
+ for (const entry of entries) {
147
+ const destAgents = join(cacheDir, "packages", entry, "node_modules", "devloom", "agents")
148
+ try {
149
+ mkdirSync(destAgents, { recursive: true })
150
+ const files = readdirSync(agentsDir).filter((f) => f.startsWith("devloom-") && f.endsWith(".md"))
151
+ for (const file of files) {
152
+ copyFileSync(join(agentsDir, file), join(destAgents, file))
153
+ }
154
+ summary.refreshed.push(entry)
155
+ } catch (err) {
156
+ summary.errors.push(`${entry}: ${err.message}`)
157
+ }
158
+ }
159
+ return summary
160
+ }
161
+
162
+ // Sync the runtime dependencies of the installed package into every existing
163
+ // cache entry. The TUI plugin (dist/tui.js) imports `@opentui/solid` +
164
+ // `solid-js`, and OpenCode installs plugins with `ignoreScripts` so those deps
165
+ // are not guaranteed to exist in the cache. Running `npm install` inside the
166
+ // cached plugin directory mirrors what a normal package install would do,
167
+ // without touching the cache entry root (no pruning of existing packages).
168
+ // Best-effort: never throws, reports per-entry results.
169
+ export function syncOpenCodePluginDependencies(sourceDir, cacheDir = getOpenCodeCacheDir(), exec = execFileSync) {
170
+ const summary = { synced: [], skipped: [], errors: [] }
171
+ const entries = openCodePluginEntries(cacheDir)
172
+ if (entries.length === 0) {
173
+ summary.skipped.push(cacheDir)
174
+ return summary
175
+ }
176
+ for (const entry of entries) {
177
+ const pluginDir = join(cacheDir, "packages", entry, "node_modules", "devloom")
178
+ try {
179
+ exec(
180
+ "npm",
181
+ [
182
+ "install", "--prefix", pluginDir,
183
+ "--no-save", "--ignore-scripts", "--no-audit", "--no-fund",
184
+ "--no-package-lock", "--omit=dev", "--loglevel=error",
185
+ ],
186
+ { stdio: "ignore", timeout: 120_000 }
187
+ )
188
+ summary.synced.push(entry)
189
+ } catch (err) {
190
+ summary.errors.push(`${entry}: ${err && typeof err.message === "string" ? err.message : String(err)}`)
191
+ }
192
+ }
193
+ return summary
194
+ }
195
+
196
+ // Per-entry cache health breakdown.
197
+ // "ok" – every existing cache entry has the config hook that injects DevLoom
198
+ // agents AND the current feature code (the profile-filter wiring in
199
+ // dist/agents.js, detected via `agentVariantVisible`). Conservative:
200
+ // one stale entry makes the whole cache stale.
201
+ // "stale"– at least one cache entry lacks the config hook or the feature code.
202
+ // "missing" – DevLoom was never installed as an OpenCode plugin.
203
+ export function pluginCacheStatusDetail(cacheDir = getOpenCodeCacheDir()) {
204
+ const entries = openCodePluginEntries(cacheDir)
205
+ if (entries.length === 0) return { status: "missing", entries: [] }
206
+ const details = entries.map((entry) => {
207
+ const base = join(cacheDir, "packages", entry, "node_modules", "devloom")
208
+ let hook = false
209
+ let featureCode = false
210
+ let orchestratorLabel = false
211
+ try {
212
+ const pluginJs = join(base, "dist", "plugin.js")
213
+ if (existsSync(pluginJs)) {
214
+ hook = readFileSync(pluginJs, "utf8").includes("injectDevloomAgents")
215
+ }
216
+ } catch { /* entry treated as stale */ }
217
+ try {
218
+ // The profile-filter feature marker: present only in freshly built dist.
219
+ const agentsJs = join(base, "dist", "agents.js")
220
+ if (existsSync(agentsJs)) {
221
+ featureCode = readFileSync(agentsJs, "utf8").includes("agentVariantVisible")
222
+ }
223
+ } catch { /* entry treated as stale */ }
224
+ try {
225
+ const orchestratorMd = join(base, "agents", "devloom-orchestrator.md")
226
+ if (existsSync(orchestratorMd)) {
227
+ orchestratorLabel = /\(profile: [^)]+\)/.test(readFileSync(orchestratorMd, "utf8"))
228
+ }
229
+ } catch { /* label unknown -> false */ }
230
+ return { entry, hook, featureCode, orchestratorLabel }
231
+ })
232
+ const status = details.every((detail) => detail.hook && detail.featureCode) ? "ok" : "stale"
233
+ return { status, entries: details }
234
+ }
235
+
236
+ export function pluginCacheStatus(cacheDir = getOpenCodeCacheDir()) {
237
+ return pluginCacheStatusDetail(cacheDir).status
238
+ }
239
+
240
+ export function runCli(args, cacheDir = getOpenCodeCacheDir()) {
241
+ const command = args[0] || ""
242
+ if (command === "status") {
243
+ const detail = pluginCacheStatusDetail(cacheDir)
244
+ console.log(`DevLoom plugin cache: ${detail.status}`)
245
+ if (detail.entries.length === 0) {
246
+ console.log(" No DevLoom plugin cache entry found.")
247
+ console.log(" Install the plugin once with: opencode plugin devloom --global")
248
+ console.log(" then refresh the cache with: npm install -g devloom && node $(npm root -g)/devloom/postinstall.mjs")
249
+ return detail.status
250
+ }
251
+ for (const entry of detail.entries) {
252
+ const hook = entry.hook ? "yes" : "NO"
253
+ const feature = entry.featureCode ? "yes" : "NO"
254
+ const label = entry.orchestratorLabel ? "yes" : "no"
255
+ console.log(` ${entry.entry}: config-hook=${hook} feature-code=${feature} orchestrator-profile-label=${label}`)
256
+ }
257
+ if (detail.status !== "ok") {
258
+ console.log(" Fix: run /devloom-refresh, then restart opencode (or: opencode --continue).")
259
+ }
260
+ return detail.status
261
+ }
262
+ console.log("DevLoom plugin cache helpers")
263
+ console.log("Commands:")
264
+ console.log(" devloom plugin-cache status — print cache health (ok/stale/missing) + per-entry info")
265
+ return null
266
+ }
267
+
268
+ const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false
269
+ if (isMain) runCli(process.argv.slice(2))