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.
- package/.ai/core.dsl +44 -0
- package/.ai/skills.dsl +39 -0
- package/.ai/verify.dsl +97 -0
- package/.ai/workflow.dsl +81 -0
- package/.opencode/themes/devloom-night-owl.json +103 -0
- package/GUIDE.md +572 -115
- package/README.md +583 -109
- package/SECURITY.md +93 -0
- package/agents/devloom-developer-flash.md +31 -0
- package/agents/devloom-developer-senior.md +32 -0
- package/agents/devloom-developer.md +21 -55
- package/agents/devloom-documenter-flash.md +28 -0
- package/agents/devloom-documenter.md +17 -56
- package/agents/devloom-orchestrator.md +236 -356
- package/agents/devloom-planner-flash.md +34 -0
- package/agents/devloom-planner-senior.md +35 -0
- package/agents/devloom-planner.md +36 -0
- package/agents/devloom-qa-flash.md +32 -0
- package/agents/devloom-qa.md +21 -83
- package/agents/devloom-security-senior.md +35 -0
- package/agents/devloom-security.md +34 -0
- package/agents/devloom-verifier.md +33 -0
- package/agents/devloom-vision.md +113 -0
- package/agents/devloom-visual-critic.md +102 -0
- package/agents/devloom-visual-director.md +84 -0
- package/commands/devloom-agents.md +49 -0
- package/commands/devloom-auto.md +11 -0
- package/commands/devloom-context.md +82 -0
- package/commands/devloom-deepseek-mimo.md +11 -0
- package/commands/devloom-deepseek.md +11 -0
- package/commands/devloom-free.md +11 -0
- package/commands/devloom-go-economy.md +11 -0
- package/commands/devloom-go-flash.md +11 -0
- package/commands/devloom-go.md +11 -0
- package/commands/devloom-init.md +28 -50
- package/commands/devloom-loop-status.md +25 -0
- package/commands/devloom-loop.md +36 -0
- package/commands/devloom-mimo.md +11 -0
- package/commands/devloom-plan.md +13 -0
- package/commands/devloom-refresh.md +22 -0
- package/commands/devloom-resume.md +30 -49
- package/commands/devloom-save.md +69 -0
- package/commands/devloom-status.md +9 -18
- package/commands/devloom.md +99 -67
- package/dist/agents.d.ts +30 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +139 -0
- package/dist/agents.js.map +1 -0
- package/dist/bootstrap.d.ts +56 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +116 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/constraints.d.ts +5 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +45 -0
- package/dist/constraints.js.map +1 -0
- package/dist/context.d.ts +42 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +636 -0
- package/dist/context.js.map +1 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +418 -0
- package/dist/guard.js.map +1 -0
- package/dist/loop.d.ts +64 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +164 -0
- package/dist/loop.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +101 -1
- package/dist/plugin.js.map +1 -1
- package/dist/tui-agents.d.ts +61 -0
- package/dist/tui-agents.d.ts.map +1 -0
- package/dist/tui-agents.js +89 -0
- package/dist/tui-agents.js.map +1 -0
- package/dist/tui.d.ts +6 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +92 -0
- package/dist/tui.js.map +1 -0
- package/dist/worktree.d.ts +46 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +272 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +46 -6
- package/patterns/changelog-drafter.md +38 -0
- package/patterns/ci-sweeper.md +40 -0
- package/patterns/daily-triage.md +37 -0
- package/patterns/dependency-sweeper.md +40 -0
- package/patterns/design-audit.md +63 -0
- package/patterns/issue-triage.md +37 -0
- package/patterns/post-merge-cleanup.md +34 -0
- package/patterns/pr-babysitter.md +37 -0
- package/patterns/registry.yaml +52 -0
- package/postinstall.mjs +264 -64
- package/project/README.md +29 -0
- package/protocol/agent-contracts.md +27 -0
- package/protocol/artifact-system.md +53 -0
- package/protocol/model-routing.md +194 -0
- package/protocol/orchestrator-core.md +43 -0
- package/protocol/project-system.md +45 -0
- package/protocol/rules.md +18 -0
- package/protocol/verification-policy.md +48 -0
- package/scripts/briefing.mjs +192 -0
- package/scripts/loop-run.mjs +205 -0
- package/scripts/model-capabilities.mjs +194 -0
- package/scripts/plugin-cache.mjs +269 -0
- package/scripts/profile.mjs +755 -0
- package/scripts/visual-benchmark.mjs +149 -0
- package/scripts/worktree.mjs +445 -0
- package/skills/build/development.md +33 -0
- package/skills/build/live-docs.md +38 -0
- package/skills/build/simplify.md +50 -0
- package/skills/build/vision-analysis.md +98 -0
- package/skills/design/app-design.md +31 -0
- package/skills/design/design-grounding.md +30 -0
- package/skills/design/design-system.md +27 -0
- package/skills/design/game-design.md +35 -0
- package/skills/design/motion-design.md +34 -0
- package/skills/design/visual-direction.md +32 -0
- package/skills/design/web-design.md +33 -0
- package/skills/loop/changelog-drafter.md +32 -0
- package/skills/loop/ci-sweeper.md +24 -0
- package/skills/loop/constraints.md +21 -0
- package/skills/loop/dependency-sweeper.md +24 -0
- package/skills/loop/design-audit.md +67 -0
- package/skills/loop/issue-triage.md +27 -0
- package/skills/loop/post-merge-cleanup.md +24 -0
- package/skills/loop/pr-babysitter.md +26 -0
- package/skills/loop/triage.md +25 -0
- package/skills/loop/verifier.md +20 -0
- package/skills/meta/skill-discovery.md +23 -0
- package/skills/plan/planning.md +27 -0
- package/skills/plan/verification-planning.md +40 -0
- package/skills/review/security-review.md +63 -0
- package/skills/ship/documentation.md +11 -0
- package/skills/verify/app-verification.md +19 -0
- package/skills/verify/quality-assurance.md +17 -0
- package/skills/verify/visual-critique.md +30 -0
- package/skills/verify/visual-quality-gate.md +29 -0
- package/skills/verify/visual-regression.md +25 -0
- package/agents/devloom-analyst.md +0 -83
- 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
|