fabis-ralph-loop 1.1.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.
@@ -1,11 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
- import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { consola } from "consola";
5
5
  import { existsSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import ejs from "ejs";
8
- import { tmpdir } from "node:os";
9
8
  import { generate, writeGeneratedFiles } from "universal-ai-config";
10
9
 
11
10
  //#region src/utils/template.ts
@@ -137,29 +136,21 @@ async function generateSkills(config, projectRoot) {
137
136
  async function generateDirect(config, projectRoot) {
138
137
  const variables = buildLevel1Variables(config);
139
138
  const skills = await discoverSkills();
140
- const tempDir = join(tmpdir(), `ralph-skills-${Date.now()}`);
141
- try {
142
- for (const skill of skills) {
139
+ const files = await generate({
140
+ root: projectRoot,
141
+ targets: ["claude"],
142
+ types: ["skills"],
143
+ templates: await Promise.all(skills.map(async (skill) => {
143
144
  const template = await readFile(join(UAC_TEMPLATES_DIR, "skills", skill, "SKILL.md"), "utf8");
144
- const rendered = ejs.render(template, variables);
145
- const outDir = join(tempDir, "skills", skill);
146
- await mkdir(outDir, { recursive: true });
147
- await writeFile(join(outDir, "SKILL.md"), rendered, "utf8");
148
- }
149
- const files = await generate({
150
- root: projectRoot,
151
- targets: ["claude"],
152
- types: ["skills"],
153
- overrides: { templatesDir: tempDir }
154
- });
155
- await writeGeneratedFiles(files, projectRoot);
156
- consola.info(`Generated ${files.length} skill file(s)`);
157
- } finally {
158
- await rm(tempDir, {
159
- recursive: true,
160
- force: true
161
- });
162
- }
145
+ return {
146
+ name: skill,
147
+ type: "skills",
148
+ content: ejs.render(template, variables)
149
+ };
150
+ }))
151
+ });
152
+ await writeGeneratedFiles(files, projectRoot);
153
+ consola.info(`Generated ${files.length} skill file(s)`);
163
154
  }
164
155
  async function generateUac(config, projectRoot) {
165
156
  const variables = buildLevel1Variables(config);
@@ -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 }) 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 hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\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 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 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 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, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } 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 and write to a temp dir structured as UAC templates\n const tempDir = join(tmpdir(), `ralph-skills-${Date.now()}`)\n\n try {\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 const rendered = ejs.render(template, variables) as string\n\n const outDir = join(tempDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\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 overrides: { templatesDir: tempDir },\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\n } finally {\n await rm(tempDir, { recursive: true, force: true })\n }\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) 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/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 }) 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 hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\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 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 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 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) 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) 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":";;;;;;;;;;;;;;;AAWA,SAAgB,gBAAgB,WAAmB,SAAyB;CAC1E,MAAM,MAAM,QAAQ,cAAc,QAAQ,CAAC;CAC3C,MAAM,UAAU,KAAK,KAAK,UAAU;AACpC,KAAI,WAAW,QAAQ,CAAE,QAAO;AAChC,QAAO,KAAK,KAAK,MAAM,UAAU;;AAGnC,MAAM,gBAAgB,gBAAgB,aAAa,OAAO,KAAK,IAAI;AAEnE,eAAsB,eACpB,cACA,MACiB;CAEjB,MAAM,WAAW,MAAM,SADF,KAAK,eAAe,aAAa,EACR,OAAO;AACrD,QAAO,IAAI,OAAO,UAAU,MAAM,EAAE,OAAO,OAAO,CAAC;;AAGrD,MAAa,mBAAmB;;;;;;AC3BhC,SAAgB,oBAA4B;AAC1C,KAAI;AAGF,SAFgB,cAAc,OAAO,KAAK,IAAI,CAC1B,qBAAqB,CAC9B;SACL;AACN,SAAO;;;;;;;;;ACDX,SAAS,gBAAgB,WAA4B;AACnD,QAAO,aAAa,KAAK,UAAU;;AAGrC,eAAsB,mBAAmB,QAAyC;CAChF,MAAM,OAAO,OAAO,UAAU;AAC9B,QAAO,eAAe,kBAAkB;EACtC,iBAAiB;EACjB,WAAW,OAAO,UAAU;EAC5B,gBAAgB,OAAO,UAAU;EACjC,aAAa,CAAC,gBAAgB,OAAO,UAAU,UAAU;EACzD,YAAY,OAAO,UAAU;EAC7B,OAAO,OAAO,UAAU;EACxB;EACA,YAAY,SAAS;EACrB,SAAS,SAAS;EAClB,gBAAgB,mBAAmB;EACpC,CAAC;;;;;ACrBJ,eAAsB,gBAAgB,QAAyC;CAC7E,MAAM,UAAU,SAAS,OAAO,UAAU;CAG1C,MAAM,iBAAyC;EAC7C,uBAAuB,GAAG,QAAQ;EAClC,GAAG,OAAO,YACR,OAAO,QAAQ,OAAO,UAAU,eAAe,CAAC,KAAK,CAAC,MAAM,UAAU,CACpE,MACA,KAAK,QAAQ,iBAAiB,QAAQ,CACvC,CAAC,CACH;EACF;AAED,QAAO,eAAe,0BAA0B;EAC9C,iBAAiB;EACjB,eAAe,OAAO,UAAU;EAChC,SAAS,OAAO,UAAU;EAC1B,aAAa,OAAO,UAAU;EAC9B,cAAc,OAAO,UAAU;EAC/B,eAAe,OAAO,UAAU;EAChC;EACA,cAAc,OAAO,UAAU;EAC/B,KAAK,OAAO,UAAU;EACtB;EACD,CAAC;;;;;ACzBJ,eAAsB,mBAAmB,QAAyC;CAChF,MAAM,OAAO,OAAO,UAAU;AAC9B,QAAO,eAAe,qBAAqB;EACzC,iBAAiB,iBAAiB,QAAQ,SAAS,MAAM;EACzD,OAAO,OAAO,SAAS;EACvB,eAAe,OAAO,UAAU;EAChC,iBAAiB,OAAO,UAAU,MAAM;EACxC;EACA,SAAS,SAAS;EACnB,CAAC;;;;;ACTJ,eAAsB,eAAe,QAAyC;AAC5E,QAAO,eAAe,uBAAuB;EAC3C,iBAAiB;EACjB,aAAa,OAAO,QAAQ;EAC5B,oBAAoB,OAAO,QAAQ;EACnC,gBAAgB,OAAO,QAAQ;EAC/B,sBAAsB,OAAO,QAAQ;EACrC,cAAc,OAAO,QAAQ;EAC7B,YAAY,OAAO,UAAU;EAC7B,kBAAkB,OAAO,SAAS;EACnC,CAAC;;;;;ACJJ,MAAM,oBAAoB,gBAAgB,iBAAiB,OAAO,KAAK,IAAI;AAE3E,SAAS,qBAAqB,QAAiD;AAC7E,QAAO;EACL,sBAAsB,OAAO,QAAQ;EACrC,aAAa,OAAO,QAAQ;EAC5B,gBAAgB,OAAO,QAAQ;EAC/B,cAAc,OAAO,QAAQ;EAC7B,YAAY,OAAO,UAAU;EAC7B;EACD;;AAGH,eAAe,iBAAoC;AAGjD,SADgB,MAAM,QADJ,KAAK,mBAAmB,SAAS,EACV,EAAE,eAAe,MAAM,CAAC,EAClD,QAAQ,MAAM,EAAE,aAAa,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK;;AAGlE,eAAsB,eAAe,QAAwB,aAAoC;AAC/F,KAAI,OAAO,OAAO,SAAS,SACzB,OAAM,eAAe,QAAQ,YAAY;KAEzC,OAAM,YAAY,QAAQ,YAAY;;AAI1C,eAAe,eAAe,QAAwB,aAAoC;CACxF,MAAM,YAAY,qBAAqB,OAAO;CAC9C,MAAM,SAAS,MAAM,gBAAgB;CAarC,MAAM,QAAQ,MAAM,SAAS;EAC3B,MAAM;EACN,SAAS,CAAC,SAAS;EACnB,OAAO,CAAC,SAAS;EACjB,WAdoC,MAAM,QAAQ,IAClD,OAAO,IAAI,OAAO,UAAU;GAE1B,MAAM,WAAW,MAAM,SADF,KAAK,mBAAmB,UAAU,OAAO,WAAW,EAC3B,OAAO;AAErD,UAAO;IAAE,MAAM;IAAO,MAAM;IAAmB,SAD9B,IAAI,OAAO,UAAU,UAAU;IACkB;IAClE,CACH;EAQA,CAAC;AAEF,OAAM,oBAAoB,OAAO,YAAY;AAC7C,SAAQ,KAAK,aAAa,MAAM,OAAO,gBAAgB;;AAGzD,eAAe,YAAY,QAAwB,aAAoC;CACrF,MAAM,YAAY,qBAAqB,OAAO;CAC9C,MAAM,SAAS,MAAM,gBAAgB;CACrC,IAAI,QAAQ;AAEZ,MAAK,MAAM,SAAS,QAAQ;EAE1B,MAAM,WAAW,MAAM,SADF,KAAK,mBAAmB,UAAU,OAAO,WAAW,EAC3B,OAAO;EAErD,MAAM,WAAW,IAAI,OAAO,UAAU,UAAU;EAEhD,MAAM,SAAS,KAAK,aAAa,OAAO,OAAO,iBAAiB,UAAU,MAAM;AAChF,QAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;AACxC,QAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,UAAU,OAAO;AAC3D;;AAGF,SAAQ,KAAK,aAAa,MAAM,wBAAwB,OAAO,OAAO,gBAAgB,UAAU;;;;;AC3DlG,eAAsB,YACpB,QACA,aACA,UAA2B,EAAE,EACH;CAC1B,MAAM,QAAyB,EAAE;AAEjC,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,aAAa;AAEjD,QAAM,MADe,KAAK,aAAa,mBAAmB,EAChC,EAAE,WAAW,MAAM,CAAC;EAE9C,MAAM,aAAa,MAAM,mBAAmB,OAAO;AACnD,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,aAAa;GAAE,SAAS;GAAY,CAAC;EAEjF,MAAM,aAAa,MAAM,mBAAmB,OAAO;AACnD,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,gBAAgB;GAAE,SAAS;GAAY,CAAC;EAEpF,MAAM,UAAU,MAAM,gBAAgB,OAAO;AAC7C,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,qBAAqB;GAAE,SAAS;GAAS,CAAC;;AAGxF,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,UAAU;EAC9C,MAAM,SAAS,MAAM,eAAe,OAAO;AAC3C,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,kBAAkB;GAAE,SAAS;GAAQ,CAAC;;AAGpF,KAAI,QAAQ,OACV,MAAK,MAAM,QAAQ,MACjB,SAAQ,KAAK,0BAA0B,KAAK,OAAO;KAGrD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,aAAa,KAAK,KAAK;AAC7C,QAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,QAAM,UAAU,UAAU,KAAK,SAAS,OAAO;AAC/C,UAAQ,QAAQ,YAAY,KAAK,OAAO;;AAI5C,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,SACpC,KAAI,QAAQ,OACV,SAAQ,KAAK,kCAAkC;KAE/C,OAAM,eAAe,QAAQ,YAAY;AAI7C,QAAO;;;;;AC/DT,MAAM,eAAe;AAGrB,MAAM,kBAAkB,GAAG,aAAa;;;;;;;AAQxC,eAAsB,qBAAqB,MAAc,QAAQ,KAAK,EAAiB;CACrF,MAAM,gBAAgB,KAAK,KAAK,aAAa;CAE7C,IAAI,UAAU;AACd,KAAI,WAAW,cAAc,CAC3B,WAAU,MAAM,SAAS,eAAe,OAAO;AAGjD,KAAI,QAAQ,SAAS,aAAa,CAAE;AAMpC,OAAM,UAAU,eAJG,QAAQ,SAAS,GAChC,GAAG,QAAQ,SAAS,CAAC,MAAM,gBAAgB,MAC3C,GAAG,gBAAgB,KAEoB,OAAO"}
package/dist/run.mjs CHANGED
@@ -190,6 +190,45 @@ function buildAgentArgs(agent, options) {
190
190
  }
191
191
  return [];
192
192
  }
193
+ function formatCost(cost) {
194
+ return cost != null ? `$${cost.toFixed(4)}` : "$?";
195
+ }
196
+ function logSummaryTable(stats) {
197
+ const totalTurns = stats.reduce((sum, s) => sum + s.turns, 0);
198
+ const costs = stats.map((s) => s.cost);
199
+ const totalCost = costs.every((c) => c != null) ? costs.reduce((sum, c) => sum + c, 0) : null;
200
+ const rows = stats.map((s) => [
201
+ String(s.iteration),
202
+ String(s.turns),
203
+ formatCost(s.cost)
204
+ ]);
205
+ const totalRow = [
206
+ "Total",
207
+ String(totalTurns),
208
+ formatCost(totalCost)
209
+ ];
210
+ const headers = [
211
+ "Iteration",
212
+ "Turns",
213
+ "Cost"
214
+ ];
215
+ const allRows = [...rows, totalRow];
216
+ const widths = headers.map((h, col) => {
217
+ const cellLengths = allRows.map((r) => (r[col] ?? "").length);
218
+ return Math.max(h.length, ...cellLengths);
219
+ });
220
+ const formatRow = (row) => row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ");
221
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
222
+ const lines = [
223
+ "",
224
+ formatRow(headers),
225
+ separator,
226
+ ...rows.map(formatRow),
227
+ separator,
228
+ formatRow(totalRow)
229
+ ];
230
+ consola.info(lines.join("\n"));
231
+ }
193
232
  function sleep(ms, signal) {
194
233
  return new Promise((resolve) => {
195
234
  if (signal?.aborted) {
@@ -224,6 +263,7 @@ async function runLoop(config, options) {
224
263
  `Verbose: ${verbose}`,
225
264
  `Iterations: ${options.iterations}`
226
265
  ].join(" | "));
266
+ const iterationStats = [];
227
267
  try {
228
268
  for (let i = 1; i <= options.iterations; i++) {
229
269
  if (signal.aborted) break;
@@ -252,6 +292,11 @@ async function runLoop(config, options) {
252
292
  const progress = parser.getResult();
253
293
  finalOutput = progress.output;
254
294
  turnsUsed = String(progress.turns);
295
+ iterationStats.push({
296
+ iteration: i,
297
+ turns: progress.turns,
298
+ cost: progress.cost
299
+ });
255
300
  if (result.exitCode !== 0) consola.warn(`Agent exited with code ${result.exitCode}`);
256
301
  } else {
257
302
  const result = await execAgent({
@@ -287,6 +332,7 @@ async function runLoop(config, options) {
287
332
  consola.warn("Check .ralph/progress.txt for status.");
288
333
  process.exitCode = 1;
289
334
  } finally {
335
+ if (verbose && iterationStats.length > 0) logSummaryTable(iterationStats);
290
336
  process.removeListener("SIGINT", handleSignal);
291
337
  process.removeListener("SIGTERM", handleSignal);
292
338
  }
package/dist/run.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.mjs","names":[],"sources":["../src/container/exec.ts","../src/loop/progress.ts","../src/loop/archive.ts","../src/loop/runner.ts","../src/commands/run.ts"],"sourcesContent":["import { execa } from 'execa'\n\ninterface AgentExecOptions {\n command: string\n args: string[]\n input?: string\n onData?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n signal?: AbortSignal\n}\n\ninterface AgentExecResult {\n stdout: string\n stderr: string\n exitCode: number\n aborted: boolean\n}\n\n/**\n * Execute a command directly as a child process.\n * Returns stdout, stderr, and exit code. If onData/onStderr are provided,\n * streams chunks in real-time. Supports AbortSignal for clean cancellation.\n */\nexport async function execAgent(options: AgentExecOptions): Promise<AgentExecResult> {\n const { command, args, input, onData, onStderr, signal } = options\n\n const proc = execa(command, args, { input, reject: false })\n\n if (onData && proc.stdout) {\n proc.stdout.on('data', onData)\n }\n if (onStderr && proc.stderr) {\n proc.stderr.on('data', onStderr)\n }\n\n let aborted = false\n if (signal) {\n const onAbort = () => {\n aborted = true\n proc.kill('SIGTERM')\n }\n if (signal.aborted) {\n onAbort()\n } else {\n signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n const result = await proc\n return {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 1,\n aborted,\n }\n}\n","import { consola } from 'consola'\n\ninterface StreamMessage {\n type: string\n message?: {\n content?: Array<{\n type: string\n name?: string\n text?: string\n input?: Record<string, unknown>\n }>\n }\n result?: string\n total_cost_usd?: number\n}\n\ninterface ProgressResult {\n output: string\n turns: number\n cost: number | null\n}\n\n/**\n * Streaming progress parser for Claude stream-json output.\n * Processes lines incrementally as they arrive, logging progress to stderr.\n */\nexport class StreamProgressParser {\n private turns = 0\n private cost: number | null = null\n private resultText = ''\n private buffer = ''\n\n constructor(private iteration: number) {}\n\n /**\n * Feed a raw chunk of data. Internally buffers and processes complete lines.\n */\n processChunk(chunk: Buffer | string): void {\n this.buffer += chunk.toString()\n const lines = this.buffer.split('\\n')\n this.buffer = lines.pop() ?? '' // keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.trim()) this.processLine(line)\n }\n }\n\n /**\n * Flush any remaining buffered data. Call after the process exits.\n */\n flush(): void {\n if (this.buffer.trim()) {\n this.processLine(this.buffer)\n this.buffer = ''\n }\n }\n\n getResult(): ProgressResult {\n return { output: this.resultText, turns: this.turns, cost: this.cost }\n }\n\n private processLine(line: string): void {\n let message: StreamMessage\n try {\n message = JSON.parse(line) as StreamMessage\n } catch {\n return\n }\n\n switch (message.type) {\n case 'system':\n case 'user':\n break\n\n case 'assistant': {\n this.turns++\n consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`)\n\n if (message.message?.content) {\n const toolDetails = message.message.content\n .filter((c) => c.type === 'tool_use')\n .map((c) => {\n const name = c.name || ''\n const input = (c.input || {}) as Record<string, string>\n\n if (['Read', 'Write', 'Edit'].includes(name)) {\n const filePath = input.file_path || ''\n return `${name} ${filePath.split('/').pop()}`\n }\n if (name === 'Glob') return `${name} ${input.pattern || ''}`\n if (name === 'Grep') return `${name} ${input.pattern || ''}`\n if (name === 'Bash') return `${name} ${(input.command || '').slice(0, 80)}`\n return name\n })\n\n if (toolDetails.length > 0) {\n consola.info(` ${toolDetails.join('\\n ')}`)\n }\n\n const text = message.message.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text || '')\n .join('')\n\n if (text) {\n consola.info(text)\n }\n }\n break\n }\n\n case 'result': {\n this.resultText = message.result || ''\n this.cost = message.total_cost_usd ?? null\n consola.info('---')\n consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? '?'}`)\n break\n }\n\n default: {\n if (message.type) {\n consola.debug(`[debug] unrecognized type: ${message.type}`)\n }\n break\n }\n }\n }\n}\n\n/**\n * Parse Claude stream-json output into human-readable progress.\n * Returns the final result text and metadata.\n */\nexport function parseStreamOutput(rawOutput: string, iteration: number): ProgressResult {\n const parser = new StreamProgressParser(iteration)\n parser.processChunk(rawOutput)\n parser.flush()\n return parser.getResult()\n}\n","import { readFile, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\n\nconst RALPH_DIR = '.ralph'\nconst PRD_FILE = join(RALPH_DIR, 'prd.json')\nconst PROGRESS_FILE = join(RALPH_DIR, 'progress.txt')\nconst LAST_BRANCH_FILE = join(RALPH_DIR, '.last-branch')\nconst ARCHIVE_DIR = join(RALPH_DIR, 'archive')\n\nexport async function archiveIfBranchChanged(): Promise<void> {\n if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {\n await trackCurrentBranch()\n return\n }\n\n let currentBranch: string\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n currentBranch = prd.branchName || ''\n } catch {\n return\n }\n\n let lastBranch: string\n try {\n lastBranch = (await readFile(LAST_BRANCH_FILE, 'utf8')).trim()\n } catch {\n lastBranch = ''\n }\n\n if (!currentBranch || !lastBranch || currentBranch === lastBranch) {\n await trackCurrentBranch()\n return\n }\n\n // Archive the previous run\n const date = new Date().toISOString().split('T')[0]\n const folderName = lastBranch.replace(/^ralph\\//, '')\n const archiveFolder = join(ARCHIVE_DIR, `${date}-${folderName}`)\n\n consola.info(`Archiving previous run: ${lastBranch}`)\n await mkdir(archiveFolder, { recursive: true })\n\n if (existsSync(PRD_FILE)) {\n await cp(PRD_FILE, join(archiveFolder, 'prd.json'))\n }\n if (existsSync(PROGRESS_FILE)) {\n await cp(PROGRESS_FILE, join(archiveFolder, 'progress.txt'))\n }\n consola.info(`Archived to: ${archiveFolder}`)\n\n // Reset progress file\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n\n await trackCurrentBranch()\n}\n\nasync function trackCurrentBranch(): Promise<void> {\n if (!existsSync(PRD_FILE)) return\n\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n const branch = prd.branchName\n if (branch) {\n await mkdir(RALPH_DIR, { recursive: true })\n await writeFile(LAST_BRANCH_FILE, branch + '\\n')\n }\n } catch {\n // PRD doesn't exist or is invalid, skip\n }\n}\n\nexport async function ensureProgressFile(): Promise<void> {\n await mkdir(RALPH_DIR, { recursive: true })\n if (!existsSync(PROGRESS_FILE)) {\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n }\n}\n","import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { consola } from 'consola'\nimport { execAgent } from '../container/exec.js'\nimport { StreamProgressParser } from './progress.js'\nimport { archiveIfBranchChanged, ensureProgressFile } from './archive.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface RunOptions {\n iterations: number\n model?: string\n verbose?: boolean\n}\n\nfunction buildAgentArgs(agent: string, options: { model: string; verbose: boolean }): string[] {\n if (agent === 'claude') {\n const args = ['--dangerously-skip-permissions', '--model', options.model, '--print']\n if (options.verbose) {\n args.push('--verbose', '--output-format', 'stream-json')\n }\n return args\n }\n // Future: support other agents\n return []\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n })\n}\n\nexport async function runLoop(config: ResolvedConfig, options: RunOptions): Promise<void> {\n const model = options.model || config.defaults.model\n const verbose = options.verbose ?? config.defaults.verbose\n const { sleepBetweenMs, completionSignal } = config.defaults\n\n if (!existsSync('/.dockerenv')) {\n consola.warn(\n 'It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).',\n )\n }\n\n await ensureProgressFile()\n\n const abortController = new AbortController()\n const { signal } = abortController\n\n // First Ctrl+C: graceful cleanup (abort agent + container process).\n // Second Ctrl+C: force exit (process.once removes the handler after first call,\n // so the default Node.js SIGINT behavior kicks in).\n const handleSignal = () => {\n consola.warn('\\nInterrupted. Cleaning up...')\n abortController.abort()\n }\n process.once('SIGINT', handleSignal)\n process.once('SIGTERM', handleSignal)\n\n consola.info(\n [\n `Starting Ralph`,\n `Agent: ${config.defaults.agent}`,\n `Model: ${model}`,\n `Verbose: ${verbose}`,\n `Iterations: ${options.iterations}`,\n ].join(' | '),\n )\n\n try {\n for (let i = 1; i <= options.iterations; i++) {\n if (signal.aborted) break\n\n consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`)\n\n await archiveIfBranchChanged()\n\n const promptContent = await readFile('.ralph-container/ralph-prompt.md', 'utf8')\n const agentArgs = buildAgentArgs(config.defaults.agent, { model, verbose })\n\n let finalOutput: string\n let turnsUsed = '?'\n\n try {\n if (verbose) {\n // Verbose: parse stream-json lines in real-time, show progress on stderr\n const parser = new StreamProgressParser(i)\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => parser.processChunk(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n parser.flush()\n const progress = parser.getResult()\n finalOutput = progress.output\n turnsUsed = String(progress.turns)\n\n if (result.exitCode !== 0) {\n consola.warn(`Agent exited with code ${result.exitCode}`)\n }\n } else {\n // Non-verbose: stream both stdout and stderr to stderr in real-time\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => process.stderr.write(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n finalOutput = result.stdout\n }\n } catch (error) {\n consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`)\n finalOutput = ''\n }\n\n if (finalOutput.includes(completionSignal)) {\n consola.success(\n `All stories complete! Completed at iteration ${i} of ${options.iterations}`,\n )\n return\n }\n\n if (i < options.iterations) {\n consola.info(\n `Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`,\n )\n await sleep(sleepBetweenMs, signal)\n }\n }\n\n if (signal.aborted) {\n consola.info('Stopped.')\n process.exitCode = 130\n return\n }\n\n consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`)\n consola.warn('Check .ralph/progress.txt for status.')\n process.exitCode = 1\n } finally {\n process.removeListener('SIGINT', handleSignal)\n process.removeListener('SIGTERM', handleSignal)\n }\n}\n","import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { runLoop } from '../loop/runner.js'\n\nexport default defineCommand({\n meta: {\n name: 'run',\n description: 'Execute the ralph iteration loop',\n },\n args: {\n iterations: {\n type: 'positional',\n description: 'Number of iterations to run (required)',\n required: true,\n },\n model: {\n type: 'string',\n description: 'Override default model',\n },\n verbose: {\n type: 'boolean',\n description: 'Enable verbose stream-json progress output',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const iterations = Number.parseInt(args.iterations as string, 10)\n\n if (Number.isNaN(iterations) || iterations < 1) {\n throw new Error('iterations must be a positive integer')\n }\n\n await runLoop(config, {\n iterations,\n model: args.model,\n verbose: args.verbose,\n })\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"run.mjs","names":[],"sources":["../src/container/exec.ts","../src/loop/progress.ts","../src/loop/archive.ts","../src/loop/runner.ts","../src/commands/run.ts"],"sourcesContent":["import { execa } from 'execa'\n\ninterface AgentExecOptions {\n command: string\n args: string[]\n input?: string\n onData?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n signal?: AbortSignal\n}\n\ninterface AgentExecResult {\n stdout: string\n stderr: string\n exitCode: number\n aborted: boolean\n}\n\n/**\n * Execute a command directly as a child process.\n * Returns stdout, stderr, and exit code. If onData/onStderr are provided,\n * streams chunks in real-time. Supports AbortSignal for clean cancellation.\n */\nexport async function execAgent(options: AgentExecOptions): Promise<AgentExecResult> {\n const { command, args, input, onData, onStderr, signal } = options\n\n const proc = execa(command, args, { input, reject: false })\n\n if (onData && proc.stdout) {\n proc.stdout.on('data', onData)\n }\n if (onStderr && proc.stderr) {\n proc.stderr.on('data', onStderr)\n }\n\n let aborted = false\n if (signal) {\n const onAbort = () => {\n aborted = true\n proc.kill('SIGTERM')\n }\n if (signal.aborted) {\n onAbort()\n } else {\n signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n const result = await proc\n return {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 1,\n aborted,\n }\n}\n","import { consola } from 'consola'\n\ninterface StreamMessage {\n type: string\n message?: {\n content?: Array<{\n type: string\n name?: string\n text?: string\n input?: Record<string, unknown>\n }>\n }\n result?: string\n total_cost_usd?: number\n}\n\ninterface ProgressResult {\n output: string\n turns: number\n cost: number | null\n}\n\n/**\n * Streaming progress parser for Claude stream-json output.\n * Processes lines incrementally as they arrive, logging progress to stderr.\n */\nexport class StreamProgressParser {\n private turns = 0\n private cost: number | null = null\n private resultText = ''\n private buffer = ''\n\n constructor(private iteration: number) {}\n\n /**\n * Feed a raw chunk of data. Internally buffers and processes complete lines.\n */\n processChunk(chunk: Buffer | string): void {\n this.buffer += chunk.toString()\n const lines = this.buffer.split('\\n')\n this.buffer = lines.pop() ?? '' // keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.trim()) this.processLine(line)\n }\n }\n\n /**\n * Flush any remaining buffered data. Call after the process exits.\n */\n flush(): void {\n if (this.buffer.trim()) {\n this.processLine(this.buffer)\n this.buffer = ''\n }\n }\n\n getResult(): ProgressResult {\n return { output: this.resultText, turns: this.turns, cost: this.cost }\n }\n\n private processLine(line: string): void {\n let message: StreamMessage\n try {\n message = JSON.parse(line) as StreamMessage\n } catch {\n return\n }\n\n switch (message.type) {\n case 'system':\n case 'user':\n break\n\n case 'assistant': {\n this.turns++\n consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`)\n\n if (message.message?.content) {\n const toolDetails = message.message.content\n .filter((c) => c.type === 'tool_use')\n .map((c) => {\n const name = c.name || ''\n const input = (c.input || {}) as Record<string, string>\n\n if (['Read', 'Write', 'Edit'].includes(name)) {\n const filePath = input.file_path || ''\n return `${name} ${filePath.split('/').pop()}`\n }\n if (name === 'Glob') return `${name} ${input.pattern || ''}`\n if (name === 'Grep') return `${name} ${input.pattern || ''}`\n if (name === 'Bash') return `${name} ${(input.command || '').slice(0, 80)}`\n return name\n })\n\n if (toolDetails.length > 0) {\n consola.info(` ${toolDetails.join('\\n ')}`)\n }\n\n const text = message.message.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text || '')\n .join('')\n\n if (text) {\n consola.info(text)\n }\n }\n break\n }\n\n case 'result': {\n this.resultText = message.result || ''\n this.cost = message.total_cost_usd ?? null\n consola.info('---')\n consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? '?'}`)\n break\n }\n\n default: {\n if (message.type) {\n consola.debug(`[debug] unrecognized type: ${message.type}`)\n }\n break\n }\n }\n }\n}\n\n/**\n * Parse Claude stream-json output into human-readable progress.\n * Returns the final result text and metadata.\n */\nexport function parseStreamOutput(rawOutput: string, iteration: number): ProgressResult {\n const parser = new StreamProgressParser(iteration)\n parser.processChunk(rawOutput)\n parser.flush()\n return parser.getResult()\n}\n","import { readFile, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\n\nconst RALPH_DIR = '.ralph'\nconst PRD_FILE = join(RALPH_DIR, 'prd.json')\nconst PROGRESS_FILE = join(RALPH_DIR, 'progress.txt')\nconst LAST_BRANCH_FILE = join(RALPH_DIR, '.last-branch')\nconst ARCHIVE_DIR = join(RALPH_DIR, 'archive')\n\nexport async function archiveIfBranchChanged(): Promise<void> {\n if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {\n await trackCurrentBranch()\n return\n }\n\n let currentBranch: string\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n currentBranch = prd.branchName || ''\n } catch {\n return\n }\n\n let lastBranch: string\n try {\n lastBranch = (await readFile(LAST_BRANCH_FILE, 'utf8')).trim()\n } catch {\n lastBranch = ''\n }\n\n if (!currentBranch || !lastBranch || currentBranch === lastBranch) {\n await trackCurrentBranch()\n return\n }\n\n // Archive the previous run\n const date = new Date().toISOString().split('T')[0]\n const folderName = lastBranch.replace(/^ralph\\//, '')\n const archiveFolder = join(ARCHIVE_DIR, `${date}-${folderName}`)\n\n consola.info(`Archiving previous run: ${lastBranch}`)\n await mkdir(archiveFolder, { recursive: true })\n\n if (existsSync(PRD_FILE)) {\n await cp(PRD_FILE, join(archiveFolder, 'prd.json'))\n }\n if (existsSync(PROGRESS_FILE)) {\n await cp(PROGRESS_FILE, join(archiveFolder, 'progress.txt'))\n }\n consola.info(`Archived to: ${archiveFolder}`)\n\n // Reset progress file\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n\n await trackCurrentBranch()\n}\n\nasync function trackCurrentBranch(): Promise<void> {\n if (!existsSync(PRD_FILE)) return\n\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n const branch = prd.branchName\n if (branch) {\n await mkdir(RALPH_DIR, { recursive: true })\n await writeFile(LAST_BRANCH_FILE, branch + '\\n')\n }\n } catch {\n // PRD doesn't exist or is invalid, skip\n }\n}\n\nexport async function ensureProgressFile(): Promise<void> {\n await mkdir(RALPH_DIR, { recursive: true })\n if (!existsSync(PROGRESS_FILE)) {\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n }\n}\n","import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { consola } from 'consola'\nimport { execAgent } from '../container/exec.js'\nimport { StreamProgressParser } from './progress.js'\nimport { archiveIfBranchChanged, ensureProgressFile } from './archive.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface RunOptions {\n iterations: number\n model?: string\n verbose?: boolean\n}\n\ninterface IterationStats {\n iteration: number\n turns: number\n cost: number | null\n}\n\nfunction buildAgentArgs(agent: string, options: { model: string; verbose: boolean }): string[] {\n if (agent === 'claude') {\n const args = ['--dangerously-skip-permissions', '--model', options.model, '--print']\n if (options.verbose) {\n args.push('--verbose', '--output-format', 'stream-json')\n }\n return args\n }\n // Future: support other agents\n return []\n}\n\nfunction formatCost(cost: number | null): string {\n return cost != null ? `$${cost.toFixed(4)}` : '$?'\n}\n\nfunction logSummaryTable(stats: IterationStats[]): void {\n const totalTurns = stats.reduce((sum, s) => sum + s.turns, 0)\n const costs = stats.map((s) => s.cost)\n const totalCost = costs.every((c): c is number => c != null)\n ? costs.reduce((sum, c) => sum + c, 0)\n : null\n\n const rows = stats.map((s) => [String(s.iteration), String(s.turns), formatCost(s.cost)])\n const totalRow = ['Total', String(totalTurns), formatCost(totalCost)]\n\n const headers = ['Iteration', 'Turns', 'Cost']\n const allRows = [...rows, totalRow]\n const widths = headers.map((h, col) => {\n const cellLengths = allRows.map((r) => (r[col] ?? '').length)\n return Math.max(h.length, ...cellLengths)\n })\n\n const formatRow = (row: string[]) => row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(' ')\n const separator = widths.map((w) => '-'.repeat(w)).join(' ')\n\n const lines = [\n '',\n formatRow(headers),\n separator,\n ...rows.map(formatRow),\n separator,\n formatRow(totalRow),\n ]\n\n consola.info(lines.join('\\n'))\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n })\n}\n\nexport async function runLoop(config: ResolvedConfig, options: RunOptions): Promise<void> {\n const model = options.model || config.defaults.model\n const verbose = options.verbose ?? config.defaults.verbose\n const { sleepBetweenMs, completionSignal } = config.defaults\n\n if (!existsSync('/.dockerenv')) {\n consola.warn(\n 'It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).',\n )\n }\n\n await ensureProgressFile()\n\n const abortController = new AbortController()\n const { signal } = abortController\n\n // First Ctrl+C: graceful cleanup (abort agent + container process).\n // Second Ctrl+C: force exit (process.once removes the handler after first call,\n // so the default Node.js SIGINT behavior kicks in).\n const handleSignal = () => {\n consola.warn('\\nInterrupted. Cleaning up...')\n abortController.abort()\n }\n process.once('SIGINT', handleSignal)\n process.once('SIGTERM', handleSignal)\n\n consola.info(\n [\n `Starting Ralph`,\n `Agent: ${config.defaults.agent}`,\n `Model: ${model}`,\n `Verbose: ${verbose}`,\n `Iterations: ${options.iterations}`,\n ].join(' | '),\n )\n\n const iterationStats: IterationStats[] = []\n\n try {\n for (let i = 1; i <= options.iterations; i++) {\n if (signal.aborted) break\n\n consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`)\n\n await archiveIfBranchChanged()\n\n const promptContent = await readFile('.ralph-container/ralph-prompt.md', 'utf8')\n const agentArgs = buildAgentArgs(config.defaults.agent, { model, verbose })\n\n let finalOutput: string\n let turnsUsed = '?'\n\n try {\n if (verbose) {\n // Verbose: parse stream-json lines in real-time, show progress on stderr\n const parser = new StreamProgressParser(i)\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => parser.processChunk(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n parser.flush()\n const progress = parser.getResult()\n finalOutput = progress.output\n turnsUsed = String(progress.turns)\n iterationStats.push({ iteration: i, turns: progress.turns, cost: progress.cost })\n\n if (result.exitCode !== 0) {\n consola.warn(`Agent exited with code ${result.exitCode}`)\n }\n } else {\n // Non-verbose: stream both stdout and stderr to stderr in real-time\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => process.stderr.write(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n finalOutput = result.stdout\n }\n } catch (error) {\n consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`)\n finalOutput = ''\n }\n\n if (finalOutput.includes(completionSignal)) {\n consola.success(\n `All stories complete! Completed at iteration ${i} of ${options.iterations}`,\n )\n return\n }\n\n if (i < options.iterations) {\n consola.info(\n `Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`,\n )\n await sleep(sleepBetweenMs, signal)\n }\n }\n\n if (signal.aborted) {\n consola.info('Stopped.')\n process.exitCode = 130\n return\n }\n\n consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`)\n consola.warn('Check .ralph/progress.txt for status.')\n process.exitCode = 1\n } finally {\n if (verbose && iterationStats.length > 0) {\n logSummaryTable(iterationStats)\n }\n process.removeListener('SIGINT', handleSignal)\n process.removeListener('SIGTERM', handleSignal)\n }\n}\n","import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { runLoop } from '../loop/runner.js'\n\nexport default defineCommand({\n meta: {\n name: 'run',\n description: 'Execute the ralph iteration loop',\n },\n args: {\n iterations: {\n type: 'positional',\n description: 'Number of iterations to run (required)',\n required: true,\n },\n model: {\n type: 'string',\n description: 'Override default model',\n },\n verbose: {\n type: 'boolean',\n description: 'Enable verbose stream-json progress output',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const iterations = Number.parseInt(args.iterations as string, 10)\n\n if (Number.isNaN(iterations) || iterations < 1) {\n throw new Error('iterations must be a positive integer')\n }\n\n await runLoop(config, {\n iterations,\n model: args.model,\n verbose: args.verbose,\n })\n },\n})\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabis-ralph-loop",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for setting up and running Claude Ralph autonomous coding loops in Docker containers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "consola": "^3.4.2",
44
44
  "ejs": "^4.0.1",
45
45
  "execa": "^9.5.2",
46
- "universal-ai-config": "^1.7.0",
46
+ "universal-ai-config": "^1.9.0",
47
47
  "zod": "^4.3.6"
48
48
  },
49
49
  "devDependencies": {