fabis-ralph-loop 1.5.3 → 1.7.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/README.md +1 -0
- package/dist/gitignore.mjs +12 -2
- package/dist/gitignore.mjs.map +1 -1
- package/dist/index.d.mts +6 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/loader.mjs +2 -1
- package/dist/loader.mjs.map +1 -1
- package/dist/templates/Dockerfile.ejs +6 -0
- package/dist/templates/entrypoint.ts.ejs +29 -0
- package/dist/templates/ralph-prompt.md.ejs +27 -7
- package/dist/uac-templates/skills/update-fabis-ralph-loop-config/SKILL.md +24 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ export default defineConfig({
|
|
|
80
80
|
name: 'my-project-ralph',
|
|
81
81
|
playwright: true, // auto-configures Playwright CLI + headless Chromium (or 'mcp' for MCP mode)
|
|
82
82
|
sslCerts: '.certs', // trust custom SSL certs in container (for local HTTPS dev servers)
|
|
83
|
+
blockedDomains: ['figma.com', 'linear.app'], // block MCP server domains not needed in autonomous mode
|
|
83
84
|
systemPackages: ['ripgrep'],
|
|
84
85
|
env: { NODE_ENV: 'development' },
|
|
85
86
|
hooks: {
|
package/dist/gitignore.mjs
CHANGED
|
@@ -58,6 +58,7 @@ async function generateDockerfile(config) {
|
|
|
58
58
|
installNode: !isNodeBaseImage(config.container.baseImage),
|
|
59
59
|
playwright: config.container.playwright,
|
|
60
60
|
sslCerts: !!config.container.sslCerts,
|
|
61
|
+
blockedDomains: config.container.blockedDomains,
|
|
61
62
|
hooks: config.container.hooks,
|
|
62
63
|
user,
|
|
63
64
|
createUser: user === "sandbox",
|
|
@@ -104,6 +105,7 @@ async function generateEntrypoint(config) {
|
|
|
104
105
|
return renderTemplate("entrypoint.ts.ejs", {
|
|
105
106
|
generatedHeader: GENERATED_HEADER.replace(/^# /gm, "// "),
|
|
106
107
|
agent: config.defaults.agent,
|
|
108
|
+
blockedDomains: config.container.blockedDomains,
|
|
107
109
|
shadowVolumes: config.container.shadowVolumes,
|
|
108
110
|
entrypointSetup: config.container.hooks.entrypointSetup,
|
|
109
111
|
sslCerts: !!config.container.sslCerts,
|
|
@@ -113,6 +115,13 @@ async function generateEntrypoint(config) {
|
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/generators/normalize.ts
|
|
120
|
+
function normalizeOpenAppSkills(value) {
|
|
121
|
+
if (Array.isArray(value)) return value;
|
|
122
|
+
return value ? [value] : [];
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
//#endregion
|
|
117
126
|
//#region src/generators/prompt.ts
|
|
118
127
|
async function generatePrompt(config) {
|
|
@@ -122,8 +131,9 @@ async function generatePrompt(config) {
|
|
|
122
131
|
projectDescription: config.project.description,
|
|
123
132
|
projectContext: config.project.context,
|
|
124
133
|
backpressureCommands: config.project.backpressureCommands,
|
|
125
|
-
|
|
134
|
+
openAppSkills: normalizeOpenAppSkills(config.project.openAppSkill),
|
|
126
135
|
playwright: config.container.playwright,
|
|
136
|
+
blockedDomains: config.container.blockedDomains,
|
|
127
137
|
completionSignal: config.defaults.completionSignal
|
|
128
138
|
});
|
|
129
139
|
}
|
|
@@ -136,7 +146,7 @@ function buildLevel1Variables(config) {
|
|
|
136
146
|
backpressureCommands: config.project.backpressureCommands,
|
|
137
147
|
projectName: config.project.name,
|
|
138
148
|
projectContext: config.project.context,
|
|
139
|
-
|
|
149
|
+
openAppSkills: normalizeOpenAppSkills(config.project.openAppSkill),
|
|
140
150
|
playwright: config.container.playwright,
|
|
141
151
|
config
|
|
142
152
|
};
|
package/dist/gitignore.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitignore.mjs","names":[],"sources":["../src/utils/template.ts","../src/utils/version.ts","../src/generators/dockerfile.ts","../src/generators/compose.ts","../src/generators/entrypoint.ts","../src/generators/prompt.ts","../src/generators/skills.ts","../src/generators/index.ts","../src/utils/gitignore.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport ejs from 'ejs'\n\n/**\n * Resolve a bundled asset directory (templates, static, uac-templates).\n * In dist (flat layout): assets are siblings of the compiled files.\n * In src (nested layout via tsx): assets are siblings of the parent dir.\n */\nexport function resolveAssetDir(assetName: string, metaUrl: string): string {\n const dir = dirname(fileURLToPath(metaUrl))\n const sibling = join(dir, assetName)\n if (existsSync(sibling)) return sibling\n return join(dir, '..', assetName)\n}\n\nconst TEMPLATES_DIR = resolveAssetDir('templates', import.meta.url)\n\nexport async function renderTemplate(\n templateName: string,\n data: Record<string, unknown>,\n): Promise<string> {\n const templatePath = join(TEMPLATES_DIR, templateName)\n const template = await readFile(templatePath, 'utf8')\n return ejs.render(template, data, { async: false, escape: (s: string) => s }) as string\n}\n\nexport const GENERATED_HEADER = `# Generated by fabis-ralph-loop — DO NOT EDIT MANUALLY\n# Regenerate with: npx fabis-ralph-loop generate\n`\n","import { createRequire } from 'node:module'\n\nexport function getPackageVersion(): string {\n try {\n const require = createRequire(import.meta.url)\n const pkg = require('../../package.json') as { version: string }\n return pkg.version\n } catch {\n return 'latest'\n }\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport { getPackageVersion } from '../utils/version.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Detect whether the base image already includes Node.js.\n */\nfunction isNodeBaseImage(baseImage: string): boolean {\n return /^node[:/]/i.test(baseImage)\n}\n\nexport async function generateDockerfile(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('Dockerfile.ejs', {\n generatedHeader: GENERATED_HEADER,\n baseImage: config.container.baseImage,\n systemPackages: config.container.systemPackages,\n installNode: !isNodeBaseImage(config.container.baseImage),\n playwright: config.container.playwright,\n sslCerts: !!config.container.sslCerts,\n hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { isAbsolute } from 'node:path'\nimport { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Resolve a host path for use in docker-compose volumes.\n * Relative paths get `../` prepended since compose runs from `.ralph-container/`.\n */\nfunction resolveHostPath(hostPath: string): string {\n return isAbsolute(hostPath) ? hostPath : `../${hostPath}`\n}\n\nexport async function generateCompose(config: ResolvedConfig): Promise<string> {\n const homeDir = `/home/${config.container.user}`\n\n // Ensure .claude config is always persisted with the correct home dir\n const persistVolumes: Record<string, string> = {\n 'ralph-claude-config': `${homeDir}/.claude`,\n ...Object.fromEntries(\n Object.entries(config.container.persistVolumes).map(([name, path]) => [\n name,\n path.replace('/home/sandbox', homeDir),\n ]),\n ),\n }\n\n const sslCertsVolume = config.container.sslCerts\n ? `${resolveHostPath(config.container.sslCerts)}:/tmp/ssl-certs:ro`\n : undefined\n\n return renderTemplate('docker-compose.yml.ejs', {\n generatedHeader: GENERATED_HEADER,\n containerName: config.container.name,\n shmSize: config.container.shmSize,\n networkMode: config.container.networkMode,\n capabilities: config.container.capabilities,\n shadowVolumes: config.container.shadowVolumes,\n persistVolumes,\n extraVolumes: config.container.volumes,\n sslCertsVolume,\n env: config.container.env,\n homeDir,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateEntrypoint(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('entrypoint.ts.ejs', {\n generatedHeader: GENERATED_HEADER.replace(/^# /gm, '// '),\n agent: config.defaults.agent,\n shadowVolumes: config.container.shadowVolumes,\n entrypointSetup: config.container.hooks.entrypointSetup,\n sslCerts: !!config.container.sslCerts,\n playwright: config.container.playwright,\n user,\n homeDir: `/home/${user}`,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generatePrompt(config: ResolvedConfig): Promise<string> {\n return renderTemplate('ralph-prompt.md.ejs', {\n generatedHeader: GENERATED_HEADER,\n projectName: config.project.name,\n projectDescription: config.project.description,\n projectContext: config.project.context,\n backpressureCommands: config.project.backpressureCommands,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n completionSignal: config.defaults.completionSignal,\n })\n}\n","import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { InMemoryTemplate } from 'universal-ai-config'\nimport type { ResolvedConfig } from '../config/schema.js'\nimport { resolveAssetDir } from '../utils/template.js'\n\nconst UAC_TEMPLATES_DIR = resolveAssetDir('uac-templates', import.meta.url)\n\nfunction buildLevel1Variables(config: ResolvedConfig): Record<string, unknown> {\n return {\n backpressureCommands: config.project.backpressureCommands,\n projectName: config.project.name,\n projectContext: config.project.context,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n config,\n }\n}\n\nasync function discoverSkills(): Promise<string[]> {\n const skillsDir = join(UAC_TEMPLATES_DIR, 'skills')\n const entries = await readdir(skillsDir, { withFileTypes: true })\n return entries.filter((e) => e.isDirectory()).map((e) => e.name)\n}\n\nexport async function generateSkills(config: ResolvedConfig, projectRoot: string): Promise<void> {\n if (config.output.mode === 'direct') {\n await generateDirect(config, projectRoot)\n } else {\n await generateUac(config, projectRoot)\n }\n}\n\nasync function generateDirect(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n\n // Render Level 1 EJS into in-memory templates for UAC's second pass\n const templates: InMemoryTemplate[] = await Promise.all(\n skills.map(async (skill) => {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n const rendered = ejs.render(template, variables, { escape: (s: string) => s }) as string\n return { name: skill, type: 'skills' as const, content: rendered }\n }),\n )\n\n // Use UAC's generate() API for the second pass (handles Level 2 EJS + frontmatter mapping)\n const files = await generate({\n root: projectRoot,\n targets: ['claude'],\n types: ['skills'],\n templates,\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\n}\n\nasync function generateUac(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n let count = 0\n\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n // Render Level 1 EJS — Level 2 <%% %> becomes <% %> in output\n const rendered = ejs.render(template, variables, { escape: (s: string) => s }) as string\n\n const outDir = join(projectRoot, config.output.uacTemplatesDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n count++\n }\n\n consola.info(`Generated ${count} skill template(s) to ${config.output.uacTemplatesDir}/skills/`)\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\nimport { generateDockerfile } from './dockerfile.js'\nimport { generateCompose } from './compose.js'\nimport { generateEntrypoint } from './entrypoint.js'\nimport { generatePrompt } from './prompt.js'\nimport { generateSkills } from './skills.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface GenerateOptions {\n dryRun?: boolean\n only?: 'container' | 'prompt' | 'skills'\n}\n\ninterface GeneratedFile {\n path: string\n content: string\n}\n\nexport async function generateAll(\n config: ResolvedConfig,\n projectRoot: string,\n options: GenerateOptions = {},\n): Promise<GeneratedFile[]> {\n const files: GeneratedFile[] = []\n\n if (!options.only || options.only === 'container') {\n const containerDir = join(projectRoot, '.ralph-container')\n await mkdir(containerDir, { recursive: true })\n\n const dockerfile = await generateDockerfile(config)\n files.push({ path: join('.ralph-container', 'Dockerfile'), content: dockerfile })\n\n const entrypoint = await generateEntrypoint(config)\n files.push({ path: join('.ralph-container', 'entrypoint.ts'), content: entrypoint })\n\n const compose = await generateCompose(config)\n files.push({ path: join('.ralph-container', 'docker-compose.yml'), content: compose })\n }\n\n if (!options.only || options.only === 'prompt') {\n const prompt = await generatePrompt(config)\n files.push({ path: join('.ralph-container', 'ralph-prompt.md'), content: prompt })\n }\n\n if (options.dryRun) {\n for (const file of files) {\n consola.info(`[dry-run] Would write: ${file.path}`)\n }\n } else {\n for (const file of files) {\n const fullPath = join(projectRoot, file.path)\n await mkdir(join(fullPath, '..'), { recursive: true })\n await writeFile(fullPath, file.content, 'utf8')\n consola.success(`Written: ${file.path}`)\n }\n }\n\n if (!options.only || options.only === 'skills') {\n if (options.dryRun) {\n consola.info('[dry-run] Would generate skills')\n } else {\n await generateSkills(config, projectRoot)\n }\n }\n\n return files\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst MARKER_START = '# >>> fabis-ralph-loop >>>'\nconst MARKER_END = '# <<< fabis-ralph-loop <<<'\n\nconst GITIGNORE_BLOCK = `${MARKER_START}\n/fabis-ralph-loop.overrides.*\n${MARKER_END}`\n\n/**\n * Idempotently add fabis-ralph-loop gitignore entries to .gitignore.\n * Uses marker comments to detect existing blocks and avoid duplication.\n */\nexport async function ensureGitignoreBlock(cwd: string = process.cwd()): Promise<void> {\n const gitignorePath = join(cwd, '.gitignore')\n\n let content = ''\n if (existsSync(gitignorePath)) {\n content = await readFile(gitignorePath, 'utf8')\n }\n\n if (content.includes(MARKER_START)) return\n\n const newContent = content.trimEnd()\n ? `${content.trimEnd()}\\n\\n${GITIGNORE_BLOCK}\\n`\n : `${GITIGNORE_BLOCK}\\n`\n\n await writeFile(gitignorePath, newContent, 'utf8')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gitignore.mjs","names":[],"sources":["../src/utils/template.ts","../src/utils/version.ts","../src/generators/dockerfile.ts","../src/generators/compose.ts","../src/generators/entrypoint.ts","../src/generators/normalize.ts","../src/generators/prompt.ts","../src/generators/skills.ts","../src/generators/index.ts","../src/utils/gitignore.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport ejs from 'ejs'\n\n/**\n * Resolve a bundled asset directory (templates, static, uac-templates).\n * In dist (flat layout): assets are siblings of the compiled files.\n * In src (nested layout via tsx): assets are siblings of the parent dir.\n */\nexport function resolveAssetDir(assetName: string, metaUrl: string): string {\n const dir = dirname(fileURLToPath(metaUrl))\n const sibling = join(dir, assetName)\n if (existsSync(sibling)) return sibling\n return join(dir, '..', assetName)\n}\n\nconst TEMPLATES_DIR = resolveAssetDir('templates', import.meta.url)\n\nexport async function renderTemplate(\n templateName: string,\n data: Record<string, unknown>,\n): Promise<string> {\n const templatePath = join(TEMPLATES_DIR, templateName)\n const template = await readFile(templatePath, 'utf8')\n return ejs.render(template, data, { async: false, escape: (s: string) => s }) as string\n}\n\nexport const GENERATED_HEADER = `# Generated by fabis-ralph-loop — DO NOT EDIT MANUALLY\n# Regenerate with: npx fabis-ralph-loop generate\n`\n","import { createRequire } from 'node:module'\n\nexport function getPackageVersion(): string {\n try {\n const require = createRequire(import.meta.url)\n const pkg = require('../../package.json') as { version: string }\n return pkg.version\n } catch {\n return 'latest'\n }\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport { getPackageVersion } from '../utils/version.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Detect whether the base image already includes Node.js.\n */\nfunction isNodeBaseImage(baseImage: string): boolean {\n return /^node[:/]/i.test(baseImage)\n}\n\nexport async function generateDockerfile(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('Dockerfile.ejs', {\n generatedHeader: GENERATED_HEADER,\n baseImage: config.container.baseImage,\n systemPackages: config.container.systemPackages,\n installNode: !isNodeBaseImage(config.container.baseImage),\n playwright: config.container.playwright,\n sslCerts: !!config.container.sslCerts,\n blockedDomains: config.container.blockedDomains,\n hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { isAbsolute } from 'node:path'\nimport { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Resolve a host path for use in docker-compose volumes.\n * Relative paths get `../` prepended since compose runs from `.ralph-container/`.\n */\nfunction resolveHostPath(hostPath: string): string {\n return isAbsolute(hostPath) ? hostPath : `../${hostPath}`\n}\n\nexport async function generateCompose(config: ResolvedConfig): Promise<string> {\n const homeDir = `/home/${config.container.user}`\n\n // Ensure .claude config is always persisted with the correct home dir\n const persistVolumes: Record<string, string> = {\n 'ralph-claude-config': `${homeDir}/.claude`,\n ...Object.fromEntries(\n Object.entries(config.container.persistVolumes).map(([name, path]) => [\n name,\n path.replace('/home/sandbox', homeDir),\n ]),\n ),\n }\n\n const sslCertsVolume = config.container.sslCerts\n ? `${resolveHostPath(config.container.sslCerts)}:/tmp/ssl-certs:ro`\n : undefined\n\n return renderTemplate('docker-compose.yml.ejs', {\n generatedHeader: GENERATED_HEADER,\n containerName: config.container.name,\n shmSize: config.container.shmSize,\n networkMode: config.container.networkMode,\n capabilities: config.container.capabilities,\n shadowVolumes: config.container.shadowVolumes,\n persistVolumes,\n extraVolumes: config.container.volumes,\n sslCertsVolume,\n env: config.container.env,\n homeDir,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateEntrypoint(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('entrypoint.ts.ejs', {\n generatedHeader: GENERATED_HEADER.replace(/^# /gm, '// '),\n agent: config.defaults.agent,\n blockedDomains: config.container.blockedDomains,\n shadowVolumes: config.container.shadowVolumes,\n entrypointSetup: config.container.hooks.entrypointSetup,\n sslCerts: !!config.container.sslCerts,\n playwright: config.container.playwright,\n user,\n homeDir: `/home/${user}`,\n })\n}\n","export function normalizeOpenAppSkills(value: string | string[]): string[] {\n if (Array.isArray(value)) return value\n return value ? [value] : []\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\nimport { normalizeOpenAppSkills } from './normalize.js'\n\nexport async function generatePrompt(config: ResolvedConfig): Promise<string> {\n return renderTemplate('ralph-prompt.md.ejs', {\n generatedHeader: GENERATED_HEADER,\n projectName: config.project.name,\n projectDescription: config.project.description,\n projectContext: config.project.context,\n backpressureCommands: config.project.backpressureCommands,\n openAppSkills: normalizeOpenAppSkills(config.project.openAppSkill),\n playwright: config.container.playwright,\n blockedDomains: config.container.blockedDomains,\n completionSignal: config.defaults.completionSignal,\n })\n}\n","import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { InMemoryTemplate } from 'universal-ai-config'\nimport type { ResolvedConfig } from '../config/schema.js'\nimport { resolveAssetDir } from '../utils/template.js'\nimport { normalizeOpenAppSkills } from './normalize.js'\n\nconst UAC_TEMPLATES_DIR = resolveAssetDir('uac-templates', import.meta.url)\n\nfunction buildLevel1Variables(config: ResolvedConfig): Record<string, unknown> {\n return {\n backpressureCommands: config.project.backpressureCommands,\n projectName: config.project.name,\n projectContext: config.project.context,\n openAppSkills: normalizeOpenAppSkills(config.project.openAppSkill),\n playwright: config.container.playwright,\n config,\n }\n}\n\nasync function discoverSkills(): Promise<string[]> {\n const skillsDir = join(UAC_TEMPLATES_DIR, 'skills')\n const entries = await readdir(skillsDir, { withFileTypes: true })\n return entries.filter((e) => e.isDirectory()).map((e) => e.name)\n}\n\nexport async function generateSkills(config: ResolvedConfig, projectRoot: string): Promise<void> {\n if (config.output.mode === 'direct') {\n await generateDirect(config, projectRoot)\n } else {\n await generateUac(config, projectRoot)\n }\n}\n\nasync function generateDirect(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n\n // Render Level 1 EJS into in-memory templates for UAC's second pass\n const templates: InMemoryTemplate[] = await Promise.all(\n skills.map(async (skill) => {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n const rendered = ejs.render(template, variables, { escape: (s: string) => s }) as string\n return { name: skill, type: 'skills' as const, content: rendered }\n }),\n )\n\n // Use UAC's generate() API for the second pass (handles Level 2 EJS + frontmatter mapping)\n const files = await generate({\n root: projectRoot,\n targets: ['claude'],\n types: ['skills'],\n templates,\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\n}\n\nasync function generateUac(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n let count = 0\n\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n // Render Level 1 EJS — Level 2 <%% %> becomes <% %> in output\n const rendered = ejs.render(template, variables, { escape: (s: string) => s }) as string\n\n const outDir = join(projectRoot, config.output.uacTemplatesDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n count++\n }\n\n consola.info(`Generated ${count} skill template(s) to ${config.output.uacTemplatesDir}/skills/`)\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\nimport { generateDockerfile } from './dockerfile.js'\nimport { generateCompose } from './compose.js'\nimport { generateEntrypoint } from './entrypoint.js'\nimport { generatePrompt } from './prompt.js'\nimport { generateSkills } from './skills.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface GenerateOptions {\n dryRun?: boolean\n only?: 'container' | 'prompt' | 'skills'\n}\n\ninterface GeneratedFile {\n path: string\n content: string\n}\n\nexport async function generateAll(\n config: ResolvedConfig,\n projectRoot: string,\n options: GenerateOptions = {},\n): Promise<GeneratedFile[]> {\n const files: GeneratedFile[] = []\n\n if (!options.only || options.only === 'container') {\n const containerDir = join(projectRoot, '.ralph-container')\n await mkdir(containerDir, { recursive: true })\n\n const dockerfile = await generateDockerfile(config)\n files.push({ path: join('.ralph-container', 'Dockerfile'), content: dockerfile })\n\n const entrypoint = await generateEntrypoint(config)\n files.push({ path: join('.ralph-container', 'entrypoint.ts'), content: entrypoint })\n\n const compose = await generateCompose(config)\n files.push({ path: join('.ralph-container', 'docker-compose.yml'), content: compose })\n }\n\n if (!options.only || options.only === 'prompt') {\n const prompt = await generatePrompt(config)\n files.push({ path: join('.ralph-container', 'ralph-prompt.md'), content: prompt })\n }\n\n if (options.dryRun) {\n for (const file of files) {\n consola.info(`[dry-run] Would write: ${file.path}`)\n }\n } else {\n for (const file of files) {\n const fullPath = join(projectRoot, file.path)\n await mkdir(join(fullPath, '..'), { recursive: true })\n await writeFile(fullPath, file.content, 'utf8')\n consola.success(`Written: ${file.path}`)\n }\n }\n\n if (!options.only || options.only === 'skills') {\n if (options.dryRun) {\n consola.info('[dry-run] Would generate skills')\n } else {\n await generateSkills(config, projectRoot)\n }\n }\n\n return files\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst MARKER_START = '# >>> fabis-ralph-loop >>>'\nconst MARKER_END = '# <<< fabis-ralph-loop <<<'\n\nconst GITIGNORE_BLOCK = `${MARKER_START}\n/fabis-ralph-loop.overrides.*\n${MARKER_END}`\n\n/**\n * Idempotently add fabis-ralph-loop gitignore entries to .gitignore.\n * Uses marker comments to detect existing blocks and avoid duplication.\n */\nexport async function ensureGitignoreBlock(cwd: string = process.cwd()): Promise<void> {\n const gitignorePath = join(cwd, '.gitignore')\n\n let content = ''\n if (existsSync(gitignorePath)) {\n content = await readFile(gitignorePath, 'utf8')\n }\n\n if (content.includes(MARKER_START)) return\n\n const newContent = content.trimEnd()\n ? `${content.trimEnd()}\\n\\n${GITIGNORE_BLOCK}\\n`\n : `${GITIGNORE_BLOCK}\\n`\n\n await writeFile(gitignorePath, newContent, 'utf8')\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -17,6 +17,7 @@ declare const ralphLoopConfigSchema: z.ZodObject<{
|
|
|
17
17
|
}>]>>, z.ZodTransform<false | "cli" | "mcp", boolean | "cli" | "mcp">>;
|
|
18
18
|
sslCerts: z.ZodOptional<z.ZodString>;
|
|
19
19
|
networkMode: z.ZodDefault<z.ZodString>;
|
|
20
|
+
blockedDomains: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
21
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
21
22
|
shmSize: z.ZodDefault<z.ZodString>;
|
|
22
23
|
capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -47,7 +48,7 @@ declare const ralphLoopConfigSchema: z.ZodObject<{
|
|
|
47
48
|
name: z.ZodString;
|
|
48
49
|
command: z.ZodString;
|
|
49
50
|
}, z.core.$strip>>>;
|
|
50
|
-
openAppSkill: z.ZodDefault<z.ZodString
|
|
51
|
+
openAppSkill: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
51
52
|
}, z.core.$strip>;
|
|
52
53
|
output: z.ZodPrefault<z.ZodObject<{
|
|
53
54
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
@@ -108,7 +109,7 @@ declare function defineConfig(config: RalphLoopConfig): {
|
|
|
108
109
|
name: string;
|
|
109
110
|
command: string;
|
|
110
111
|
}[] | undefined;
|
|
111
|
-
openAppSkill?: string | undefined;
|
|
112
|
+
openAppSkill?: string | string[] | undefined;
|
|
112
113
|
};
|
|
113
114
|
container?: {
|
|
114
115
|
name: string;
|
|
@@ -118,6 +119,7 @@ declare function defineConfig(config: RalphLoopConfig): {
|
|
|
118
119
|
playwright?: boolean | "cli" | "mcp" | undefined;
|
|
119
120
|
sslCerts?: string | undefined;
|
|
120
121
|
networkMode?: string | undefined;
|
|
122
|
+
blockedDomains?: string[] | undefined;
|
|
121
123
|
env?: Record<string, string> | undefined;
|
|
122
124
|
shmSize?: string | undefined;
|
|
123
125
|
capabilities?: string[] | undefined;
|
|
@@ -158,7 +160,7 @@ declare function defineOverridesConfig(config: DeepPartial<RalphLoopConfig>): {
|
|
|
158
160
|
name?: string | undefined;
|
|
159
161
|
command?: string | undefined;
|
|
160
162
|
} | undefined)[] | undefined;
|
|
161
|
-
openAppSkill?: string | undefined;
|
|
163
|
+
openAppSkill?: string | (string | undefined)[] | undefined;
|
|
162
164
|
} | undefined;
|
|
163
165
|
container?: {
|
|
164
166
|
name?: string | undefined;
|
|
@@ -168,6 +170,7 @@ declare function defineOverridesConfig(config: DeepPartial<RalphLoopConfig>): {
|
|
|
168
170
|
playwright?: boolean | "cli" | "mcp" | undefined;
|
|
169
171
|
sslCerts?: string | undefined;
|
|
170
172
|
networkMode?: string | undefined;
|
|
173
|
+
blockedDomains?: (string | undefined)[] | undefined;
|
|
171
174
|
env?: {
|
|
172
175
|
[x: string]: string | undefined;
|
|
173
176
|
} | undefined;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/loader.ts","../src/generators/index.ts","../src/config/merge.ts","../src/utils/gitignore.ts","../src/index.ts"],"mappings":";;;cAEM,yBAAA,EAAyB,CAAA,CAAA,SAAA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/loader.ts","../src/generators/index.ts","../src/config/merge.ts","../src/utils/gitignore.ts","../src/index.ts"],"mappings":";;;cAEM,yBAAA,EAAyB,CAAA,CAAA,SAAA;;;;cA+DlB,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQtB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,qBAAA;AAAA,KACjC,cAAA,GAAiB,CAAA,CAAE,MAAA,QAAc,qBAAA;AAAA,KACjC,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;;;iBCrE3B,eAAA,CAAgB,GAAA,YAAe,OAAA,CAAQ,cAAA;;;UCInD,eAAA;EACR,MAAA;EACA,IAAA;AAAA;AAAA,UAGQ,aAAA;EACR,IAAA;EACA,OAAA;AAAA;AAAA,iBAGoB,WAAA,CACpB,MAAA,EAAQ,cAAA,EACR,WAAA,UACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,aAAA;;;;;;AFxBY;;;;;;iBGSP,YAAA,WAAuB,MAAA,kBAAA,CACrC,IAAA,EAAM,CAAA,EACN,SAAA,EAAW,MAAA,oBACV,CAAA;;;;;;AHZoB;iBIeD,oBAAA,CAAqB,GAAA,YAA8B,OAAA;;;KCR7D,WAAA,MAAiB,CAAA,gCAAiC,CAAA,IAAK,WAAA,CAAY,CAAA,CAAE,CAAA,OAAQ,CAAA;;;;iBAKzE,YAAA,CAAa,MAAA,EAAD,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQZ,qBAAA,CACd,MAAA,EAAQ,WAAA,CAD2B,eAAA"}
|
package/dist/loader.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const containerSchema = z.object({
|
|
|
23
23
|
}),
|
|
24
24
|
sslCerts: z.string().optional(),
|
|
25
25
|
networkMode: z.string().default("host"),
|
|
26
|
+
blockedDomains: z.array(z.string().min(1)).default([]),
|
|
26
27
|
env: z.record(z.string(), z.string()).default({}),
|
|
27
28
|
shmSize: z.string().default("64m"),
|
|
28
29
|
capabilities: z.array(z.string()).default([]),
|
|
@@ -44,7 +45,7 @@ const projectSchema = z.object({
|
|
|
44
45
|
description: z.string().default(""),
|
|
45
46
|
context: z.string().default(""),
|
|
46
47
|
backpressureCommands: z.array(backpressureCommandSchema).default([]),
|
|
47
|
-
openAppSkill: z.string().default("")
|
|
48
|
+
openAppSkill: z.union([z.string(), z.array(z.string().min(1))]).default("")
|
|
48
49
|
});
|
|
49
50
|
const outputSchema = z.object({
|
|
50
51
|
mode: z.enum(["direct", "uac"]).default("direct"),
|
package/dist/loader.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.mjs","names":[],"sources":["../src/config/schema.ts","../src/config/defaults.ts","../src/config/merge.ts","../src/config/loader.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst backpressureCommandSchema = z.object({\n name: z.string().min(1),\n command: z.string().min(1),\n})\n\nconst containerHooksSchema = z.object({\n rootSetup: z.array(z.string()).default([]),\n userSetup: z.array(z.string()).default([]),\n entrypointSetup: z.array(z.string()).default([]),\n})\n\nconst containerSchema = z.object({\n name: z.string().min(1),\n baseImage: z.string().min(1).default('node:22-bookworm'),\n user: z.string().min(1).default('sandbox'),\n systemPackages: z.array(z.string()).default([]),\n playwright: z\n .union([z.boolean(), z.enum(['cli', 'mcp'])])\n .default(false)\n .transform((val) => {\n if (val === true) return 'cli' as const\n if (val === false) return false as const\n return val\n }),\n sslCerts: z.string().optional(),\n networkMode: z.string().default('host'),\n env: z.record(z.string(), z.string()).default({}),\n shmSize: z.string().default('64m'),\n capabilities: z.array(z.string()).default([]),\n volumes: z.array(z.string()).default([]),\n shadowVolumes: z.array(z.string()).default([]),\n persistVolumes: z\n .record(z.string(), z.string())\n .default({ 'ralph-claude-config': '/home/sandbox/.claude' }),\n hooks: containerHooksSchema.prefault({}),\n})\n\nconst setupSchema = z.object({\n preStartCommand: z.string().default(''),\n})\n\nconst defaultsSchema = z.object({\n agent: z.literal('claude').default('claude'),\n model: z.string().default('sonnet'),\n verbose: z.boolean().default(false),\n sleepBetweenMs: z.number().int().min(0).default(2000),\n completionSignal: z.string().default('RALPH_WORK_FULLY_DONE'),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n description: z.string().default(''),\n context: z.string().default(''),\n backpressureCommands: z.array(backpressureCommandSchema).default([]),\n openAppSkill: z.string().default(''),\n})\n\nconst outputSchema = z.object({\n mode: z.enum(['direct', 'uac']).default('direct'),\n uacTemplatesDir: z.string().default('.universal-ai-config'),\n})\n\nexport const ralphLoopConfigSchema = z.object({\n container: containerSchema.prefault({ name: 'ralph-container' }),\n setup: setupSchema.prefault({}),\n defaults: defaultsSchema.prefault({}),\n project: projectSchema,\n output: outputSchema.prefault({}),\n})\n\nexport type RalphLoopConfig = z.input<typeof ralphLoopConfigSchema>\nexport type ResolvedConfig = z.output<typeof ralphLoopConfigSchema>\nexport type BackpressureCommand = z.infer<typeof backpressureCommandSchema>\n","import type { ResolvedConfig } from './schema.js'\n\n/**\n * Apply Playwright-specific defaults when playwright is enabled ('cli' or 'mcp').\n * Merges SYS_ADMIN capability and 2gb shm_size if not already set.\n */\nexport function applyPlaywrightDefaults(config: ResolvedConfig): ResolvedConfig {\n if (!config.container.playwright) return config\n\n const shmSize = config.container.shmSize === '64m' ? '2gb' : config.container.shmSize\n\n const capabilities = config.container.capabilities.includes('SYS_ADMIN')\n ? config.container.capabilities\n : [...config.container.capabilities, 'SYS_ADMIN']\n\n return {\n ...config,\n container: {\n ...config.container,\n shmSize,\n capabilities,\n },\n }\n}\n","/**\n * Deep merge two config objects.\n *\n * Merge strategy:\n * - Arrays: overrides REPLACE base arrays entirely\n * - Plain objects: merge recursively\n * - Scalars: overrides replace base values\n * - undefined values in overrides are skipped\n */\nexport function mergeConfigs<T extends Record<string, unknown>>(\n base: T,\n overrides: Record<string, unknown>,\n): T {\n return deepMerge(base, overrides) as T\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction deepMerge(\n base: Record<string, unknown>,\n overrides: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = { ...base }\n\n for (const key of Object.keys(overrides)) {\n const overrideValue = overrides[key]\n const baseValue = base[key]\n\n if (overrideValue === undefined) continue\n\n if (Array.isArray(overrideValue)) {\n result[key] = overrideValue\n } else if (isPlainObject(overrideValue) && isPlainObject(baseValue)) {\n result[key] = deepMerge(baseValue, overrideValue)\n } else {\n result[key] = overrideValue\n }\n }\n\n return result\n}\n","import { loadConfig } from 'c12'\nimport { ralphLoopConfigSchema } from './schema.js'\nimport { applyPlaywrightDefaults } from './defaults.js'\nimport { mergeConfigs } from './merge.js'\nimport type { RalphLoopConfig, ResolvedConfig } from './schema.js'\n\nexport async function loadRalphConfig(cwd?: string): Promise<ResolvedConfig> {\n const { config: baseConfig } = await loadConfig<RalphLoopConfig>({\n name: 'fabis-ralph-loop',\n cwd,\n })\n\n if (!baseConfig || Object.keys(baseConfig).length === 0) {\n throw new Error('No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.')\n }\n\n const { config: overridesConfig } = await loadConfig<Partial<RalphLoopConfig>>({\n name: 'fabis-ralph-loop.overrides',\n cwd,\n })\n\n const merged =\n overridesConfig && Object.keys(overridesConfig).length > 0\n ? mergeConfigs(baseConfig, overridesConfig as RalphLoopConfig)\n : baseConfig\n\n const parsed = ralphLoopConfigSchema.safeParse(merged)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)\n .join('\\n')\n const suffix =\n overridesConfig && Object.keys(overridesConfig).length > 0 ? ' (after merging overrides)' : ''\n throw new Error(`Invalid fabis-ralph-loop config${suffix}:\\n${issues}`)\n }\n\n return applyPlaywrightDefaults(parsed.data)\n}\n"],"mappings":";;;;AAEA,MAAM,4BAA4B,EAAE,OAAO;CACzC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,CAAC;AAEF,MAAM,uBAAuB,EAAE,OAAO;CACpC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC1C,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC1C,iBAAiB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACjD,CAAC;AAEF,MAAM,kBAAkB,EAAE,OAAO;CAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,mBAAmB;CACxD,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,UAAU;CAC1C,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC/C,YAAY,EACT,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAC5C,QAAQ,MAAM,CACd,WAAW,QAAQ;AAClB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,QAAQ,MAAO,QAAO;AAC1B,SAAO;GACP;CACJ,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,aAAa,EAAE,QAAQ,CAAC,QAAQ,OAAO;CACvC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACjD,SAAS,EAAE,QAAQ,CAAC,QAAQ,MAAM;CAClC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC7C,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACxC,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC9C,gBAAgB,EACb,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAC9B,QAAQ,EAAE,uBAAuB,yBAAyB,CAAC;CAC9D,OAAO,qBAAqB,SAAS,EAAE,CAAC;CACzC,CAAC;AAEF,MAAM,cAAc,EAAE,OAAO,EAC3B,iBAAiB,EAAE,QAAQ,CAAC,QAAQ,GAAG,EACxC,CAAC;AAEF,MAAM,iBAAiB,EAAE,OAAO;CAC9B,OAAO,EAAE,QAAQ,SAAS,CAAC,QAAQ,SAAS;CAC5C,OAAO,EAAE,QAAQ,CAAC,QAAQ,SAAS;CACnC,SAAS,EAAE,SAAS,CAAC,QAAQ,MAAM;CACnC,gBAAgB,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,IAAK;CACrD,kBAAkB,EAAE,QAAQ,CAAC,QAAQ,wBAAwB;CAC9D,CAAC;AAEF,MAAM,gBAAgB,EAAE,OAAO;CAC7B,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,aAAa,EAAE,QAAQ,CAAC,QAAQ,GAAG;CACnC,SAAS,EAAE,QAAQ,CAAC,QAAQ,GAAG;CAC/B,sBAAsB,EAAE,MAAM,0BAA0B,CAAC,QAAQ,EAAE,CAAC;CACpE,cAAc,EAAE,QAAQ,CAAC,QAAQ,GAAG;
|
|
1
|
+
{"version":3,"file":"loader.mjs","names":[],"sources":["../src/config/schema.ts","../src/config/defaults.ts","../src/config/merge.ts","../src/config/loader.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst backpressureCommandSchema = z.object({\n name: z.string().min(1),\n command: z.string().min(1),\n})\n\nconst containerHooksSchema = z.object({\n rootSetup: z.array(z.string()).default([]),\n userSetup: z.array(z.string()).default([]),\n entrypointSetup: z.array(z.string()).default([]),\n})\n\nconst containerSchema = z.object({\n name: z.string().min(1),\n baseImage: z.string().min(1).default('node:22-bookworm'),\n user: z.string().min(1).default('sandbox'),\n systemPackages: z.array(z.string()).default([]),\n playwright: z\n .union([z.boolean(), z.enum(['cli', 'mcp'])])\n .default(false)\n .transform((val) => {\n if (val === true) return 'cli' as const\n if (val === false) return false as const\n return val\n }),\n sslCerts: z.string().optional(),\n networkMode: z.string().default('host'),\n blockedDomains: z.array(z.string().min(1)).default([]),\n env: z.record(z.string(), z.string()).default({}),\n shmSize: z.string().default('64m'),\n capabilities: z.array(z.string()).default([]),\n volumes: z.array(z.string()).default([]),\n shadowVolumes: z.array(z.string()).default([]),\n persistVolumes: z\n .record(z.string(), z.string())\n .default({ 'ralph-claude-config': '/home/sandbox/.claude' }),\n hooks: containerHooksSchema.prefault({}),\n})\n\nconst setupSchema = z.object({\n preStartCommand: z.string().default(''),\n})\n\nconst defaultsSchema = z.object({\n agent: z.literal('claude').default('claude'),\n model: z.string().default('sonnet'),\n verbose: z.boolean().default(false),\n sleepBetweenMs: z.number().int().min(0).default(2000),\n completionSignal: z.string().default('RALPH_WORK_FULLY_DONE'),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n description: z.string().default(''),\n context: z.string().default(''),\n backpressureCommands: z.array(backpressureCommandSchema).default([]),\n openAppSkill: z.union([z.string(), z.array(z.string().min(1))]).default(''),\n})\n\nconst outputSchema = z.object({\n mode: z.enum(['direct', 'uac']).default('direct'),\n uacTemplatesDir: z.string().default('.universal-ai-config'),\n})\n\nexport const ralphLoopConfigSchema = z.object({\n container: containerSchema.prefault({ name: 'ralph-container' }),\n setup: setupSchema.prefault({}),\n defaults: defaultsSchema.prefault({}),\n project: projectSchema,\n output: outputSchema.prefault({}),\n})\n\nexport type RalphLoopConfig = z.input<typeof ralphLoopConfigSchema>\nexport type ResolvedConfig = z.output<typeof ralphLoopConfigSchema>\nexport type BackpressureCommand = z.infer<typeof backpressureCommandSchema>\n","import type { ResolvedConfig } from './schema.js'\n\n/**\n * Apply Playwright-specific defaults when playwright is enabled ('cli' or 'mcp').\n * Merges SYS_ADMIN capability and 2gb shm_size if not already set.\n */\nexport function applyPlaywrightDefaults(config: ResolvedConfig): ResolvedConfig {\n if (!config.container.playwright) return config\n\n const shmSize = config.container.shmSize === '64m' ? '2gb' : config.container.shmSize\n\n const capabilities = config.container.capabilities.includes('SYS_ADMIN')\n ? config.container.capabilities\n : [...config.container.capabilities, 'SYS_ADMIN']\n\n return {\n ...config,\n container: {\n ...config.container,\n shmSize,\n capabilities,\n },\n }\n}\n","/**\n * Deep merge two config objects.\n *\n * Merge strategy:\n * - Arrays: overrides REPLACE base arrays entirely\n * - Plain objects: merge recursively\n * - Scalars: overrides replace base values\n * - undefined values in overrides are skipped\n */\nexport function mergeConfigs<T extends Record<string, unknown>>(\n base: T,\n overrides: Record<string, unknown>,\n): T {\n return deepMerge(base, overrides) as T\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction deepMerge(\n base: Record<string, unknown>,\n overrides: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = { ...base }\n\n for (const key of Object.keys(overrides)) {\n const overrideValue = overrides[key]\n const baseValue = base[key]\n\n if (overrideValue === undefined) continue\n\n if (Array.isArray(overrideValue)) {\n result[key] = overrideValue\n } else if (isPlainObject(overrideValue) && isPlainObject(baseValue)) {\n result[key] = deepMerge(baseValue, overrideValue)\n } else {\n result[key] = overrideValue\n }\n }\n\n return result\n}\n","import { loadConfig } from 'c12'\nimport { ralphLoopConfigSchema } from './schema.js'\nimport { applyPlaywrightDefaults } from './defaults.js'\nimport { mergeConfigs } from './merge.js'\nimport type { RalphLoopConfig, ResolvedConfig } from './schema.js'\n\nexport async function loadRalphConfig(cwd?: string): Promise<ResolvedConfig> {\n const { config: baseConfig } = await loadConfig<RalphLoopConfig>({\n name: 'fabis-ralph-loop',\n cwd,\n })\n\n if (!baseConfig || Object.keys(baseConfig).length === 0) {\n throw new Error('No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.')\n }\n\n const { config: overridesConfig } = await loadConfig<Partial<RalphLoopConfig>>({\n name: 'fabis-ralph-loop.overrides',\n cwd,\n })\n\n const merged =\n overridesConfig && Object.keys(overridesConfig).length > 0\n ? mergeConfigs(baseConfig, overridesConfig as RalphLoopConfig)\n : baseConfig\n\n const parsed = ralphLoopConfigSchema.safeParse(merged)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)\n .join('\\n')\n const suffix =\n overridesConfig && Object.keys(overridesConfig).length > 0 ? ' (after merging overrides)' : ''\n throw new Error(`Invalid fabis-ralph-loop config${suffix}:\\n${issues}`)\n }\n\n return applyPlaywrightDefaults(parsed.data)\n}\n"],"mappings":"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"}
|
|
@@ -30,6 +30,12 @@ RUN curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_a
|
|
|
30
30
|
<% } -%>
|
|
31
31
|
&& rm /tmp/chrome.deb && rm -rf /var/lib/apt/lists/*
|
|
32
32
|
<% } -%>
|
|
33
|
+
<% if (blockedDomains.length > 0) { %>
|
|
34
|
+
# === DNS-level domain blocking ===
|
|
35
|
+
# hadolint ignore=DL3008
|
|
36
|
+
RUN apt-get update && apt-get install -y --no-install-recommends dnsmasq \
|
|
37
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
38
|
+
<% } -%>
|
|
33
39
|
<% for (const instruction of hooks.rootSetup) { %>
|
|
34
40
|
<%= instruction %>
|
|
35
41
|
<% } -%>
|
|
@@ -18,6 +18,35 @@ run('git', ['config', '--global', 'url.blocked://push-disabled/.pushInsteadOf',
|
|
|
18
18
|
run('git', ['config', '--global', 'url.blocked://push-disabled/.pushInsteadOf', 'git@'])
|
|
19
19
|
run('git', ['config', '--global', 'url.blocked://push-disabled/.pushInsteadOf', 'ssh://'])
|
|
20
20
|
|
|
21
|
+
<% if (blockedDomains.length > 0) { %>
|
|
22
|
+
// === Blocked domains (dnsmasq wildcard) ===
|
|
23
|
+
const blockedDomains: string[] = <%- JSON.stringify(blockedDomains) %>
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Save Docker's original nameserver before we replace resolv.conf
|
|
27
|
+
const resolv = execSync('cat /etc/resolv.conf', { encoding: 'utf-8' })
|
|
28
|
+
const upstream = resolv.match(/^\s*nameserver\s+(\S+)/m)?.[1] ?? '8.8.8.8'
|
|
29
|
+
|
|
30
|
+
// Configure dnsmasq: forward to original upstream, block listed domains
|
|
31
|
+
const dnsmasqConf = [
|
|
32
|
+
`server=${upstream}`,
|
|
33
|
+
'listen-address=127.0.0.1',
|
|
34
|
+
'bind-interfaces',
|
|
35
|
+
...blockedDomains.map((d) => `address=/${d}/0.0.0.0`),
|
|
36
|
+
].join('\n')
|
|
37
|
+
|
|
38
|
+
execSync(`echo '${dnsmasqConf}' | sudo tee /etc/dnsmasq.d/blocked.conf > /dev/null`, { stdio: 'pipe' })
|
|
39
|
+
|
|
40
|
+
// Start dnsmasq and point resolver to it
|
|
41
|
+
execSync('sudo dnsmasq', { stdio: 'pipe' })
|
|
42
|
+
execSync(`echo 'nameserver 127.0.0.1' | sudo tee /etc/resolv.conf > /dev/null`, { stdio: 'pipe' })
|
|
43
|
+
|
|
44
|
+
console.log(`Blocked ${blockedDomains.length} domain(s) via dnsmasq: ${blockedDomains.join(', ')}`)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn('Warning: Could not set up domain blocking:', error)
|
|
47
|
+
}
|
|
48
|
+
<% } -%>
|
|
49
|
+
|
|
21
50
|
// === Validate agent CLI ===
|
|
22
51
|
const agent = '<%= agent %>'
|
|
23
52
|
try {
|
|
@@ -74,7 +74,7 @@ Only add patterns that are **general and reusable**, not story-specific details.
|
|
|
74
74
|
- Keep changes focused and minimal
|
|
75
75
|
- Follow existing code patterns
|
|
76
76
|
- Use `import type { ... }` for type-only imports (separate from value imports)
|
|
77
|
-
<% if (
|
|
77
|
+
<% if (openAppSkills.length > 0 || playwright) { %>
|
|
78
78
|
## Browser Testing
|
|
79
79
|
<% if (playwright === 'cli') { %>
|
|
80
80
|
This container has `@playwright/cli` installed with headless Chromium. **Do NOT use Playwright MCP or Chrome MCP tools — use the CLI instead.**
|
|
@@ -130,8 +130,13 @@ For any story that changes UI:
|
|
|
130
130
|
1. Ensure the dev servers are running before testing
|
|
131
131
|
2. Use `playwright-cli` to navigate and verify the changes
|
|
132
132
|
3. Capture a screenshot if helpful for the progress log
|
|
133
|
-
<% if (
|
|
134
|
-
Read `<%=
|
|
133
|
+
<% if (openAppSkills.length === 1) { %>
|
|
134
|
+
Read `<%= openAppSkills[0] %>` for the full app launch and authentication procedure.
|
|
135
|
+
<% } else if (openAppSkills.length > 1) { %>
|
|
136
|
+
Multiple app skills are available. Read the appropriate one based on which app you need to verify:
|
|
137
|
+
<% openAppSkills.forEach(function(skill) { %>
|
|
138
|
+
- `<%= skill %>`
|
|
139
|
+
<% }) %>
|
|
135
140
|
<% } -%>
|
|
136
141
|
<% } else if (playwright === 'mcp') { %>
|
|
137
142
|
This container has **Playwright MCP** configured with headless Chromium. For any story that changes UI:
|
|
@@ -140,12 +145,24 @@ This container has **Playwright MCP** configured with headless Chromium. For any
|
|
|
140
145
|
2. Navigate to the relevant page
|
|
141
146
|
3. Verify the UI changes work as expected
|
|
142
147
|
4. Take a screenshot if helpful for the progress log
|
|
143
|
-
<% if (
|
|
144
|
-
Read `<%=
|
|
148
|
+
<% if (openAppSkills.length === 1) { %>
|
|
149
|
+
Read `<%= openAppSkills[0] %>` for the full app launch and authentication procedure.
|
|
150
|
+
<% } else if (openAppSkills.length > 1) { %>
|
|
151
|
+
Multiple app skills are available. Read the appropriate one based on which app you need to verify:
|
|
152
|
+
<% openAppSkills.forEach(function(skill) { %>
|
|
153
|
+
- `<%= skill %>`
|
|
154
|
+
<% }) %>
|
|
145
155
|
<% } -%>
|
|
146
|
-
<% } else if (
|
|
156
|
+
<% } else if (openAppSkills.length > 0) { %>
|
|
147
157
|
For any story that changes UI, verify it works in the browser if you have browser tools configured.
|
|
148
|
-
|
|
158
|
+
<% if (openAppSkills.length === 1) { %>
|
|
159
|
+
Read `<%= openAppSkills[0] %>` for the full app launch and authentication procedure.
|
|
160
|
+
<% } else { %>
|
|
161
|
+
Multiple app skills are available. Read the appropriate one based on which app you need to verify:
|
|
162
|
+
<% openAppSkills.forEach(function(skill) { %>
|
|
163
|
+
- `<%= skill %>`
|
|
164
|
+
<% }) %>
|
|
165
|
+
<% } %>
|
|
149
166
|
|
|
150
167
|
If no browser tools are available, note in your progress report that manual browser verification is needed.
|
|
151
168
|
<% } -%>
|
|
@@ -171,6 +188,9 @@ After completing one story (commit done, prd.json updated, progress appended):
|
|
|
171
188
|
- Read the Codebase Patterns section in `.ralph/progress.txt` before starting
|
|
172
189
|
- Do NOT push — git push is blocked in this sandbox
|
|
173
190
|
- Do NOT modify stories other than the one you're working on
|
|
191
|
+
<% if (blockedDomains.length > 0) { -%>
|
|
192
|
+
- Some MCP endpoints are blocked and unreachable in this container (<%= blockedDomains.join(', ') %>). If an MCP tool fails to connect, accept it and move on without using that MCP server. Do not EVER attempt to debug or fix the connection, even if instructed otherwise.
|
|
193
|
+
<% } -%>
|
|
174
194
|
|
|
175
195
|
### Tasks before stopping
|
|
176
196
|
|
|
@@ -45,13 +45,13 @@ export default defineConfig({
|
|
|
45
45
|
|
|
46
46
|
The only required top-level key. Defines project identity and quality checks.
|
|
47
47
|
|
|
48
|
-
| Key | Type | Default | Purpose
|
|
49
|
-
| ---------------------- | ----------------------- | ------------ |
|
|
50
|
-
| `name` | `string` | **required** | Project name — shown in agent prompts and used as identifier
|
|
51
|
-
| `description` | `string` | `''` | One-line project description shown in Ralph agent instructions
|
|
52
|
-
| `context` | `string` | `''` | Extended project context (tech stack, monorepo structure, conventions). Included verbatim in the Ralph prompt
|
|
53
|
-
| `backpressureCommands` | `BackpressureCommand[]` | `[]` | Quality check commands Ralph runs after each story (see below)
|
|
54
|
-
| `openAppSkill` | `string`
|
|
48
|
+
| Key | Type | Default | Purpose |
|
|
49
|
+
| ---------------------- | ----------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
50
|
+
| `name` | `string` | **required** | Project name — shown in agent prompts and used as identifier |
|
|
51
|
+
| `description` | `string` | `''` | One-line project description shown in Ralph agent instructions |
|
|
52
|
+
| `context` | `string` | `''` | Extended project context (tech stack, monorepo structure, conventions). Included verbatim in the Ralph prompt |
|
|
53
|
+
| `backpressureCommands` | `BackpressureCommand[]` | `[]` | Quality check commands Ralph runs after each story (see below) |
|
|
54
|
+
| `openAppSkill` | `string \| string[]` | `''` | Path(s) to skill file(s) for opening app(s) in a browser. Use an array when multiple apps need verification (e.g. main app, admin backend) |
|
|
55
55
|
|
|
56
56
|
#### Backpressure Commands
|
|
57
57
|
|
|
@@ -88,22 +88,23 @@ backpressureCommands: [
|
|
|
88
88
|
|
|
89
89
|
Docker container configuration. Controls the environment Ralph runs in.
|
|
90
90
|
|
|
91
|
-
| Key | Type | Default | Purpose
|
|
92
|
-
| ---------------- | --------------------------- | ---------------------------------------------------- |
|
|
93
|
-
| `name` | `string` | `'ralph-container'` | Docker container name. Change if running multiple Ralph instances
|
|
94
|
-
| `baseImage` | `string` | `'node:22-bookworm'` | Base Docker image. Use a different Node version or distro if needed
|
|
95
|
-
| `user` | `string` | `'sandbox'` | Container user. Default creates `sandbox` (UID 1000). Set to existing user (e.g. `'node'`) to reuse it
|
|
96
|
-
| `systemPackages` | `string[]` | `[]` | APT packages to install (e.g., `['postgresql-client', 'redis-tools']`)
|
|
97
|
-
| `playwright` | `boolean \| 'cli' \| 'mcp'` | `false` | Enable Playwright browser testing. `true` or `'cli'` uses @playwright/cli (preferred). `'mcp'` uses Playwright MCP server. Auto-adds `SYS_ADMIN` capability and sets `shmSize` to `'2gb'`
|
|
98
|
-
| `sslCerts` | `string` | `undefined` | Host path to SSL certificate directory. Certs are trusted system-wide in the container. When combined with `playwright`, also adds certs to Chromium's NSS store
|
|
99
|
-
| `networkMode` | `string` | `'host'` | Docker network mode. Use `'bridge'` if host networking causes conflicts
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
91
|
+
| Key | Type | Default | Purpose |
|
|
92
|
+
| ---------------- | --------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `name` | `string` | `'ralph-container'` | Docker container name. Change if running multiple Ralph instances |
|
|
94
|
+
| `baseImage` | `string` | `'node:22-bookworm'` | Base Docker image. Use a different Node version or distro if needed |
|
|
95
|
+
| `user` | `string` | `'sandbox'` | Container user. Default creates `sandbox` (UID 1000). Set to existing user (e.g. `'node'`) to reuse it |
|
|
96
|
+
| `systemPackages` | `string[]` | `[]` | APT packages to install (e.g., `['postgresql-client', 'redis-tools']`) |
|
|
97
|
+
| `playwright` | `boolean \| 'cli' \| 'mcp'` | `false` | Enable Playwright browser testing. `true` or `'cli'` uses @playwright/cli (preferred). `'mcp'` uses Playwright MCP server. Auto-adds `SYS_ADMIN` capability and sets `shmSize` to `'2gb'` |
|
|
98
|
+
| `sslCerts` | `string` | `undefined` | Host path to SSL certificate directory. Certs are trusted system-wide in the container. When combined with `playwright`, also adds certs to Chromium's NSS store |
|
|
99
|
+
| `networkMode` | `string` | `'host'` | Docker network mode. Use `'bridge'` if host networking causes conflicts |
|
|
100
|
+
| `blockedDomains` | `string[]` | `[]` | Domains to block inside the container via dnsmasq. Blocks the domain and all subdomains (e.g., `'figma.com'` blocks `mcp.figma.com`, `api.figma.com`, etc.). Useful for blocking MCP servers meant for interactive use only |
|
|
101
|
+
| `env` | `Record<string, string>` | `{}` | Environment variables injected into the container |
|
|
102
|
+
| `shmSize` | `string` | `'64m'` | Shared memory size. Auto-upgraded to `'2gb'` when `playwright` is enabled |
|
|
103
|
+
| `capabilities` | `string[]` | `[]` | Docker capabilities (e.g., `['SYS_ADMIN']`). Auto-added when `playwright` is enabled |
|
|
104
|
+
| `volumes` | `string[]` | `[]` | Additional Docker volume mounts (standard Docker `-v` syntax) |
|
|
105
|
+
| `shadowVolumes` | `string[]` | `[]` | Paths to exclude from the project mount using anonymous volumes (see below) |
|
|
106
|
+
| `persistVolumes` | `Record<string, string>` | `{ 'ralph-claude-config': '/home/sandbox/.claude' }` | Named volumes that persist across container restarts. Key = volume name, value = container path |
|
|
107
|
+
| `hooks` | `ContainerHooks` | `{}` | Dockerfile build hooks (see below) |
|
|
107
108
|
|
|
108
109
|
#### Shadow Volumes
|
|
109
110
|
|