#!/usr/bin/env node /** * scripts/real-pm-smoke.js * * 最小验证: 真 claude CLI 跑一次 PM Agent,看 ClaudeCliProtocol + * --append-system-prompt + 工作树 mtime diff 这条 real 链路是否 * 端到端能用。预算 $0.50 cap。 * * 期望: * ① claude 进程 spawn 成功 * ② Agent 输出非空 (result_text 有内容) * ③ files_written 至少 ≥ 1 (Agent 调用 Write 工具写了文件) * ④ git 有新 commit * * NOTE: 真 LLM 输出不可控;PM 可能不完全照 prompt.md 跑全套 11 章节。 * 只要它写了任意 prd/* 文件就算 wire-up 通。 */ delete process.env.LEMONBOSS_CLAUDE_MODE; // 显式关闭 mock const fs = require('fs'); const path = require('path'); const lc = require('../server/src/core/devloop/loop-controller'); const pr = require('../server/services/devloop/path-resolver'); // --max-budget-usd 仅在 --print 模式可用,与我们的 stream-json 长连接互斥 // → 用进程超时 (5min) + 一次性单 agent 调用兜底 const HARD_TIMEOUT_MS = 5 * 60 * 1000; const cleanup = []; async function main() { console.log('\n┌─ Real Claude CLI · PM Smoke ────────────────────'); console.log(`│ Hard timeout: 5min (no maxBudgetUsd — only --print mode)`); console.log(`│ Model: claude default`); console.log('└──────────────────────────────────────────────────\n'); const cid = 'c-real-pm-' + Date.now().toString(36); cleanup.push(cid); const proj = await lc.createProject({ company_id: cid, mode: 'sandbox', idea: 'real PM smoke: minimal TODO API + Vue UI', }); console.log(`pid = ${proj.pid}`); console.log(`root = ${pr.resolveProjectRoot(proj.pid)}\n`); const userPrompt = [ 'Mode: init', 'Requirement: A minimal TODO list — POST /todos (add) + GET /todos (list).', '', '约束 (please obey for this smoke test):', ' • cwd 已经是项目根,直接相对路径写文件', ' • 至少写 prd/index.html 一个最小 HTML stub (含

...

...

)', ' • 不要尝试调 hash_contract / bootstrap_prd_site 等 LemonBoss 内部工具 — 它们在本 session 不可用', ' • 不要执行 npm install / git / curl 等 Bash 命令', ' • 完成后直接 stop, 不需要总结', '', '回 "DONE" 一词即可结束。', ].join('\n'); console.log('▸ calling devloop-pm via real claude CLI...'); console.log(` (system prompt: system/templates/devloop-pm/prompt.md, ~3KB)\n`); const startMs = Date.now(); let chunkCount = 0; const r = await Promise.race([ lc.callAgent(proj.pid, 'devloop-pm', userPrompt, { mock: false, onChunk: () => { chunkCount++; }, }), new Promise((_, rej) => setTimeout(() => rej(new Error('hard timeout 5min reached')), HARD_TIMEOUT_MS), ), ]); const elapsedSec = Math.round((Date.now() - startMs) / 1000); console.log('\n▸ result:'); console.log(` elapsed: ${elapsedSec}s`); console.log(` stream chunks: ${chunkCount}`); console.log(` result text: ${r.result_text.slice(0, 120).replace(/\n/g, ' ')}${r.result_text.length > 120 ? '…' : ''}`); console.log(` files_written: ${r.files_written.length}`); for (const f of r.files_written.slice(0, 10)) console.log(` · ${f}`); if (r.files_written.length > 10) console.log(` · (+${r.files_written.length - 10} more)`); console.log(` commit: ${r.commit?.sha?.slice(0, 7) || '(none)'}`); console.log('\n┌─ Wire-up status ────────────────────────────────'); const checks = [ ['process spawn', true], ['stream chunks', chunkCount > 0], ['result text', r.result_text.length > 0], ['files written', r.files_written.length > 0], ['git commit', !!r.commit?.sha], ]; let pass = 0; for (const [k, ok] of checks) { console.log(`│ ${ok ? '✓' : '✗'} ${k}`); if (ok) pass++; } console.log(`└─ ${pass}/${checks.length} checks passed\n`); console.log('Artifacts retained for inspection:'); console.log(` ${pr.resolveProjectRoot(proj.pid)}`); console.log(` cleanup: rm -rf data/companies/${cid}\n`); process.exit(pass === checks.length ? 0 : 1); } main().catch(e => { console.error('\n[real-pm-smoke] FATAL:', e?.message || e); console.error(e?.stack); process.exit(1); });