self-bootstrap.js
10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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=<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);
});