theslopmachine 0.3.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 (31) hide show
  1. package/MANUAL.md +63 -0
  2. package/README.md +23 -0
  3. package/RELEASE.md +81 -0
  4. package/assets/agents/developer.md +294 -0
  5. package/assets/agents/slopmachine.md +510 -0
  6. package/assets/skills/beads-operations/SKILL.md +75 -0
  7. package/assets/skills/clarification-gate/SKILL.md +51 -0
  8. package/assets/skills/developer-session-lifecycle/SKILL.md +75 -0
  9. package/assets/skills/final-evaluation-orchestration/SKILL.md +75 -0
  10. package/assets/skills/frontend-design/SKILL.md +41 -0
  11. package/assets/skills/get-overlays/SKILL.md +157 -0
  12. package/assets/skills/planning-gate/SKILL.md +68 -0
  13. package/assets/skills/submission-packaging/SKILL.md +268 -0
  14. package/assets/skills/verification-gates/SKILL.md +106 -0
  15. package/assets/slopmachine/backend-evaluation-prompt.md +275 -0
  16. package/assets/slopmachine/beads-init.js +428 -0
  17. package/assets/slopmachine/document-completeness.md +45 -0
  18. package/assets/slopmachine/engineering-results.md +59 -0
  19. package/assets/slopmachine/frontend-evaluation-prompt.md +304 -0
  20. package/assets/slopmachine/implementation-comparison.md +36 -0
  21. package/assets/slopmachine/quality-document.md +108 -0
  22. package/assets/slopmachine/templates/AGENTS.md +114 -0
  23. package/assets/slopmachine/utils/convert_ai_session.py +1837 -0
  24. package/assets/slopmachine/utils/strip_session_parent.py +66 -0
  25. package/bin/slopmachine.js +9 -0
  26. package/package.json +25 -0
  27. package/src/cli.js +32 -0
  28. package/src/constants.js +77 -0
  29. package/src/init.js +179 -0
  30. package/src/install.js +330 -0
  31. package/src/utils.js +162 -0
package/src/install.js ADDED
@@ -0,0 +1,330 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import {
5
+ BEADS_VERSION,
6
+ buildPaths,
7
+ MCP_ENTRIES,
8
+ OPCODE_VERSION,
9
+ PACKAGE_ROOT,
10
+ REQUIRED_SKILL_DIRS,
11
+ REQUIRED_SLOPMACHINE_FILES,
12
+ } from './constants.js'
13
+ import {
14
+ backupFile,
15
+ commandExists,
16
+ copyDirIfMissing,
17
+ copyFileIfMissing,
18
+ ensureDir,
19
+ log,
20
+ makeExecutableIfShellScript,
21
+ pathExists,
22
+ promptText,
23
+ promptYesNo,
24
+ readJsonIfExists,
25
+ runCommand,
26
+ warn,
27
+ writeJson,
28
+ } from './utils.js'
29
+
30
+ function assetsRoot() {
31
+ return path.join(PACKAGE_ROOT, 'assets')
32
+ }
33
+
34
+ async function getCommandVersion(command, args = ['--version']) {
35
+ const exists = await commandExists(command)
36
+ if (!exists) return null
37
+ const result = await runCommand(command, args)
38
+ if (result.code !== 0) return null
39
+ return (result.stdout || result.stderr).trim()
40
+ }
41
+
42
+ async function detectPackageManagers() {
43
+ return {
44
+ brew: await commandExists('brew'),
45
+ apt: await commandExists('apt-get'),
46
+ dnf: await commandExists('dnf'),
47
+ winget: await commandExists('winget'),
48
+ choco: await commandExists('choco'),
49
+ }
50
+ }
51
+
52
+ async function tryInstallCoreDependency(name) {
53
+ if (name === 'opencode') {
54
+ log(`Installing opencode-ai@${OPCODE_VERSION} globally via npm`)
55
+ return runCommand('npm', ['install', '-g', `opencode-ai@${OPCODE_VERSION}`], { stdio: 'inherit' })
56
+ }
57
+ if (name === 'bd') {
58
+ log(`Installing @beads/bd@${BEADS_VERSION} globally via npm`)
59
+ return runCommand('npm', ['install', '-g', `@beads/bd@${BEADS_VERSION}`], { stdio: 'inherit' })
60
+ }
61
+
62
+ const managers = await detectPackageManagers()
63
+ const installMap = {
64
+ git: {
65
+ brew: ['brew', ['install', 'git']],
66
+ apt: ['sudo', ['apt-get', 'install', '-y', 'git']],
67
+ dnf: ['sudo', ['dnf', 'install', '-y', 'git']],
68
+ winget: ['winget', ['install', '--id', 'Git.Git', '-e']],
69
+ choco: ['choco', ['install', 'git', '-y']],
70
+ },
71
+ python3: {
72
+ brew: ['brew', ['install', 'python@3.11']],
73
+ apt: ['sudo', ['apt-get', 'install', '-y', 'python3']],
74
+ dnf: ['sudo', ['dnf', 'install', '-y', 'python3']],
75
+ winget: ['winget', ['install', '--id', 'Python.Python.3.11', '-e']],
76
+ choco: ['choco', ['install', 'python', '--version=3.11.7', '-y']],
77
+ },
78
+ }
79
+
80
+ const platformKey = managers.brew
81
+ ? 'brew'
82
+ : managers.apt
83
+ ? 'apt'
84
+ : managers.dnf
85
+ ? 'dnf'
86
+ : managers.winget
87
+ ? 'winget'
88
+ : managers.choco
89
+ ? 'choco'
90
+ : null
91
+ const entry = installMap[name]?.[platformKey]
92
+ if (!entry) {
93
+ return { code: 1, stdout: '', stderr: `No automated install path for ${name} on this platform` }
94
+ }
95
+
96
+ if (platformKey === 'apt') {
97
+ const updateResult = await runCommand('sudo', ['apt-get', 'update'], { stdio: 'inherit' })
98
+ if (updateResult.code !== 0) return updateResult
99
+ }
100
+
101
+ return runCommand(entry[0], entry[1], { stdio: 'inherit' })
102
+ }
103
+
104
+ async function ensureDependency({ name, checkCommand, requiredVersion, installable }) {
105
+ const version = await getCommandVersion(checkCommand)
106
+ if (version) {
107
+ log(`${name} detected: ${version}`)
108
+ if (requiredVersion && !version.includes(requiredVersion)) {
109
+ warn(`${name} version differs from tested reference ${requiredVersion}`)
110
+ }
111
+ return
112
+ }
113
+
114
+ warn(`${name} is not installed or not available in PATH`)
115
+
116
+ if (!installable) {
117
+ return
118
+ }
119
+
120
+ const shouldInstall = await promptYesNo(`Attempt to install ${name} automatically?`, true)
121
+ if (!shouldInstall) {
122
+ warn(`Skipping ${name} installation. Please install it manually before using SlopMachine.`)
123
+ return
124
+ }
125
+
126
+ const result = await tryInstallCoreDependency(checkCommand)
127
+ if (result.code !== 0) {
128
+ warn(`Automatic installation for ${name} failed. Please install it manually.`)
129
+ }
130
+ }
131
+
132
+ async function checkDocker() {
133
+ const dockerVersion = await getCommandVersion('docker')
134
+ const composeVersion = await getCommandVersion('docker', ['compose', 'version'])
135
+ if (!dockerVersion || !composeVersion) {
136
+ warn('Docker and Docker Compose are required for SlopMachine workflows. Please install Docker Desktop or the Docker Engine + Compose plugin and start Docker.')
137
+ return
138
+ }
139
+ log(`docker detected: ${dockerVersion}`)
140
+ log(`docker compose detected: ${composeVersion}`)
141
+
142
+ const info = await runCommand('docker', ['info'])
143
+ if (info.code !== 0) {
144
+ warn('Docker is installed but does not appear to be running. Start Docker before using SlopMachine.')
145
+ }
146
+ }
147
+
148
+ async function checkInitShellSupport() {
149
+ const hasNode = await commandExists('node')
150
+ if (!hasNode) {
151
+ warn('`slopmachine init` requires Node.js to remain available on this machine after installation.')
152
+ }
153
+ }
154
+
155
+ async function installAgents(paths) {
156
+ const sourceAgents = path.join(assetsRoot(), 'agents')
157
+ await ensureDir(paths.opencodeAgentsDir)
158
+ const summary = { installed: [], skipped: [] }
159
+
160
+ for (const fileName of ['slopmachine.md', 'developer.md']) {
161
+ const result = await copyFileIfMissing(path.join(sourceAgents, fileName), path.join(paths.opencodeAgentsDir, fileName))
162
+ if (result.copied) {
163
+ log(`Installed agent: ${fileName}`)
164
+ summary.installed.push(fileName)
165
+ } else {
166
+ warn(`Skipped existing agent: ${fileName}`)
167
+ summary.skipped.push(fileName)
168
+ }
169
+ }
170
+
171
+ return summary
172
+ }
173
+
174
+ async function installSkills(paths) {
175
+ const sourceSkills = path.join(assetsRoot(), 'skills')
176
+ await ensureDir(paths.globalSkillsDir)
177
+ const summary = { installed: [], skipped: [] }
178
+
179
+ for (const dirName of REQUIRED_SKILL_DIRS) {
180
+ const result = await copyDirIfMissing(path.join(sourceSkills, dirName), path.join(paths.globalSkillsDir, dirName))
181
+ if (result.copied) {
182
+ log(`Installed skill: ${dirName}`)
183
+ summary.installed.push(dirName)
184
+ } else {
185
+ warn(`Skipped existing skill: ${dirName}`)
186
+ summary.skipped.push(dirName)
187
+ }
188
+ }
189
+
190
+ return summary
191
+ }
192
+
193
+ async function installSlopmachineAssets(paths) {
194
+ const source = path.join(assetsRoot(), 'slopmachine')
195
+ await ensureDir(paths.slopmachineDir)
196
+ const summary = { installed: [], skipped: [] }
197
+
198
+ for (const relativePath of REQUIRED_SLOPMACHINE_FILES) {
199
+ const result = await copyFileIfMissing(path.join(source, relativePath), path.join(paths.slopmachineDir, relativePath))
200
+ if (result.copied) {
201
+ await makeExecutableIfShellScript(path.join(paths.slopmachineDir, relativePath))
202
+ log(`Installed asset: ${relativePath}`)
203
+ summary.installed.push(relativePath)
204
+ } else {
205
+ warn(`Skipped existing asset: ${relativePath}`)
206
+ summary.skipped.push(relativePath)
207
+ }
208
+ }
209
+
210
+ return summary
211
+ }
212
+
213
+ async function mergeOpencodeConfig(paths, options) {
214
+ await ensureDir(paths.opencodeDir)
215
+ const backupPath = await backupFile(paths.opencodeConfigPath)
216
+ if (backupPath) {
217
+ log(`Backed up existing opencode.json to ${backupPath}`)
218
+ }
219
+
220
+ const existing = (await readJsonIfExists(paths.opencodeConfigPath)) || {
221
+ $schema: 'https://opencode.ai/config.json',
222
+ }
223
+
224
+ const plugins = Array.isArray(existing.plugin) ? [...existing.plugin] : []
225
+ if (!plugins.includes('oc-chatgpt-multi-auth')) {
226
+ plugins.push('oc-chatgpt-multi-auth')
227
+ }
228
+ existing.plugin = plugins
229
+
230
+ const mcp = typeof existing.mcp === 'object' && existing.mcp !== null ? { ...existing.mcp } : {}
231
+ mcp['chrome-devtools'] = MCP_ENTRIES['chrome-devtools']
232
+ mcp.context7 = {
233
+ ...MCP_ENTRIES.context7,
234
+ headers: {
235
+ CONTEXT7_API_KEY: options.context7ApiKey,
236
+ },
237
+ }
238
+ mcp.exa = {
239
+ ...MCP_ENTRIES.exa,
240
+ headers: {
241
+ EXA_API_KEY: options.exaApiKey,
242
+ },
243
+ }
244
+ mcp.shadcn = MCP_ENTRIES.shadcn
245
+ existing.mcp = mcp
246
+
247
+ await writeJson(paths.opencodeConfigPath, existing)
248
+ log(`Updated ${paths.opencodeConfigPath}`)
249
+ }
250
+
251
+ async function maybeInstallPluginBinary() {
252
+ if (process.env.SLOPMACHINE_PLUGIN_BOOTSTRAP === '0') {
253
+ return
254
+ }
255
+
256
+ if (process.env.SLOPMACHINE_PLUGIN_BOOTSTRAP === '1') {
257
+ const forced = await runCommand('npx', ['-y', 'oc-chatgpt-multi-auth@latest'], { stdio: 'inherit' })
258
+ if (forced.code !== 0) {
259
+ warn('Plugin bootstrap command did not complete successfully. The opencode.json plugin entry was still configured.')
260
+ }
261
+ return
262
+ }
263
+
264
+ const shouldInstall = await promptYesNo('Run `npx -y oc-chatgpt-multi-auth@latest` now?', false)
265
+ if (!shouldInstall) {
266
+ return
267
+ }
268
+ const result = await runCommand('npx', ['-y', 'oc-chatgpt-multi-auth@latest'], { stdio: 'inherit' })
269
+ if (result.code !== 0) {
270
+ warn('Plugin bootstrap command did not complete successfully. The opencode.json plugin entry was still configured.')
271
+ }
272
+ }
273
+
274
+ async function collectApiKeys() {
275
+ const forcedContext7 = process.env.SLOPMACHINE_CONTEXT7_API_KEY
276
+ const forcedExa = process.env.SLOPMACHINE_EXA_API_KEY
277
+ const nonInteractive = process.env.SLOPMACHINE_NONINTERACTIVE === '1'
278
+
279
+ if (forcedContext7 !== undefined || forcedExa !== undefined || nonInteractive) {
280
+ return {
281
+ context7ApiKey: forcedContext7 || '',
282
+ exaApiKey: forcedExa || '',
283
+ }
284
+ }
285
+
286
+ console.log('Context7 API key: https://context7.com')
287
+ const context7ApiKey = await promptText('Paste Context7 API key or leave blank to skip', '')
288
+
289
+ console.log('Exa API key: https://exa.ai')
290
+ const exaApiKey = await promptText('Paste Exa API key or leave blank to skip', '')
291
+
292
+ return { context7ApiKey, exaApiKey }
293
+ }
294
+
295
+ export async function runInstall() {
296
+ const paths = buildPaths()
297
+ log(`Installing SlopMachine into ${paths.home}`)
298
+
299
+ await ensureDependency({ name: 'git', checkCommand: 'git', installable: true })
300
+ await ensureDependency({ name: 'python3', checkCommand: 'python3', installable: true })
301
+ await ensureDependency({ name: 'opencode', checkCommand: 'opencode', requiredVersion: OPCODE_VERSION, installable: true })
302
+ await ensureDependency({ name: 'Beads (bd)', checkCommand: 'bd', requiredVersion: BEADS_VERSION, installable: true })
303
+ await checkDocker()
304
+ await checkInitShellSupport()
305
+
306
+ const agentSummary = await installAgents(paths)
307
+ const skillSummary = await installSkills(paths)
308
+ const assetSummary = await installSlopmachineAssets(paths)
309
+
310
+ await maybeInstallPluginBinary()
311
+ const keys = await collectApiKeys()
312
+ await mergeOpencodeConfig(paths, keys)
313
+
314
+ log('Installation phase completed.')
315
+ console.log('\nSlopMachine install summary')
316
+ console.log(`- Agents installed: ${agentSummary.installed.length}`)
317
+ console.log(`- Agents skipped: ${agentSummary.skipped.length}`)
318
+ console.log(`- Skills installed: ${skillSummary.installed.length}`)
319
+ console.log(`- Skills skipped: ${skillSummary.skipped.length}`)
320
+ console.log(`- SlopMachine assets installed: ${assetSummary.installed.length}`)
321
+ console.log(`- SlopMachine assets skipped: ${assetSummary.skipped.length}`)
322
+ console.log(`- Agents directory: ${paths.opencodeAgentsDir}`)
323
+ console.log(`- Skills directory: ${paths.globalSkillsDir}`)
324
+ console.log(`- SlopMachine home: ${paths.slopmachineDir}`)
325
+ console.log(`- OpenCode config: ${paths.opencodeConfigPath}`)
326
+ console.log('\nNext steps')
327
+ console.log('- Review any warnings above for skipped files or missing external dependencies.')
328
+ console.log('- If Docker was reported as stopped, start Docker before using SlopMachine on a real project.')
329
+ console.log('- Run `slopmachine init` inside a project directory to bootstrap a new SlopMachine workspace.')
330
+ }
package/src/utils.js ADDED
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs/promises'
2
+ import { constants as fsConstants } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { spawn } from 'node:child_process'
5
+ import readline from 'node:readline/promises'
6
+ import { stdin as input, stdout as output } from 'node:process'
7
+
8
+ export function log(message) {
9
+ console.log(`[slopmachine] ${message}`)
10
+ }
11
+
12
+ export function warn(message) {
13
+ console.warn(`[slopmachine] WARN: ${message}`)
14
+ }
15
+
16
+ export async function pathExists(targetPath) {
17
+ try {
18
+ await fs.access(targetPath, fsConstants.F_OK)
19
+ return true
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ export async function ensureDir(targetPath) {
26
+ await fs.mkdir(targetPath, { recursive: true })
27
+ }
28
+
29
+ export async function copyFileIfMissing(sourcePath, targetPath) {
30
+ if (await pathExists(targetPath)) {
31
+ return { copied: false, skipped: true }
32
+ }
33
+ await ensureDir(path.dirname(targetPath))
34
+ await fs.copyFile(sourcePath, targetPath)
35
+ return { copied: true, skipped: false }
36
+ }
37
+
38
+ export async function copyDirIfMissing(sourcePath, targetPath) {
39
+ if (await pathExists(targetPath)) {
40
+ return { copied: false, skipped: true }
41
+ }
42
+ await ensureDir(path.dirname(targetPath))
43
+ await fs.cp(sourcePath, targetPath, { recursive: true })
44
+ return { copied: true, skipped: false }
45
+ }
46
+
47
+ export async function makeExecutableIfShellScript(targetPath) {
48
+ if (targetPath.endsWith('.sh')) {
49
+ await fs.chmod(targetPath, 0o755)
50
+ }
51
+ }
52
+
53
+ export async function readJsonIfExists(filePath) {
54
+ if (!(await pathExists(filePath))) {
55
+ return null
56
+ }
57
+ const content = await fs.readFile(filePath, 'utf8')
58
+ return JSON.parse(content)
59
+ }
60
+
61
+ export async function writeJson(filePath, value) {
62
+ await ensureDir(path.dirname(filePath))
63
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}
64
+ `, 'utf8')
65
+ }
66
+
67
+ export async function backupFile(filePath) {
68
+ if (!(await pathExists(filePath))) {
69
+ return null
70
+ }
71
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
72
+ const backupPath = `${filePath}.backup-${stamp}`
73
+ await fs.copyFile(filePath, backupPath)
74
+ return backupPath
75
+ }
76
+
77
+ export async function runCommand(command, args, options = {}) {
78
+ return new Promise((resolve, reject) => {
79
+ const child = spawn(command, args, {
80
+ stdio: options.stdio || 'pipe',
81
+ cwd: options.cwd,
82
+ shell: options.shell || false,
83
+ env: options.env || process.env,
84
+ })
85
+
86
+ let stdoutText = ''
87
+ let stderrText = ''
88
+
89
+ if (child.stdout) {
90
+ child.stdout.on('data', (chunk) => {
91
+ stdoutText += chunk.toString()
92
+ })
93
+ }
94
+
95
+ if (child.stderr) {
96
+ child.stderr.on('data', (chunk) => {
97
+ stderrText += chunk.toString()
98
+ })
99
+ }
100
+
101
+ child.on('error', reject)
102
+ child.on('close', (code) => {
103
+ resolve({ code: code ?? 1, stdout: stdoutText, stderr: stderrText })
104
+ })
105
+ })
106
+ }
107
+
108
+ export async function commandExists(command) {
109
+ const checker = process.platform === 'win32' ? 'where' : 'which'
110
+ const result = await runCommand(checker, [command])
111
+ return result.code === 0
112
+ }
113
+
114
+ export async function findBashExecutable() {
115
+ if (await commandExists('bash')) {
116
+ return 'bash'
117
+ }
118
+
119
+ if (process.platform !== 'win32') {
120
+ return null
121
+ }
122
+
123
+ const candidates = [
124
+ path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'bash.exe'),
125
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'bash.exe'),
126
+ path.join(process.env.ProgramFiles || '', 'Git', 'usr', 'bin', 'bash.exe'),
127
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'usr', 'bin', 'bash.exe'),
128
+ ].filter(Boolean)
129
+
130
+ for (const candidate of candidates) {
131
+ if (candidate && await pathExists(candidate)) {
132
+ return candidate
133
+ }
134
+ }
135
+
136
+ return null
137
+ }
138
+
139
+ export async function promptText(question, defaultValue = '') {
140
+ const rl = readline.createInterface({ input, output })
141
+ try {
142
+ const suffix = defaultValue ? ` [${defaultValue}]` : ''
143
+ const answer = await rl.question(`${question}${suffix}: `)
144
+ return answer.trim() || defaultValue
145
+ } finally {
146
+ rl.close()
147
+ }
148
+ }
149
+
150
+ export async function promptYesNo(question, defaultYes = true) {
151
+ const rl = readline.createInterface({ input, output })
152
+ try {
153
+ const suffix = defaultYes ? ' [Y/n]' : ' [y/N]'
154
+ const answer = (await rl.question(`${question}${suffix}: `)).trim().toLowerCase()
155
+ if (!answer) {
156
+ return defaultYes
157
+ }
158
+ return answer === 'y' || answer === 'yes'
159
+ } finally {
160
+ rl.close()
161
+ }
162
+ }