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,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DevLoom visual benchmark — validates CRITIC_VERDICT records against the
4
+ * visual-loop contract: score thresholds, max 3 refinement cycles, and no
5
+ * VISUAL_PASS claimed from a limited critique.
6
+ *
7
+ * Usage:
8
+ * node scripts/visual-benchmark.mjs <verdicts-dir> [--json]
9
+ *
10
+ * Each `<name>.json` file in the directory:
11
+ * {
12
+ * "ticket": "T-1",
13
+ * "verdict": "VISUAL_PASS" | "VISUAL_FAIL" | "VISUAL_CRITIQUE_LIMITED",
14
+ * "score": 8.5,
15
+ * "cycles": 2,
16
+ * "critiqueLimited": false,
17
+ * "dimensions": { "layout": 9, "a11y": 8 }
18
+ * }
19
+ *
20
+ * Exit code 0 = every record honors the contract; 1 = violations or no records.
21
+ */
22
+
23
+ import { readdirSync, readFileSync } from "node:fs"
24
+ import { join } from "node:path"
25
+ import { pathToFileURL } from "node:url"
26
+
27
+ export const VERDICTS = ["VISUAL_PASS", "VISUAL_FAIL", "VISUAL_CRITIQUE_LIMITED"]
28
+ export const PASS_MIN_SCORE = 7
29
+ export const MAX_VISUAL_LOOPS = 3
30
+ const SCORE_MIN = 0
31
+ const SCORE_MAX = 10
32
+
33
+ function isScore(value) {
34
+ return typeof value === "number" && Number.isFinite(value) && value >= SCORE_MIN && value <= SCORE_MAX
35
+ }
36
+
37
+ /** Returns an array of contract violations for one verdict record (empty = valid). */
38
+ export function checkVerdict(record, label = "record") {
39
+ const violations = []
40
+ if (record === null || typeof record !== "object" || Array.isArray(record)) {
41
+ return [`${label}: not a JSON object`]
42
+ }
43
+ const { verdict, score, cycles, critiqueLimited, dimensions } = record
44
+
45
+ if (!VERDICTS.includes(verdict)) {
46
+ violations.push(`${label}: unknown verdict "${String(verdict)}" (expected ${VERDICTS.join(" | ")})`)
47
+ }
48
+ if (!isScore(score)) {
49
+ violations.push(`${label}: score must be a number in ${SCORE_MIN}..${SCORE_MAX} (got ${JSON.stringify(score)})`)
50
+ }
51
+ if (!Number.isInteger(cycles) || cycles < 0) {
52
+ violations.push(`${label}: cycles must be a non-negative integer (got ${JSON.stringify(cycles)})`)
53
+ } else if (cycles > MAX_VISUAL_LOOPS) {
54
+ violations.push(`${label}: loop cap exceeded — ${cycles} cycles > max ${MAX_VISUAL_LOOPS}`)
55
+ }
56
+ if (verdict === "VISUAL_PASS") {
57
+ if (isScore(score) && score < PASS_MIN_SCORE) {
58
+ violations.push(`${label}: false VISUAL_PASS — score ${score} < required ${PASS_MIN_SCORE}`)
59
+ }
60
+ if (critiqueLimited === true) {
61
+ violations.push(`${label}: false VISUAL_PASS — critique was limited (no screenshots), report VISUAL_CRITIQUE_LIMITED instead`)
62
+ }
63
+ }
64
+ if (dimensions !== undefined) {
65
+ if (dimensions === null || typeof dimensions !== "object" || Array.isArray(dimensions)) {
66
+ violations.push(`${label}: dimensions must be an object of 0..10 scores`)
67
+ } else {
68
+ for (const [dim, value] of Object.entries(dimensions)) {
69
+ if (!isScore(value)) violations.push(`${label}: dimension "${dim}" must be 0..10 (got ${JSON.stringify(value)})`)
70
+ }
71
+ }
72
+ }
73
+ return violations
74
+ }
75
+
76
+ /** Aggregates checked records into a summary object. */
77
+ export function summarize(results) {
78
+ const summary = { total: results.length, pass: 0, fail: 0, limited: 0, avgScore: 0, violations: 0 }
79
+ let scoreSum = 0
80
+ let scoreCount = 0
81
+ for (const r of results) {
82
+ if (r.record?.verdict === "VISUAL_PASS") summary.pass++
83
+ else if (r.record?.verdict === "VISUAL_CRITIQUE_LIMITED") summary.limited++
84
+ else if (r.record?.verdict === "VISUAL_FAIL") summary.fail++
85
+ summary.violations += r.violations.length
86
+ if (isScore(r.record?.score)) {
87
+ scoreSum += r.record.score
88
+ scoreCount++
89
+ }
90
+ }
91
+ summary.avgScore = scoreCount ? Math.round((scoreSum / scoreCount) * 100) / 100 : 0
92
+ return summary
93
+ }
94
+
95
+ /** Reads every `*.json` verdict in `dir`, checks each, and returns { results, summary, ok }. */
96
+ export function runBenchmark(dir) {
97
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json")).sort()
98
+ const results = files.map((file) => {
99
+ let record = null
100
+ let parseError = null
101
+ try {
102
+ record = JSON.parse(readFileSync(join(dir, file), "utf8"))
103
+ } catch (err) {
104
+ parseError = `${file}: invalid JSON (${err.message})`
105
+ }
106
+ const violations = parseError ? [parseError] : checkVerdict(record, file)
107
+ return { file, record, violations }
108
+ })
109
+ const summary = summarize(results)
110
+ const ok = results.length > 0 && summary.violations === 0
111
+ return { results, summary, ok }
112
+ }
113
+
114
+ function main(argv) {
115
+ const args = argv.filter((a) => a !== "--json")
116
+ const asJson = argv.includes("--json")
117
+ const dir = args[0]
118
+ if (!dir) {
119
+ console.error("usage: node scripts/visual-benchmark.mjs <verdicts-dir> [--json]")
120
+ return 1
121
+ }
122
+ let outcome
123
+ try {
124
+ outcome = runBenchmark(dir)
125
+ } catch (err) {
126
+ console.error(`visual-benchmark: cannot read "${dir}" — ${err.message}`)
127
+ return 1
128
+ }
129
+ const { results, summary, ok } = outcome
130
+ if (asJson) {
131
+ console.log(JSON.stringify({ summary, results }, null, 2))
132
+ } else {
133
+ if (results.length === 0) console.log("No verdict records found — nothing to claim.")
134
+ for (const r of results) {
135
+ const mark = r.violations.length ? "FAIL" : " ok "
136
+ console.log(`[${mark}] ${r.file} verdict=${r.record?.verdict ?? "?"} score=${r.record?.score ?? "?"} cycles=${r.record?.cycles ?? "?"}`)
137
+ for (const v of r.violations) console.log(` - ${v}`)
138
+ }
139
+ console.log(
140
+ `summary: total=${summary.total} pass=${summary.pass} fail=${summary.fail} limited=${summary.limited} avgScore=${summary.avgScore} violations=${summary.violations}`
141
+ )
142
+ if (results.length === 0) console.log("Contract: zero records is never a pass — produce CRITIC_VERDICT evidence first.")
143
+ }
144
+ return ok ? 0 : 1
145
+ }
146
+
147
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
148
+ process.exit(main(process.argv.slice(2)))
149
+ }
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "child_process"
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from "fs"
4
+ import { resolve, join, basename, dirname } from "path"
5
+ import { fileURLToPath } from "url"
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+
9
+ const WORKTREE_DIR = ".devloom-worktrees"
10
+ const REGISTRY_PATH = join(WORKTREE_DIR, ".registry.json")
11
+ const GIT_TIMEOUT = 30000
12
+
13
+ function git(rootDir, args, opts = {}) {
14
+ const timeout = opts.timeout || GIT_TIMEOUT
15
+ try {
16
+ return execSync(`git ${args}`, {
17
+ cwd: rootDir,
18
+ encoding: "utf8",
19
+ timeout,
20
+ stdio: opts.stdio || ["pipe", "pipe", "pipe"],
21
+ }).trim()
22
+ } catch (err) {
23
+ if (opts.throwOnError !== false) {
24
+ const stderr = err.stderr ? err.stderr.trim() : ""
25
+ throw new Error(`git ${args.split(" ")[0]} failed: ${err.message}${stderr ? " | " + stderr : ""}`)
26
+ }
27
+ return ""
28
+ }
29
+ }
30
+
31
+ function gitOk(rootDir, args) {
32
+ try {
33
+ execSync(`git ${args}`, { cwd: rootDir, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["pipe", "pipe", "pipe"] })
34
+ return true
35
+ } catch {
36
+ return false
37
+ }
38
+ }
39
+
40
+ function readJson(path, fallback = null) {
41
+ try {
42
+ return JSON.parse(readFileSync(path, "utf8"))
43
+ } catch {
44
+ return fallback
45
+ }
46
+ }
47
+
48
+ function writeJson(path, data) {
49
+ const dir = dirname(path)
50
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
51
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n")
52
+ }
53
+
54
+ function isGitRepo(rootDir) {
55
+ return gitOk(rootDir, "rev-parse --is-inside-work-tree")
56
+ }
57
+
58
+ export function getCurrentBranch(rootDir) {
59
+ return git(rootDir, "rev-parse --abbrev-ref HEAD")
60
+ }
61
+
62
+ export function isClean(rootDir) {
63
+ const status = git(rootDir, "status --porcelain")
64
+ return status.length === 0
65
+ }
66
+
67
+ export function hasUntrackedFiles(rootDir) {
68
+ const status = git(rootDir, "status --porcelain")
69
+ return status.split("\n").some(l => l.startsWith("??"))
70
+ }
71
+
72
+ function commitAll(rootDir, message) {
73
+ git(rootDir, "add -A")
74
+ if (isClean(rootDir)) return null
75
+ git(rootDir, `commit -m ${JSON.stringify(message)}`)
76
+ return git(rootDir, "rev-parse HEAD")
77
+ }
78
+
79
+ function stashIfDirty(rootDir) {
80
+ if (isClean(rootDir)) return null
81
+ try {
82
+ return git(rootDir, `stash push -u -m ${JSON.stringify("devloom-auto-stash-" + Date.now())}`)
83
+ } catch {
84
+ return commitAll(rootDir, "devloom: auto-commit before worktree creation")
85
+ }
86
+ }
87
+
88
+ function branchSlug(branchName) {
89
+ return branchName.replace(/[^a-zA-Z0-9_-]/g, "-")
90
+ }
91
+
92
+ function worktreePath(rootDir, branchName) {
93
+ return join(rootDir, WORKTREE_DIR, branchSlug(branchName))
94
+ }
95
+
96
+ function readRegistry(rootDir) {
97
+ return readJson(join(rootDir, REGISTRY_PATH), { worktrees: [] })
98
+ }
99
+
100
+ function writeRegistry(rootDir, registry) {
101
+ writeJson(join(rootDir, REGISTRY_PATH), registry)
102
+ }
103
+
104
+ function addToRegistry(rootDir, entry) {
105
+ const reg = readRegistry(rootDir)
106
+ const existing = reg.worktrees.findIndex(w => w.branch === entry.branch)
107
+ if (existing >= 0) {
108
+ reg.worktrees[existing] = entry
109
+ } else {
110
+ reg.worktrees.push(entry)
111
+ }
112
+ writeRegistry(rootDir, reg)
113
+ }
114
+
115
+ function removeFromRegistry(rootDir, branchName) {
116
+ const reg = readRegistry(rootDir)
117
+ reg.worktrees = reg.worktrees.filter(w => w.branch !== branchName)
118
+ writeRegistry(rootDir, reg)
119
+ }
120
+
121
+ function ensureGitignore(rootDir) {
122
+ const gitignorePath = join(rootDir, ".gitignore")
123
+ const entry = ".devloom-worktrees/"
124
+ try {
125
+ const content = readFileSync(gitignorePath, "utf8")
126
+ if (!content.includes(entry)) {
127
+ writeFileSync(gitignorePath, content + (content.endsWith("\n") ? "" : "\n") + entry + "\n")
128
+ }
129
+ } catch {
130
+ writeFileSync(gitignorePath, entry + "\n")
131
+ }
132
+ }
133
+
134
+ export function createWorktree(rootDir, branchName, baseRef) {
135
+ if (!isGitRepo(rootDir)) {
136
+ throw new Error("not a git repository")
137
+ }
138
+
139
+ const ref = baseRef || getCurrentBranch(rootDir)
140
+ const wtPath = worktreePath(rootDir, branchName)
141
+
142
+ if (existsSync(wtPath)) {
143
+ gitOk(rootDir, `worktree remove --force ${JSON.stringify(wtPath)}`)
144
+ rmSync(wtPath, { recursive: true, force: true })
145
+ }
146
+
147
+ const existingBranches = git(rootDir, "branch --list", { throwOnError: false })
148
+ if (existingBranches.includes(branchName)) {
149
+ git(rootDir, `branch -D ${JSON.stringify(branchName)}`)
150
+ }
151
+
152
+ ensureGitignore(rootDir)
153
+
154
+ if (!isClean(rootDir)) {
155
+ commitAll(rootDir, `devloom: auto-commit before worktree ${branchName}`)
156
+ }
157
+
158
+ git(rootDir, `worktree add -b ${JSON.stringify(branchName)} ${JSON.stringify(wtPath)} ${JSON.stringify(ref)}`)
159
+
160
+ const entry = {
161
+ branch: branchName,
162
+ path: wtPath,
163
+ baseRef: ref,
164
+ createdAt: new Date().toISOString(),
165
+ ticket: branchName.includes("/") ? branchName.split("/")[1] : branchName,
166
+ agent: branchName.split("/")[2] || "unknown",
167
+ }
168
+ addToRegistry(rootDir, entry)
169
+
170
+ return entry
171
+ }
172
+
173
+ export function mergeWorktree(rootDir, branchName) {
174
+ if (!isGitRepo(rootDir)) {
175
+ throw new Error("not a git repository")
176
+ }
177
+
178
+ const wtPath = worktreePath(rootDir, branchName)
179
+ if (!existsSync(wtPath)) {
180
+ throw new Error(`worktree not found: ${branchName}`)
181
+ }
182
+
183
+ if (!isClean(wtPath)) {
184
+ commitAll(wtPath, `devloom: auto-commit in worktree ${branchName} before merge`)
185
+ }
186
+
187
+ const currentBranch = getCurrentBranch(rootDir)
188
+ if (!isClean(rootDir)) {
189
+ commitAll(rootDir, `devloom: auto-commit before merge of ${branchName}`)
190
+ }
191
+
192
+ const mergeResult = git(rootDir, `merge --no-ff ${JSON.stringify(branchName)} -m ${JSON.stringify(`devloom: merge ${branchName}`)}`, { throwOnError: false })
193
+
194
+ const hasMergeHead = gitOk(rootDir, "rev-parse -q --verify MERGE_HEAD")
195
+
196
+ if (hasMergeHead) {
197
+ const conflicts = []
198
+ const status = git(rootDir, "diff --name-only --diff-filter=U", { throwOnError: false })
199
+ if (status) {
200
+ conflicts.push(...status.split("\n").filter(Boolean))
201
+ }
202
+
203
+ if (conflicts.length > 0 || mergeResult.includes("CONFLICT") || mergeResult.includes("Automatic merge failed")) {
204
+ git(rootDir, "merge --abort")
205
+ return {
206
+ success: false,
207
+ conflicts,
208
+ message: `merge conflict in ${conflicts.length} file(s): ${conflicts.join(", ")}`,
209
+ branch: branchName,
210
+ worktreePath: wtPath,
211
+ }
212
+ }
213
+ git(rootDir, "merge --abort")
214
+ return {
215
+ success: false,
216
+ conflicts: [],
217
+ message: "merge conflict detected (auto-aborted)",
218
+ branch: branchName,
219
+ worktreePath: wtPath,
220
+ }
221
+ }
222
+
223
+ git(rootDir, `worktree remove ${JSON.stringify(wtPath)}`)
224
+ git(rootDir, `branch -d ${JSON.stringify(branchName)}`, { throwOnError: false })
225
+ rmSync(wtPath, { recursive: true, force: true })
226
+ removeFromRegistry(rootDir, branchName)
227
+
228
+ return {
229
+ success: true,
230
+ conflicts: [],
231
+ message: `merged ${branchName} into ${currentBranch}`,
232
+ branch: branchName,
233
+ mergedInto: currentBranch,
234
+ }
235
+ }
236
+
237
+ export function removeWorktree(rootDir, branchName, force = false) {
238
+ if (!isGitRepo(rootDir)) {
239
+ throw new Error("not a git repository")
240
+ }
241
+
242
+ const wtPath = worktreePath(rootDir, branchName)
243
+ if (!existsSync(wtPath)) {
244
+ removeFromRegistry(rootDir, branchName)
245
+ return { removed: false, message: `worktree not found: ${branchName}` }
246
+ }
247
+
248
+ if (!force && !isClean(wtPath)) {
249
+ const status = git(wtPath, "status --porcelain")
250
+ throw new Error(`worktree ${branchName} has uncommitted changes. Use --force to discard.\nUncommitted:\n${status}`)
251
+ }
252
+
253
+ git(rootDir, `worktree remove --force ${JSON.stringify(wtPath)}`)
254
+ git(rootDir, `branch -D ${JSON.stringify(branchName)}`, { throwOnError: false })
255
+ rmSync(wtPath, { recursive: true, force: true })
256
+ removeFromRegistry(rootDir, branchName)
257
+
258
+ return { removed: true, message: `removed worktree ${branchName}` }
259
+ }
260
+
261
+ export function listWorktrees(rootDir) {
262
+ if (!isGitRepo(rootDir)) return []
263
+ const reg = readRegistry(rootDir)
264
+ return reg.worktrees.filter(w => existsSync(w.path))
265
+ }
266
+
267
+ export function worktreeStatus(rootDir, branchName) {
268
+ if (!isGitRepo(rootDir)) {
269
+ throw new Error("not a git repository")
270
+ }
271
+
272
+ const reg = readRegistry(rootDir)
273
+ const entry = reg.worktrees.find(w => w.branch === branchName)
274
+ if (!entry || !existsSync(entry.path)) {
275
+ return { branch: branchName, exists: false, clean: false, ahead: 0, behind: 0 }
276
+ }
277
+
278
+ const clean = isClean(entry.path)
279
+ const aheadStr = git(rootDir, `rev-list --count ${JSON.stringify(entry.baseRef)}..${JSON.stringify(branchName)}`, { throwOnError: false })
280
+ const behindStr = git(rootDir, `rev-list --count ${JSON.stringify(branchName)}..${JSON.stringify(entry.baseRef)}`, { throwOnError: false })
281
+
282
+ return {
283
+ branch: branchName,
284
+ exists: true,
285
+ path: entry.path,
286
+ clean,
287
+ ahead: parseInt(aheadStr) || 0,
288
+ behind: parseInt(behindStr) || 0,
289
+ createdAt: entry.createdAt,
290
+ ticket: entry.ticket,
291
+ agent: entry.agent,
292
+ }
293
+ }
294
+
295
+ export function cleanWorktrees(rootDir, force = false) {
296
+ if (!isGitRepo(rootDir)) return []
297
+ const reg = readRegistry(rootDir)
298
+ const results = []
299
+
300
+ for (const w of reg.worktrees) {
301
+ try {
302
+ const aheadStr = git(rootDir, `rev-list --count ${JSON.stringify(w.baseRef)}..${JSON.stringify(w.branch)}`, { throwOnError: false })
303
+ const ahead = parseInt(aheadStr) || 0
304
+ const isMerged = ahead > 0 && gitOk(rootDir, `merge-base --is-ancestor ${JSON.stringify(w.branch)} HEAD`)
305
+ if (isMerged || force) {
306
+ const r = removeWorktree(rootDir, w.branch, true)
307
+ results.push({ branch: w.branch, removed: true, ...r })
308
+ } else {
309
+ results.push({ branch: w.branch, removed: false, reason: "unmerged (use --force)" })
310
+ }
311
+ } catch (err) {
312
+ results.push({ branch: w.branch, removed: false, reason: err.message })
313
+ }
314
+ }
315
+
316
+ return results
317
+ }
318
+
319
+ function cmdCreate(rootDir, branchName, baseRef) {
320
+ const entry = createWorktree(rootDir, branchName, baseRef)
321
+ console.log(JSON.stringify({ ok: true, ...entry }, null, 2))
322
+ }
323
+
324
+ function cmdMerge(rootDir, branchName) {
325
+ const result = mergeWorktree(rootDir, branchName)
326
+ console.log(JSON.stringify({ ok: result.success, ...result }, null, 2))
327
+ if (!result.success) process.exitCode = 1
328
+ }
329
+
330
+ function cmdRemove(rootDir, branchName, force) {
331
+ const result = removeWorktree(rootDir, branchName, force)
332
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2))
333
+ }
334
+
335
+ function cmdList(rootDir) {
336
+ const worktrees = listWorktrees(rootDir)
337
+ if (worktrees.length === 0) {
338
+ console.log("No active DevLoom worktrees.")
339
+ return
340
+ }
341
+ console.log(`Active DevLoom worktrees (${worktrees.length}):`)
342
+ for (const w of worktrees) {
343
+ const st = worktreeStatus(rootDir, w.branch)
344
+ const dirty = st.clean ? "clean" : "DIRTY"
345
+ console.log(` ${w.branch} [${dirty}] ahead=${st.ahead} behind=${st.behind} ${w.path}`)
346
+ }
347
+ }
348
+
349
+ function cmdStatus(rootDir, branchName) {
350
+ if (branchName) {
351
+ const st = worktreeStatus(rootDir, branchName)
352
+ console.log(JSON.stringify(st, null, 2))
353
+ } else {
354
+ const worktrees = listWorktrees(rootDir)
355
+ const statuses = worktrees.map(w => worktreeStatus(rootDir, w.branch))
356
+ console.log(JSON.stringify(statuses, null, 2))
357
+ }
358
+ }
359
+
360
+ function cmdClean(rootDir, force) {
361
+ const results = cleanWorktrees(rootDir, force)
362
+ if (results.length === 0) {
363
+ console.log("No worktrees to clean.")
364
+ return
365
+ }
366
+ for (const r of results) {
367
+ const status = r.removed ? "removed" : `skipped (${r.reason})`
368
+ console.log(` ${r.branch}: ${status}`)
369
+ }
370
+ }
371
+
372
+ function printHelp() {
373
+ console.log("DevLoom Git Worktree Manager")
374
+ console.log("")
375
+ console.log("Usage:")
376
+ console.log(" worktree.mjs create <branch> [--base <ref>] Create a worktree on a new branch")
377
+ console.log(" worktree.mjs merge <branch> Merge worktree branch to current, remove worktree")
378
+ console.log(" worktree.mjs remove <branch> [--force] Remove worktree + branch (force discards uncommitted)")
379
+ console.log(" worktree.mjs list List active DevLoom worktrees")
380
+ console.log(" worktree.mjs status [branch] Show worktree status")
381
+ console.log(" worktree.mjs clean [--force] Remove all merged worktrees (or all with --force)")
382
+ console.log("")
383
+ console.log("Safety guarantees:")
384
+ console.log(" - Auto-commits dirty changes before worktree creation")
385
+ console.log(" - Auto-commits in worktree before merge")
386
+ console.log(" - Aborts merge on conflict, keeps worktree for manual resolution")
387
+ console.log(" - Never deletes unmerged branches without --force")
388
+ console.log("")
389
+ console.log("Branch naming convention: devloom/<ticket-id>/<agent-name>")
390
+ }
391
+
392
+ function main() {
393
+ const args = process.argv.slice(2)
394
+ const command = args[0] || ""
395
+ const rootDir = process.cwd()
396
+
397
+ try {
398
+ switch (command) {
399
+ case "create": {
400
+ const branch = args[1]
401
+ if (!branch) { console.error("Usage: worktree.mjs create <branch> [--base <ref>]"); process.exit(1) }
402
+ const baseIdx = args.indexOf("--base")
403
+ const baseRef = baseIdx >= 0 ? args[baseIdx + 1] : undefined
404
+ cmdCreate(rootDir, branch, baseRef)
405
+ break
406
+ }
407
+ case "merge": {
408
+ const branch = args[1]
409
+ if (!branch) { console.error("Usage: worktree.mjs merge <branch>"); process.exit(1) }
410
+ cmdMerge(rootDir, branch)
411
+ break
412
+ }
413
+ case "remove": {
414
+ const branch = args[1]
415
+ if (!branch) { console.error("Usage: worktree.mjs remove <branch> [--force]"); process.exit(1) }
416
+ const force = args.includes("--force")
417
+ cmdRemove(rootDir, branch, force)
418
+ break
419
+ }
420
+ case "list":
421
+ cmdList(rootDir)
422
+ break
423
+ case "status":
424
+ cmdStatus(rootDir, args[1])
425
+ break
426
+ case "clean":
427
+ cmdClean(rootDir, args.includes("--force"))
428
+ break
429
+ case "help":
430
+ case "--help":
431
+ case "-h":
432
+ printHelp()
433
+ break
434
+ default:
435
+ printHelp()
436
+ process.exit(1)
437
+ }
438
+ } catch (err) {
439
+ console.error(`Error: ${err.message}`)
440
+ process.exit(1)
441
+ }
442
+ }
443
+
444
+ const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false
445
+ if (isMain) main()
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: development
3
+ description: Implement tickets and fix defects with TDD, SOLID, and clean code; root-cause-first for bugs.
4
+ ---
5
+
6
+ LOAD: ~/.config/opencode/devloom-ai/core.dsl|~/.config/opencode/devloom-ai/workflow.dsl
7
+
8
+ IMPLEMENT:
9
+ - read ticket + PLAN + source + .opencode/devloom/context/design.md (DESIGN_CONTRACT when VISUAL_REQUIRED)
10
+ - TDD: Red>Green>Refactor; test-first for new logic
11
+ - min code to pass, then refactor to SOLID/CleanCode
12
+ - respect CleanArch layer boundaries (no domain->framework)
13
+ - MatchLocalStyle|NoScopeCreep|GuardEdges
14
+ - VISUAL_REQUIRED: match design tokens/spacing/type/motion from DESIGN_CONTRACT; finish with `VISUAL_IMPLEMENTATION_COMPLETE` + touched UI routes/components
15
+ - CHK: build|tests|regr
16
+
17
+ FIX_DEFECT:
18
+ - root-cause-first: symptom>Repro(failing test)>trace>cause>proof>fix
19
+ - NoGuess|NoSymptomPatch|FixRootOnly|MinChange|KeepTests
20
+ - add guard test that fails pre-fix, passes post-fix
21
+ - BUGFLOW: ReproTest>Fail>Fix>Pass>FullRegr
22
+
23
+ FRONTEND:
24
+ - responsive + semantic HTML + accessible interactions (WCAG-AA)
25
+ - explicit loading|error|empty|success states; no layout shift
26
+ BACKEND:
27
+ - focused modules, validate all inputs, explicit auth/authz, least privilege
28
+ - handle null|empty|boundary|error paths
29
+ API:
30
+ - explicit request|response|error|auth|paging contracts; stable names; backward-compatible by default
31
+ - align runtime with OpenAPI/spec when present
32
+
33
+ RULES: one ticket/defect at a time|surgical smallest-correct diff|NO workarounds — fix root with the idiomatic solution|assert outcomes not impl trivia|OfficialDocsFirst for stack-specific code.
@@ -0,0 +1,38 @@
1
+ # Live Docs Fetching
2
+
3
+ ROLE: fetch current official documentation for external libraries before generating code that uses them — no outdated training data
4
+
5
+ ## When to use (auto-triggered)
6
+
7
+ - You are about to write code that imports or uses an external library
8
+ - You are unsure of the current API surface of a library
9
+ - The library version in package.json/requirements.txt/go.mod is newer than your training cutoff
10
+ - A user mentions a specific library version or feature
11
+
12
+ ## Process (3 steps max)
13
+
14
+ 1. DETECT: scan the prompt or plan for library/framework names. Check package.json, requirements.txt, go.mod, Cargo.toml for exact versions.
15
+ 2. FETCH: use `webfetch` to get the official documentation page for the detected library. Prefer:
16
+ - Official docs site (e.g., https://nextjs.org/docs, https://drizzle.team/docs)
17
+ - npm/GitHub README for smaller libraries
18
+ - API reference pages for specific function signatures
19
+ 3. GROUND: write code using the fetched API surface, not your training data. Cite the doc URL in a comment if the API is non-obvious.
20
+
21
+ ## Rules
22
+
23
+ - Fetch ONLY when you will actually use the library in code. Do not pre-fetch "just in case" — that wastes tokens.
24
+ - One fetch per library per session is enough. Cache the result mentally.
25
+ - If the fetch fails or returns irrelevant content, fall back to your training data and flag the uncertainty.
26
+ - Never guess API signatures from training data when a fetch is possible — one webfetch is cheaper than debugging wrong API usage.
27
+ - Keep fetched context minimal — read the specific API page, not the entire docs site.
28
+
29
+ ## Anti-rationalization
30
+
31
+ | Excuse | Rebuttal |
32
+ |---|---|
33
+ | "I know this library from training" | Training data has a cutoff. The version in package.json may be newer. One fetch confirms. |
34
+ | "Fetching docs wastes tokens" | Wrong API usage wastes 10x more tokens in debugging. One fetch is cheaper. |
35
+ | "The API hasn't changed" | You don't know that without checking. Fetch to confirm. |
36
+ | "I'll fix it if the API is wrong" | Prevention is cheaper than repair. Fetch first, code once. |
37
+
38
+ OUT: code grounded in current official docs, not stale training data