Skip to content

Instantly share code, notes, and snippets.

@mlshv
Created January 14, 2026 16:49
Show Gist options
  • Select an option

  • Save mlshv/1ea771d114cc00356b4845ba477959a7 to your computer and use it in GitHub Desktop.

Select an option

Save mlshv/1ea771d114cc00356b4845ba477959a7 to your computer and use it in GitHub Desktop.
My Ralph Wiggum Loop
#!/usr/bin/env node
import { spawn } from 'child_process'
import fs from 'fs'
// tools that run without prompting
const ALLOWED_TOOLS = [
'Read',
'Write',
'Edit',
'Glob',
'LS',
'Grep',
'Bash(find:*)',
'Bash(tree:*)',
'Bash(git status:*)',
'Bash(git log:*)',
'Bash(git diff:*)',
'Bash(pnpm add:*)',
'Bash(pnpm install:*)',
'Bash(pnpm remove:*)'
]
// tools completely blocked
const DISALLOWED_TOOLS = [
'Bash(git push:*)',
'Bash(git push --force:*)',
'Bash(rm -rf:*)',
'Bash(rm -r:*)'
]
// --- Parse CLI args ---
const parseArgs = () => {
const args = process.argv.slice(2)
const result = { prompt: null, plan: null }
for (let i = 0; i < args.length; i++) {
if (args[i] === '--prompt' && args[i + 1]) result.prompt = args[++i]
else if (args[i] === '--plan' && args[i + 1]) result.plan = args[++i]
}
return result
}
const showUsage = () => {
console.log(`Usage: loop.js --prompt <prompt-file> --plan <plan-file>
Example:
node .claude/loop.js --prompt prompt.md --plan plan.md
`)
process.exit(1)
}
const { prompt: PROMPT_FILE, plan: PLAN_FILE } = parseArgs()
if (!PROMPT_FILE || !PLAN_FILE) showUsage()
// --- Configuration ---
const MAX_ITERATIONS = 30
const DONE_MARKER = 'STATUS: DONE'
// --- Utils ---
const orange = (text) => `\x1b[38;5;208m${text}\x1b[0m`
const log = (msg) => console.log(`${orange('✦')} ${msg}`)
async function runIteration(iteration) {
log(`Iteration ${iteration}/${MAX_ITERATIONS}`)
// 1. Check Exit Condition
try {
const planContent = fs.readFileSync(PLAN_FILE, 'utf8')
if (planContent.includes(DONE_MARKER)) {
log(`Found "${DONE_MARKER}" in ${PLAN_FILE}. Exiting.`)
process.exit(0)
}
} catch (err) {
if (err.code !== 'ENOENT') throw err
// If plan.md doesn't exist yet, we continue (Claude might create it)
}
// 2. Prepare Context
// We read prompt.md dynamically each time so you can edit it mid-flight if needed
if (!fs.existsSync(PROMPT_FILE)) {
log(`Error: ${PROMPT_FILE} not found.`)
process.exit(1)
}
// We just cat the file into the process, but we also need to append
// the context about the plan so Claude knows what to do.
// The user asked to "pass prompt.md directly", but for the loop to work
// (Ralph style), we usually need to inject the Plan state too.
// However, strict adherence to "pass prompt.md directly":
const promptContent = fs.readFileSync(PROMPT_FILE, 'utf8')
// NOTE: If prompt.md doesn't reference plan.md, Claude won't know to check it.
// Ensure your prompt.md includes: "Check plan.md, do next step, update plan.md"
// 3. Spawn Claude
return new Promise((resolve, reject) => {
const args = [
'-p',
promptContent,
...ALLOWED_TOOLS.flatMap((t) => ['--allowedTools', t]),
...DISALLOWED_TOOLS.flatMap((t) => ['--disallowedTools', t])
]
const claude = spawn('claude', args, {
stdio: ['inherit', 'inherit', 'inherit']
})
claude.on('close', (code) => {
if (code !== 0) {
log(`Claude exited with code ${code}`)
// We generally continue unless it's a fatal error,
// but a non-zero exit might just mean it failed a task.
}
resolve()
})
claude.on('error', (err) => {
log(`Failed to start Claude: ${err.message}`)
reject(err)
})
})
}
async function main() {
log(`Starting Autonomous Loop`)
log(`Target: ${PROMPT_FILE}`)
for (let i = 1; i <= MAX_ITERATIONS; i++) {
await runIteration(i)
// Safety pause to let you Ctrl+C if things go haywire
await new Promise((r) => setTimeout(r, 2000))
}
log(`Max iterations (${MAX_ITERATIONS}) reached.`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

(here goes the text of the plan - bullet points with checkboxes that agent is checking as it goes through the plan)

STATUS: NOT STARTED

study specs/export.md study specs/export-update-plan.md and pick the most important thing to do

IMPORTANT:

  • update the plan when the task is done
  • create property based tests or unit tests (which ever is best)
  • after making the changes to the files run tests
  • set status to DONE in the implementation plan everything is finished (STATUS: DONE)
  • only use STATUS: DONE once at the end of the plan when the entire plan is done (each and every task from the plan is marked as completed)
@mlshv
Copy link
Author

mlshv commented Jan 14, 2026

put loop.js it the project's .claude/ folder and run:

node .claude/loop.js --prompt prompt.md --plan plan.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment