#!/usr/bin/env node /** * Deterministic plan scaffolder for this repo. * * Creates `.kilo/plans/-.md` from `.kilo/templates/plan.md` so * every new plan is kilo-loop compatible by construction (contains `- [ ]` * stages). kilo-loop only discovers markdown plans with checkbox stages. * * Usage: * node tools/kilocode-loop/new-plan.mjs "My plan title" * node tools/kilocode-loop/new-plan.mjs "My plan title" --slug my-plan * node tools/kilocode-loop/new-plan.mjs "My plan title" --print * node tools/kilocode-loop/new-plan.mjs "My plan title" --force * * Options: * --slug filename slug (default: slugified title) * --epoch timestamp for the filename (default: now); useful for tests * --dir target directory (default: .kilo/plans) * --project

project root (default: git/cwd discovery upward) * --print print the plan to stdout instead of writing a file * --force overwrite an existing file * -h, --help show this help */ import fs from 'node:fs' import path from 'node:path' const DEFAULT_DIR = path.join('.kilo', 'plans') const TEMPLATE = path.join('.kilo', 'templates', 'plan.md') const CHECKBOX_RE = /^\s*[-*]\s+\[[ xX]\]/m /** * Built-in fallback template, used when the project has no * `.kilo/templates/plan.md`. Guarantees a kilo-loop-compatible plan everywhere. */ const DEFAULT_TEMPLATE = `# {{TITLE}} > **Status:** draft. ## Stages (kilo-loop) - [ ] Stage 1 — :: acceptance: - [ ] Stage 2 — :: acceptance: --- ## 0. Goal ## 1. Current state (verified facts) ## 2. Decisions | # | Decision | Choice | |---|----------|--------| | D1 | | | ## 3. Data model / contracts ## 4. Backend API ## 5. Frontend ## 6. Verification ## 7. Risks | Risk | Mitigation | |---|---| | | | ` function usage() { const txt = fs.readFileSync(new URL(import.meta.url), 'utf8') const block = txt.match(/\/\*\*([\s\S]*?)\*\//) if (block) console.log(block[1].replace(/^\s*\* ?/gm, '').trim()) } function fail(msg) { console.error(`new-plan: ${msg}`) process.exit(1) } function slugify(s) { return s .toLowerCase() .normalize('NFKD') .replace(/[^\p{L}\p{N}]+/gu, '-') .replace(/^-+|-+$/g, '') .slice(0, 80) || 'plan' } /** Walk up from `start` until a directory containing `.kilo` (or `.git`) is found. */ function findProject(start) { let dir = path.resolve(start) for (;;) { if (fs.existsSync(path.join(dir, '.kilo')) || fs.existsSync(path.join(dir, '.git'))) return dir const parent = path.dirname(dir) if (parent === dir) return path.resolve(start) dir = parent } } function parseArgs(argv) { const out = { title: [], slug: '', dir: DEFAULT_DIR, project: '', print: false, force: false, epoch: 0 } for (let i = 0; i < argv.length; i++) { const a = argv[i] if (a === '--slug') out.slug = argv[++i] ?? '' else if (a === '--dir') out.dir = argv[++i] ?? DEFAULT_DIR else if (a === '--project') out.project = argv[++i] ?? '' else if (a === '--epoch') out.epoch = Number(argv[++i]) || 0 else if (a === '--print') out.print = true else if (a === '--force') out.force = true else if (a === '-h' || a === '--help') { usage() process.exit(0) } else out.title.push(a) } out.title = out.title.join(' ').trim() return out } const args = parseArgs(process.argv.slice(2)) if (!args.title) { usage() fail('a plan title is required') } const project = args.project ? path.resolve(args.project) : findProject(process.cwd()) const templatePath = path.join(project, TEMPLATE) let body if (fs.existsSync(templatePath)) { body = fs.readFileSync(templatePath, 'utf8') } else { console.error(`new-plan: no template at ${rel(templatePath)} — using the built-in default`) body = DEFAULT_TEMPLATE } if (!body.includes('{{TITLE}}')) fail(`template is missing the {{TITLE}} placeholder`) const slug = slugify(args.slug || args.title) const epoch = args.epoch || Date.now() const fileName = `${epoch}-${slug}.md` const targetDir = path.resolve(project, args.dir) const targetPath = path.join(targetDir, fileName) body = body .replaceAll('{{TITLE}}', args.title) .replaceAll('{{SLUG}}', slug) .replaceAll('{{EPOCH}}', String(epoch)) .replaceAll('{{DATE}}', new Date(epoch).toISOString()) // Defensive: keep plans discoverable even if the template is edited badly. if (!CHECKBOX_RE.test(body)) { body += `\n## Stages (kilo-loop)\n\n- [ ] Complete the plan :: acceptance: goal achieved\n` console.error('new-plan: template had no checkbox stages — injected a default Stages section') } if (args.print) { process.stdout.write(body) process.exit(0) } fs.mkdirSync(targetDir, { recursive: true }) if (fs.existsSync(targetPath) && !args.force) fail(`already exists: ${rel(targetPath)} (use --force)`) fs.writeFileSync(targetPath, body) console.log(rel(targetPath)) function rel(abs) { return path.relative(project, abs).split(path.sep).join('/') }