diff --git a/scripts/self-bootstrap.js b/scripts/self-bootstrap.js new file mode 100644 index 0000000..880911c --- /dev/null +++ b/scripts/self-bootstrap.js @@ -0,0 +1,254 @@ +#!/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); +});