self-bootstrap.js 10.6 KB
#!/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=<rev> 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);
});