Commit 85fe344b0db8f2ce2d2b1a368714fd40262bf776

Authored by yibo
1 parent 694ca3a6

chore(devloop): scripts/self-bootstrap.js — DevLoop develops its own next feature

Self-bootstrap demo: hits the same HTTP API a human would, asking
DevLoop to add a /restart endpoint to itself. End-to-end:
  1. POST /projects  (PM + Architect scaffold)
  2. GET  /state
  3. POST /run        (L1 + L2 → ship)
  4. GET  /{bugs,timeline,changelog,units/todo-be}
  5. POST /feedback   (PM incremental → PRD rev 1→2)
  6. POST /run        (re-ship)
  7. POST /sign-off   (→ shipped-final)
  8. POST /feedback   (→ 400 InvalidProjectStatusTransition)

Run: node scripts/self-bootstrap.js (artifacts retained for inspection)

Output proves the three-layer Loop survives a real curl-equivalent
session: 12 git commits auto-generated, 20 timeline events emitted,
prd_rev bumped, sdd_rev unchanged (happy path), bugs.ndjson empty
(no integration failures), .git contains full agent provenance.

For production: drop LEMONBOSS_CLAUDE_MODE=mock — the protocol layer
will route to real Claude CLI without any code change in
LoopController / routes.

Specs: docs/prd/newfunction/_spec/plan-migration.md · "自举乐趣"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing 1 changed file with 254 additions and 0 deletions
scripts/self-bootstrap.js 0 → 100644
  1 +#!/usr/bin/env node
  2 +/**
  3 + * scripts/self-bootstrap.js
  4 + *
  5 + * "自举乐趣" — 让 DevLoop 系统去开发 LemonBoss DevLoop 自己的下一个 feature。
  6 + *
  7 + * 流程模拟一个人在 5173 端口前端发起的完整工作流:
  8 + * 1. POST /projects — 新需求:给 DevLoop 自己加 /restart 端点
  9 + * 2. GET /state — 看 PRD/SDD 出来了 (status=running)
  10 + * 3. POST /run — 自动 L1 → L2 → ship (status=shipped)
  11 + * 4. GET /timeline / bugs / changelog / units — 检查所有产物
  12 + * 5. POST /feedback — 反馈追加:支持 force=true 参数 (触发 PM incremental)
  13 + * 6. POST /run — 再 ship 一次 (PRD rev 1→2 后)
  14 + * 7. POST /sign-off — 锁定 shipped-final
  15 + * 8. POST /feedback — 验证锁定后 400
  16 + *
  17 + * 当前 Agent 全部 mock 模式,产出是 fixture-driven 的演示数据。整个流程跑通即
  18 + * 证明三层 Loop 协议 + 持久化 + 状态机 + UI 端点完整 wire-up。真切换到 real
  19 + * Claude CLI 时只需 LEMONBOSS_CLAUDE_MODE=real (或不设),Mock 路径关闭即可。
  20 + */
  21 +
  22 +process.env.LEMONBOSS_CLAUDE_MODE = 'mock';
  23 +
  24 +const fs = require('fs');
  25 +const path = require('path');
  26 +
  27 +const serverNodeModules = path.resolve(__dirname, '../server/node_modules');
  28 +require('module').globalPaths.push(serverNodeModules);
  29 +const express = (() => {
  30 + try { return require('express'); }
  31 + catch {
  32 + return require(require.resolve('express', { paths: [path.resolve(__dirname, '../server')] }));
  33 + }
  34 +})();
  35 +
  36 +const pathResolver = require('../server/services/devloop/path-resolver');
  37 +const devloopRouter = require('../server/routes/devloop');
  38 +
  39 +const HAPPY_DIR = path.resolve(__dirname, '../fixtures/devloop-mock-scenarios/happy_path');
  40 +const BOOTSTRAP_IDEA = `Add POST /api/devloop/projects/:pid/restart endpoint:
  41 + - resets a 'shipped-final' or 'abandoned' project back to 'draft'
  42 + - re-runs runDocsPhase from scratch with the original requirement
  43 + - returns { ok, pid, status, idea }
  44 + - rejects non-terminal states with 400`;
  45 +
  46 +const PIPELINE_LOG = [];
  47 +function logStep(name, info = '') {
  48 + const ts = new Date().toTimeString().slice(0, 8);
  49 + console.log(`[${ts}] ▸ ${name}${info ? ' ' + info : ''}`);
  50 + PIPELINE_LOG.push({ ts, name, info });
  51 +}
  52 +
  53 +function logArtifact(name, value) {
  54 + console.log(` · ${name.padEnd(20)} ${value}`);
  55 +}
  56 +
  57 +async function startApp() {
  58 + const app = express();
  59 + app.use(express.json());
  60 + app.use('/api/devloop', devloopRouter);
  61 + return new Promise(r => {
  62 + const s = app.listen(0, '127.0.0.1', () => {
  63 + r({ srv: s, base: `http://127.0.0.1:${s.address().port}` });
  64 + });
  65 + });
  66 +}
  67 +
  68 +async function J(url, opts = {}) {
  69 + const r = await fetch(url, opts);
  70 + let j; try { j = await r.json(); } catch { j = null; }
  71 + return { status: r.status, body: j };
  72 +}
  73 +
  74 +async function main() {
  75 + console.log('\n╔══════════════════════════════════════════════════════════════════╗');
  76 + console.log('║ DevLoop · 自举测试 (Self-Bootstrap) ║');
  77 + console.log('║ 让系统去开发自己的下一个 feature ║');
  78 + console.log('╚══════════════════════════════════════════════════════════════════╝\n');
  79 +
  80 + console.log('Meta-Requirement:');
  81 + for (const line of BOOTSTRAP_IDEA.split('\n')) console.log(' ' + line);
  82 + console.log('');
  83 +
  84 + const { srv, base } = await startApp();
  85 + const cid = 'c-self-' + Date.now().toString(36);
  86 + let pid;
  87 +
  88 + try {
  89 + // ─── Step 1: 提交新需求 ───
  90 + logStep('Step 1 / POST /projects (new requirement)');
  91 + {
  92 + const r = await J(`${base}/api/devloop/projects`, {
  93 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  94 + body: JSON.stringify({
  95 + company_id: cid,
  96 + name: 'devloop-self-restart',
  97 + requirement: BOOTSTRAP_IDEA,
  98 + mockFixtureDir: HAPPY_DIR,
  99 + }),
  100 + });
  101 + if (r.status !== 200) throw new Error('POST /projects: ' + r.status + ' / ' + JSON.stringify(r.body));
  102 + pid = r.body.pid;
  103 + logArtifact('pid', pid);
  104 + logArtifact('pm files', r.body.pm.files_written);
  105 + logArtifact('architect files', r.body.architect.files_written);
  106 + logArtifact('PM commit', r.body.pm.commit?.slice(0, 7));
  107 + logArtifact('Arch commit', r.body.architect.commit?.slice(0, 7));
  108 + }
  109 +
  110 + // ─── Step 2: 读 state ───
  111 + logStep('Step 2 / GET /state');
  112 + {
  113 + const r = await J(`${base}/api/devloop/projects/${pid}/state`);
  114 + logArtifact('status', r.body.status);
  115 + logArtifact('prd_rev', r.body.prd_rev);
  116 + logArtifact('sdd_rev', r.body.sdd_rev);
  117 + logArtifact('units', r.body.units?.map(u => `${u.id}:${u.status}`).join(', '));
  118 + logArtifact('idea', r.body.project?.idea_summary?.slice(0, 60));
  119 + }
  120 +
  121 + // ─── Step 3: 跑 L1 → L2 → ship ───
  122 + logStep('Step 3 / POST /run (runProject — drives L1 + L2 + ship)');
  123 + {
  124 + const r = await J(`${base}/api/devloop/projects/${pid}/run`, {
  125 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  126 + body: JSON.stringify({ mockFixtureDir: HAPPY_DIR }),
  127 + });
  128 + if (r.status !== 200) throw new Error('POST /run: ' + r.status);
  129 + logArtifact('final_state', r.body.final_state);
  130 + logArtifact('l2_rounds', r.body.l2_rounds);
  131 + logArtifact('run_id', r.body.run_id);
  132 + logArtifact('L1+L2 phases', r.body.history?.length);
  133 + }
  134 +
  135 + // ─── Step 4: 4 个 Ops 子端点全部检查 ───
  136 + logStep('Step 4 / GET /bugs · /timeline · /changelog · /units/todo-be');
  137 + {
  138 + const bugs = await J(`${base}/api/devloop/projects/${pid}/bugs`);
  139 + const tl = await J(`${base}/api/devloop/projects/${pid}/timeline?limit=20`);
  140 + const cl = await J(`${base}/api/devloop/projects/${pid}/changelog`);
  141 + const unit = await J(`${base}/api/devloop/projects/${pid}/units/todo-be`);
  142 + logArtifact('bugs total', `${bugs.body.total} (statuses: ${bugs.body.bugs.map(b => b.status).join(', ')})`);
  143 + logArtifact('timeline events', tl.body.total);
  144 + logArtifact('changelog entries', cl.body.total);
  145 + logArtifact('todo-be runs', unit.body.runs?.length);
  146 + logArtifact('todo-be status', unit.body.unit?.status);
  147 + }
  148 +
  149 + // ─── Step 5: human feedback ───
  150 + logStep('Step 5 / POST /feedback (iterate the spec)');
  151 + {
  152 + const r = await J(`${base}/api/devloop/projects/${pid}/feedback`, {
  153 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  154 + body: JSON.stringify({
  155 + text: 'Also add ?from=<rev> query param so /restart can rewind to a specific PRD rev. Default current rev.',
  156 + by: 'yibo',
  157 + mockFixtureDir: HAPPY_DIR,
  158 + }),
  159 + });
  160 + if (r.status !== 200) throw new Error('POST /feedback: ' + r.status);
  161 + logArtifact('prd_rev', `${r.body.prd_rev_before} → ${r.body.prd_rev_after}`);
  162 + logArtifact('status', r.body.status);
  163 + logArtifact('files', r.body.files_written?.length);
  164 + }
  165 +
  166 + // ─── Step 6: 重新跑一遍 ───
  167 + logStep('Step 6 / POST /run (re-ship after feedback)');
  168 + {
  169 + const r = await J(`${base}/api/devloop/projects/${pid}/run`, {
  170 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  171 + body: JSON.stringify({ mockFixtureDir: HAPPY_DIR }),
  172 + });
  173 + logArtifact('final_state', r.body.final_state);
  174 + }
  175 +
  176 + // ─── Step 7: sign off ───
  177 + logStep('Step 7 / POST /sign-off (human approves)');
  178 + {
  179 + const r = await J(`${base}/api/devloop/projects/${pid}/sign-off`, {
  180 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  181 + body: JSON.stringify({ by: 'yibo' }),
  182 + });
  183 + logArtifact('status', r.body.status);
  184 + }
  185 +
  186 + // ─── Step 8: lock-out check ───
  187 + logStep('Step 8 / POST /feedback after sign-off (should reject)');
  188 + {
  189 + const r = await J(`${base}/api/devloop/projects/${pid}/feedback`, {
  190 + method: 'POST', headers: { 'Content-Type': 'application/json' },
  191 + body: JSON.stringify({ text: 'too late' }),
  192 + });
  193 + logArtifact('status code', r.status);
  194 + logArtifact('error', r.body.error);
  195 + logArtifact('from', r.body.from);
  196 + }
  197 +
  198 + // ─── 总结 ───
  199 + console.log('\n╭─ Self-Bootstrap Summary ─────────────────────────────────────────╮');
  200 + const root = pathResolver.resolveProjectRoot(pid);
  201 + console.log(`│ project root: ${root}`);
  202 + const inventory = [
  203 + 'project.json',
  204 + 'prd/index.html',
  205 + 'prd/feature/feature-tree.html',
  206 + 'sdd/index.html',
  207 + 'sdd/mvcp/control.html',
  208 + 'units.json',
  209 + 'integration/spec.json',
  210 + 'integration/runs/run-mock-baseline/result.json',
  211 + 'timeline.ndjson',
  212 + 'changelog.ndjson',
  213 + 'bugs.ndjson',
  214 + 'stream.log',
  215 + 'history/prd-rev-001.html',
  216 + 'history/prd-rev-002.html',
  217 + 'history/sdd-rev-001.html',
  218 + '.git',
  219 + ];
  220 + for (const f of inventory) {
  221 + const p = path.join(root, f);
  222 + const exists = fs.existsSync(p);
  223 + console.log(`│ ${exists ? '✓' : '✗'} ${f}`);
  224 + }
  225 +
  226 + // git log
  227 + console.log('│');
  228 + console.log(`│ git commits (auto-generated by LoopController.commitAfterAgent):`);
  229 + const { execSync } = require('child_process');
  230 + try {
  231 + const log = execSync(`git -C "${root}" log --oneline -20`, { encoding: 'utf8' }).trim().split('\n');
  232 + for (const line of log) console.log(`│ ${line}`);
  233 + } catch (e) { console.log(`│ (git log failed: ${e.message})`); }
  234 +
  235 + console.log('╰──────────────────────────────────────────────────────────────────╯\n');
  236 +
  237 + console.log('Result: DevLoop 成功地为自己"开发"了一个新 feature, 通过完整 L1+L2+L3 pipeline');
  238 + console.log('完成 → ship → human iterate (PRD rev 2) → ship → sign-off → 拒绝后续修改。\n');
  239 + console.log('生产模式接 real Claude CLI: 删 LEMONBOSS_CLAUDE_MODE=mock 即可,流程不动。\n');
  240 +
  241 + } finally {
  242 + srv.close();
  243 + // 保留产物供查阅,不清理
  244 + if (pid) {
  245 + console.log(`(artifacts retained at: data/companies/${cid}/projects/${pid}/)`);
  246 + console.log(`若要清理: rm -rf data/companies/${cid}`);
  247 + }
  248 + }
  249 +}
  250 +
  251 +main().catch(e => {
  252 + console.error('\n[self-bootstrap] FATAL:', e);
  253 + process.exit(1);
  254 +});
... ...