#!/usr/bin/env node /** * scripts/self-bootstrap.js * * "自举乐趣" — 让 DevLoop 系统去开发 LemonBoss DevLoop 自己的下一个 feature。 * * 流程模拟一个人在 5173 端口前端发起的完整工作流: * 1. POST /projects — 新需求:给 DevLoop 自己加 /restart 端点 * 2. GET /state — 看 PRD/SDD 出来了 (status=running) * 3. POST /run — 自动 L1 → L2 → ship (status=shipped) * 4. GET /timeline / bugs / changelog / units — 检查所有产物 * 5. POST /feedback — 反馈追加:支持 force=true 参数 (触发 PM incremental) * 6. POST /run — 再 ship 一次 (PRD rev 1→2 后) * 7. POST /sign-off — 锁定 shipped-final * 8. POST /feedback — 验证锁定后 400 * * 当前 Agent 全部 mock 模式,产出是 fixture-driven 的演示数据。整个流程跑通即 * 证明三层 Loop 协议 + 持久化 + 状态机 + UI 端点完整 wire-up。真切换到 real * Claude CLI 时只需 LEMONBOSS_CLAUDE_MODE=real (或不设),Mock 路径关闭即可。 */ process.env.LEMONBOSS_CLAUDE_MODE = 'mock'; const fs = require('fs'); const path = require('path'); const serverNodeModules = path.resolve(__dirname, '../server/node_modules'); require('module').globalPaths.push(serverNodeModules); const express = (() => { try { return require('express'); } catch { return require(require.resolve('express', { paths: [path.resolve(__dirname, '../server')] })); } })(); const pathResolver = require('../server/services/devloop/path-resolver'); const devloopRouter = require('../server/routes/devloop'); const HAPPY_DIR = path.resolve(__dirname, '../fixtures/devloop-mock-scenarios/happy_path'); const BOOTSTRAP_IDEA = `Add POST /api/devloop/projects/:pid/restart endpoint: - resets a 'shipped-final' or 'abandoned' project back to 'draft' - re-runs runDocsPhase from scratch with the original requirement - returns { ok, pid, status, idea } - rejects non-terminal states with 400`; const PIPELINE_LOG = []; function logStep(name, info = '') { const ts = new Date().toTimeString().slice(0, 8); console.log(`[${ts}] ▸ ${name}${info ? ' ' + info : ''}`); PIPELINE_LOG.push({ ts, name, info }); } function logArtifact(name, value) { console.log(` · ${name.padEnd(20)} ${value}`); } async function startApp() { const app = express(); app.use(express.json()); app.use('/api/devloop', devloopRouter); return new Promise(r => { const s = app.listen(0, '127.0.0.1', () => { r({ srv: s, base: `http://127.0.0.1:${s.address().port}` }); }); }); } async function J(url, opts = {}) { const r = await fetch(url, opts); let j; try { j = await r.json(); } catch { j = null; } return { status: r.status, body: j }; } async function main() { console.log('\n╔══════════════════════════════════════════════════════════════════╗'); console.log('║ DevLoop · 自举测试 (Self-Bootstrap) ║'); console.log('║ 让系统去开发自己的下一个 feature ║'); console.log('╚══════════════════════════════════════════════════════════════════╝\n'); console.log('Meta-Requirement:'); for (const line of BOOTSTRAP_IDEA.split('\n')) console.log(' ' + line); console.log(''); const { srv, base } = await startApp(); const cid = 'c-self-' + Date.now().toString(36); let pid; try { // ─── Step 1: 提交新需求 ─── logStep('Step 1 / POST /projects (new requirement)'); { const r = await J(`${base}/api/devloop/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: cid, name: 'devloop-self-restart', requirement: BOOTSTRAP_IDEA, mockFixtureDir: HAPPY_DIR, }), }); if (r.status !== 200) throw new Error('POST /projects: ' + r.status + ' / ' + JSON.stringify(r.body)); pid = r.body.pid; logArtifact('pid', pid); logArtifact('pm files', r.body.pm.files_written); logArtifact('architect files', r.body.architect.files_written); logArtifact('PM commit', r.body.pm.commit?.slice(0, 7)); logArtifact('Arch commit', r.body.architect.commit?.slice(0, 7)); } // ─── Step 2: 读 state ─── logStep('Step 2 / GET /state'); { const r = await J(`${base}/api/devloop/projects/${pid}/state`); logArtifact('status', r.body.status); logArtifact('prd_rev', r.body.prd_rev); logArtifact('sdd_rev', r.body.sdd_rev); logArtifact('units', r.body.units?.map(u => `${u.id}:${u.status}`).join(', ')); logArtifact('idea', r.body.project?.idea_summary?.slice(0, 60)); } // ─── Step 3: 跑 L1 → L2 → ship ─── logStep('Step 3 / POST /run (runProject — drives L1 + L2 + ship)'); { const r = await J(`${base}/api/devloop/projects/${pid}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mockFixtureDir: HAPPY_DIR }), }); if (r.status !== 200) throw new Error('POST /run: ' + r.status); logArtifact('final_state', r.body.final_state); logArtifact('l2_rounds', r.body.l2_rounds); logArtifact('run_id', r.body.run_id); logArtifact('L1+L2 phases', r.body.history?.length); } // ─── Step 4: 4 个 Ops 子端点全部检查 ─── logStep('Step 4 / GET /bugs · /timeline · /changelog · /units/todo-be'); { const bugs = await J(`${base}/api/devloop/projects/${pid}/bugs`); const tl = await J(`${base}/api/devloop/projects/${pid}/timeline?limit=20`); const cl = await J(`${base}/api/devloop/projects/${pid}/changelog`); const unit = await J(`${base}/api/devloop/projects/${pid}/units/todo-be`); logArtifact('bugs total', `${bugs.body.total} (statuses: ${bugs.body.bugs.map(b => b.status).join(', ')})`); logArtifact('timeline events', tl.body.total); logArtifact('changelog entries', cl.body.total); logArtifact('todo-be runs', unit.body.runs?.length); logArtifact('todo-be status', unit.body.unit?.status); } // ─── Step 5: human feedback ─── logStep('Step 5 / POST /feedback (iterate the spec)'); { const r = await J(`${base}/api/devloop/projects/${pid}/feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Also add ?from= query param so /restart can rewind to a specific PRD rev. Default current rev.', by: 'yibo', mockFixtureDir: HAPPY_DIR, }), }); if (r.status !== 200) throw new Error('POST /feedback: ' + r.status); logArtifact('prd_rev', `${r.body.prd_rev_before} → ${r.body.prd_rev_after}`); logArtifact('status', r.body.status); logArtifact('files', r.body.files_written?.length); } // ─── Step 6: 重新跑一遍 ─── logStep('Step 6 / POST /run (re-ship after feedback)'); { const r = await J(`${base}/api/devloop/projects/${pid}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mockFixtureDir: HAPPY_DIR }), }); logArtifact('final_state', r.body.final_state); } // ─── Step 7: sign off ─── logStep('Step 7 / POST /sign-off (human approves)'); { const r = await J(`${base}/api/devloop/projects/${pid}/sign-off`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ by: 'yibo' }), }); logArtifact('status', r.body.status); } // ─── Step 8: lock-out check ─── logStep('Step 8 / POST /feedback after sign-off (should reject)'); { const r = await J(`${base}/api/devloop/projects/${pid}/feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'too late' }), }); logArtifact('status code', r.status); logArtifact('error', r.body.error); logArtifact('from', r.body.from); } // ─── 总结 ─── console.log('\n╭─ Self-Bootstrap Summary ─────────────────────────────────────────╮'); const root = pathResolver.resolveProjectRoot(pid); console.log(`│ project root: ${root}`); const inventory = [ 'project.json', 'prd/index.html', 'prd/feature/feature-tree.html', 'sdd/index.html', 'sdd/mvcp/control.html', 'units.json', 'integration/spec.json', 'integration/runs/run-mock-baseline/result.json', 'timeline.ndjson', 'changelog.ndjson', 'bugs.ndjson', 'stream.log', 'history/prd-rev-001.html', 'history/prd-rev-002.html', 'history/sdd-rev-001.html', '.git', ]; for (const f of inventory) { const p = path.join(root, f); const exists = fs.existsSync(p); console.log(`│ ${exists ? '✓' : '✗'} ${f}`); } // git log console.log('│'); console.log(`│ git commits (auto-generated by LoopController.commitAfterAgent):`); const { execSync } = require('child_process'); try { const log = execSync(`git -C "${root}" log --oneline -20`, { encoding: 'utf8' }).trim().split('\n'); for (const line of log) console.log(`│ ${line}`); } catch (e) { console.log(`│ (git log failed: ${e.message})`); } console.log('╰──────────────────────────────────────────────────────────────────╯\n'); console.log('Result: DevLoop 成功地为自己"开发"了一个新 feature, 通过完整 L1+L2+L3 pipeline'); console.log('完成 → ship → human iterate (PRD rev 2) → ship → sign-off → 拒绝后续修改。\n'); console.log('生产模式接 real Claude CLI: 删 LEMONBOSS_CLAUDE_MODE=mock 即可,流程不动。\n'); } finally { srv.close(); // 保留产物供查阅,不清理 if (pid) { console.log(`(artifacts retained at: data/companies/${cid}/projects/${pid}/)`); console.log(`若要清理: rm -rf data/companies/${cid}`); } } } main().catch(e => { console.error('\n[self-bootstrap] FATAL:', e); process.exit(1); });