From e1f71cf1a29f041a502d7960dcd7c90b43883ad2 Mon Sep 17 00:00:00 2001 From: yibo Date: Wed, 13 May 2026 17:12:51 +0800 Subject: [PATCH] feat(devloop): P0 phase 0 — mock CLI, agent registry, loop controller, acceptance scripts --- fixtures/devloop-mock/default.json | 7 +++++++ fixtures/devloop-mock/devloop-pm.json | 11 +++++++++++ packages/acp-core/protocol/ClaudeCliProtocol.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/extract-agent-prompts.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/phase0-test-call.js | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/phase0-verify.js | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ server/src/core/devloop/loop-controller.js | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ server/src/tools/devloop/index.js | 27 +++++++++++++++++++++++++++ system/templates/_registry.json | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- system/templates/devloop-architect/config.json | 29 +++++++++++++++++++++++++++++ system/templates/devloop-architect/prompt.md | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/templates/devloop-coder/config.json | 25 +++++++++++++++++++++++++ system/templates/devloop-coder/prompt.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/templates/devloop-integrator/config.json | 28 ++++++++++++++++++++++++++++ system/templates/devloop-integrator/prompt.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/templates/devloop-pm/config.json | 26 ++++++++++++++++++++++++++ system/templates/devloop-pm/prompt.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/templates/devloop-tester/config.json | 27 +++++++++++++++++++++++++++ system/templates/devloop-tester/prompt.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ system/templates/devloop-triager/config.json | 26 ++++++++++++++++++++++++++ system/templates/devloop-triager/prompt.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ vitest.config.js | 2 +- 23 files changed, 1598 insertions(+), 2 deletions(-) create mode 100644 fixtures/devloop-mock/default.json create mode 100644 fixtures/devloop-mock/devloop-pm.json create mode 100644 packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js create mode 100644 scripts/extract-agent-prompts.js create mode 100644 scripts/phase0-test-call.js create mode 100644 scripts/phase0-verify.js create mode 100644 server/src/core/devloop/loop-controller.js create mode 100644 server/src/tools/devloop/index.js create mode 100644 system/templates/devloop-architect/config.json create mode 100644 system/templates/devloop-architect/prompt.md create mode 100644 system/templates/devloop-coder/config.json create mode 100644 system/templates/devloop-coder/prompt.md create mode 100644 system/templates/devloop-integrator/config.json create mode 100644 system/templates/devloop-integrator/prompt.md create mode 100644 system/templates/devloop-pm/config.json create mode 100644 system/templates/devloop-pm/prompt.md create mode 100644 system/templates/devloop-tester/config.json create mode 100644 system/templates/devloop-tester/prompt.md create mode 100644 system/templates/devloop-triager/config.json create mode 100644 system/templates/devloop-triager/prompt.md diff --git a/fixtures/devloop-mock/default.json b/fixtures/devloop-mock/default.json new file mode 100644 index 0000000..2f42ef8 --- /dev/null +++ b/fixtures/devloop-mock/default.json @@ -0,0 +1,7 @@ +{ + "chunks": [ + "[mock] DevLoop agent received prompt.\n", + "I would now plan the work and invoke tools, but this is the default mock response.\n" + ], + "result": "mock-ok (default fixture)" +} diff --git a/fixtures/devloop-mock/devloop-pm.json b/fixtures/devloop-mock/devloop-pm.json new file mode 100644 index 0000000..c38394a --- /dev/null +++ b/fixtures/devloop-mock/devloop-pm.json @@ -0,0 +1,11 @@ +{ + "chunks": [ + "## 第 1 步: 解析 idea\n", + "用户需求是构建一个最小可行的 DevLoop demo。\n", + "\n## 第 2 步: 写 prd 站点\n", + "(在真实运行时这里会调 bootstrap_prd_site + write_chapter)\n", + "\n## 完成\n", + "已生成 PRD v1。\n" + ], + "result": "mock-ok devloop-pm: PRD v1 scaffolded (no real writes in mock mode)" +} diff --git a/packages/acp-core/protocol/ClaudeCliProtocol.js b/packages/acp-core/protocol/ClaudeCliProtocol.js index 0b3c345..8f35dd3 100644 --- a/packages/acp-core/protocol/ClaudeCliProtocol.js +++ b/packages/acp-core/protocol/ClaudeCliProtocol.js @@ -23,6 +23,13 @@ const { EventEmitter } = require('events'); const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// LEMONBOSS_CLAUDE_MODE=mock 时,跳过真实 claude CLI 子进程, +// 改读 fixtures/devloop-mock/{agentName}.json 模拟流式响应。 +// 用于 DevLoop phase0 acceptance (scripts/phase0-test-call.js)。 +const MOCK_MODE = process.env.LEMONBOSS_CLAUDE_MODE === 'mock'; class ClaudeCliProtocol extends EventEmitter { constructor(agentInfo = {}, options = {}) { @@ -38,6 +45,10 @@ class ClaudeCliProtocol extends EventEmitter { this._promptReject = null; // 工具执行状态追踪(用于动态调整空闲超时) this._inToolExecution = false; + // mock 模式(构造时锁定,环境变量优先于 options.mock) + this._mockMode = MOCK_MODE || options.mock === true; + this._mockFixtureDir = options.mockFixtureDir + || path.resolve(__dirname, '../../../fixtures/devloop-mock'); } /** @@ -47,6 +58,13 @@ class ClaudeCliProtocol extends EventEmitter { async connect(transport, workingDir) { this._workingDir = workingDir; + // Mock 模式:跳过子进程,立即"已连接" + if (this._mockMode) { + console.log('[ClaudeCliProtocol] 🪄 mock 模式启用 (LEMONBOSS_CLAUDE_MODE=mock 或 options.mock)'); + this.isConnected = true; + return; + } + const args = [ '--input-format', 'stream-json', '--output-format', 'stream-json', @@ -202,6 +220,11 @@ class ClaudeCliProtocol extends EventEmitter { * @returns {Promise} */ sendPrompt(text) { + // Mock 模式:从 fixture 读 scripted 响应 + if (this._mockMode) { + return this._sendMockPrompt(text); + } + // 按照用户需求取消闲置超时限制 (设置为 24 小时) const IDLE_TIMEOUT_CHAT_MS = 24 * 60 * 60_000; // 已取消正常对话无活动上限 const IDLE_TIMEOUT_TOOL_MS = 24 * 60 * 60_000; // 已取消工具执行无活动上限 @@ -497,6 +520,73 @@ class ClaudeCliProtocol extends EventEmitter { this.isConnected = false; this.sessionId = null; } + + /** + * Mock 模式 sendPrompt 实现: + * 1. 找 fixture: {mockFixtureDir}/{agentName}.json (若无,用 default.json,再无 → 走 echo fallback) + * 2. fixture 形如:{ chunks: ["...text...", ...], result: "summary text" } + * 3. 同步 emit session_update (agent_message_chunk) + acp_event session/result + * 4. resolve + * + * 不发起任何子进程,不依赖外部网络;用于 phase0 测试。 + */ + async _sendMockPrompt(text) { + const agentName = this.agentInfo?.name || this.options.agentName || 'default'; + const fixturePath = path.join(this._mockFixtureDir, `${agentName}.json`); + const defaultPath = path.join(this._mockFixtureDir, 'default.json'); + + let fixture; + if (fs.existsSync(fixturePath)) { + try { + fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8')); + } catch (e) { + console.warn(`[ClaudeCliProtocol mock] fixture parse failed (${fixturePath}): ${e.message}`); + } + } + if (!fixture && fs.existsSync(defaultPath)) { + try { + fixture = JSON.parse(fs.readFileSync(defaultPath, 'utf8')); + } catch { /* ignore */ } + } + if (!fixture) { + // echo fallback: 把 prompt 头 80 字符回显 + fixture = { + chunks: [`[mock] received prompt for ${agentName}: `, text.slice(0, 80)], + result: `mock-ok (no fixture for ${agentName})`, + }; + } + + this.sessionId = `claude-cli-mock-${Date.now()}`; + + // 发 chunks(同步触发 session_update,模拟 stream_event delta) + const chunks = Array.isArray(fixture.chunks) ? fixture.chunks : []; + for (const c of chunks) { + this.emit('session_update', { + update: { + sessionUpdate: 'agent_message_chunk', + content: { text: typeof c === 'string' ? c : JSON.stringify(c) }, + }, + }); + } + + // 发 session/result(模拟 type=result) + const resultText = fixture.result || chunks.join(''); + this.emit('acp_event', { + type: 'notification', + method: 'session/result', + params: { + result: resultText, + duration_ms: 1, + cost_usd: 0, + session_id: this.sessionId, + stop_reason: 'end_turn', + usage: { input: 0, output: 0 }, + model: 'mock', + mock: true, + }, + timestamp: Date.now(), + }); + } } module.exports = { ClaudeCliProtocol }; diff --git a/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js b/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js new file mode 100644 index 0000000..4104432 --- /dev/null +++ b/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js @@ -0,0 +1,50 @@ +/** + * ClaudeCliProtocol · mock 模式 smoke test + */ + +const { ClaudeCliProtocol } = require('./ClaudeCliProtocol'); + +describe('ClaudeCliProtocol · mock mode', () => { + it('connect is no-op in mock mode', async () => { + const p = new ClaudeCliProtocol({ name: 'devloop-pm' }, { mock: true }); + await p.connect(null, process.cwd()); + expect(p.isConnected).toBe(true); + p.disconnect(); + }); + + it('sendPrompt emits chunks + session/result from fixture', async () => { + const p = new ClaudeCliProtocol({ name: 'devloop-pm' }, { mock: true }); + await p.connect(null, process.cwd()); + + const chunks = []; + let result = null; + p.on('session_update', (u) => { + if (u.update?.sessionUpdate === 'agent_message_chunk') { + chunks.push(u.update.content.text); + } + }); + p.on('acp_event', (ev) => { + if (ev.method === 'session/result') result = ev.params; + }); + + await p.sendPrompt('please plan the work'); + expect(chunks.length).toBeGreaterThan(0); + expect(result).not.toBeNull(); + expect(result.mock).toBe(true); + expect(result.stop_reason).toBe('end_turn'); + p.disconnect(); + }); + + it('falls back to echo response when no fixture matches', async () => { + const p = new ClaudeCliProtocol( + { name: 'no-such-agent-zzz' }, + { mock: true, mockFixtureDir: '/tmp/devloop-mock-empty-' + Date.now() }, + ); + await p.connect(null, process.cwd()); + let result = null; + p.on('acp_event', (ev) => { if (ev.method === 'session/result') result = ev.params; }); + await p.sendPrompt('hello'); + expect(result.result).toMatch(/mock-ok/); + p.disconnect(); + }); +}); diff --git a/scripts/extract-agent-prompts.js b/scripts/extract-agent-prompts.js new file mode 100644 index 0000000..3478785 --- /dev/null +++ b/scripts/extract-agent-prompts.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * scripts/extract-agent-prompts.js + * + * 从 docs/prd/newfunction/agents-{role}.html 抽取 §2.2 "完整 System Prompt" + * 区块,写到 system/templates/devloop-{role}/prompt.md。 + * + * role ∈ pm | architect | coder | tester | integrator | triager + * + * 抽取规则(本脚本只做规则正向匹配,不依赖 cheerio 来保留原始换行): + * 1. 定位

2.2 完整 System Prompt

+ * 2. 取其后第一个
...
的内层 + * 3. 去除全部 ... 包装,保留文本 + * 4. 反转义 HTML entity (< > & ") + * + * 规格: docs/prd/newfunction/plan-migration.md · P0.B + */ + +const fs = require('fs'); +const path = require('path'); + +const ROLES = ['pm', 'architect', 'coder', 'tester', 'integrator', 'triager']; +const SRC_DIR = path.resolve(__dirname, '../docs/prd/newfunction'); +const OUT_DIR = path.resolve(__dirname, '../system/templates'); + +function decodeHtmlEntities(s) { + return s + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&/g, '&'); +} + +function stripSpans(s) { + // 去除 ...,保留内部文本(可能嵌套也用同一规则处理) + let prev; + let cur = s; + do { + prev = cur; + cur = cur.replace(/]*>([\s\S]*?)<\/span>/g, '$1'); + } while (cur !== prev); + // 去除残留的开闭标签 (容错) + cur = cur.replace(/<\/?span[^>]*>/g, ''); + return cur; +} + +function extractPrompt(html, role) { + const anchorRe = /]*>\s*2\.2\s+完整\s*System Prompt[^<]*<\/h3>/; + const am = html.match(anchorRe); + if (!am) throw new Error(`§2.2 header not found in agents-${role}.html`); + const after = html.slice(am.index + am[0].length); + const preRe = /]*>([\s\S]*?)<\/pre>/; + const pm = after.match(preRe); + if (!pm) throw new Error(`
 after §2.2 not found in agents-${role}.html`);
+  const inner = pm[1];
+  const stripped = stripSpans(inner);
+  const decoded = decodeHtmlEntities(stripped);
+  return decoded.trim();
+}
+
+function run() {
+  fs.mkdirSync(OUT_DIR, { recursive: true });
+  const results = [];
+  for (const role of ROLES) {
+    const src = path.join(SRC_DIR, `agents-${role}.html`);
+    if (!fs.existsSync(src)) {
+      results.push({ role, ok: false, reason: 'source missing: ' + src });
+      continue;
+    }
+    try {
+      const html = fs.readFileSync(src, 'utf8');
+      const prompt = extractPrompt(html, role);
+      const outDir = path.join(OUT_DIR, `devloop-${role}`);
+      fs.mkdirSync(outDir, { recursive: true });
+      const outPath = path.join(outDir, 'prompt.md');
+      const header = `\n\n`;
+      fs.writeFileSync(outPath, header + prompt + '\n');
+      results.push({ role, ok: true, bytes: prompt.length, path: outPath });
+    } catch (e) {
+      results.push({ role, ok: false, reason: e.message });
+    }
+  }
+  for (const r of results) {
+    if (r.ok) console.log(`✓ devloop-${r.role}.md (${r.bytes} bytes)`);
+    else console.error(`✗ ${r.role}: ${r.reason}`);
+  }
+  const allOk = results.every(r => r.ok);
+  if (!allOk) process.exit(1);
+}
+
+if (require.main === module) run();
+
+module.exports = { extractPrompt, stripSpans, decodeHtmlEntities };
diff --git a/scripts/phase0-test-call.js b/scripts/phase0-test-call.js
new file mode 100644
index 0000000..cf12bfe
--- /dev/null
+++ b/scripts/phase0-test-call.js
@@ -0,0 +1,167 @@
+#!/usr/bin/env node
+/**
+ * scripts/phase0-test-call.js
+ *
+ * 真跑一遍 mock 模式下的 PM Agent 调用,并验证:
+ *  [1] LoopController.createProject 能在 sandbox 模式下分配项目 + git init + 首次 commit
+ *  [2] ClaudeCliProtocol(mock=true) 能 connect / sendPrompt / disconnect 无报错
+ *  [3] fixture 的 chunks 全部通过 session_update 流出
+ *  [4] session/result 事件 mock=true & stop_reason=end_turn
+ *  [5] LoopController.commitAfterAgent 在工作树无改动时返回 null
+ *  [6] 写一个虚拟产出文件 → commitAfterAgent 能产出新 sha
+ *  [7] git log 至少 2 个 commit (init + agent 产出)
+ *
+ * 通过即 exit 0,任一失败 exit 1。
+ *
+ * 用法:  LEMONBOSS_CLAUDE_MODE=mock node scripts/phase0-test-call.js
+ *        (脚本会自动设置环境变量,无需手动)
+ */
+
+process.env.LEMONBOSS_CLAUDE_MODE = 'mock';
+
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const loopController = require('../server/src/core/devloop/loop-controller');
+const pathResolver = require('../server/services/devloop/path-resolver');
+const gitVault = require('../server/services/devloop/git-vault');
+const { ClaudeCliProtocol } = require('../packages/acp-core/protocol/ClaudeCliProtocol');
+
+const results = [];
+function step(name, fn) {
+  return (async () => {
+    try {
+      const r = await fn();
+      results.push({ name, ok: true, detail: r?.detail || '' });
+      return r;
+    } catch (e) {
+      results.push({ name, ok: false, reason: e.message });
+      throw e;
+    }
+  })();
+}
+
+async function main() {
+  let pid;
+
+  // 用唯一 company_id 隔离这次跑(不污染 real data dir 的常规 c-test)
+  const cid = 'c-phase0-' + Date.now().toString(36);
+
+  try {
+    const proj = await step('createProject (sandbox + git init)', async () => {
+      const r = await loopController.createProject({
+        company_id: cid,
+        mode: 'sandbox',
+        idea: 'Phase0 acceptance test: minimal devloop scaffolding',
+      });
+      if (!r.pid?.startsWith('p-')) throw new Error('bad pid: ' + r.pid);
+      if (!fs.existsSync(path.join(r.root_path, '.git'))) {
+        throw new Error('.git not initialized');
+      }
+      return { detail: `pid=${r.pid} sha=${r.first_commit?.sha?.slice(0, 7) || 'init'}` };
+    });
+    pid = (await pathResolver.listProjects()).find(p => p.company_id === cid)?.pid;
+    if (!pid) {
+      const all = pathResolver.listProjects();
+      pid = all.find(p => p.company_id === cid)?.pid;
+    }
+    if (!pid) throw new Error('cannot resolve pid after createProject');
+
+    const protocol = new ClaudeCliProtocol({ name: 'devloop-pm' }, { mock: true });
+
+    await step('ClaudeCliProtocol(mock) connect', async () => {
+      await protocol.connect(null, pathResolver.resolveProjectRoot(pid));
+      if (!protocol.isConnected) throw new Error('not connected');
+      return { detail: 'isConnected=true' };
+    });
+
+    const chunks = [];
+    let resultEvent = null;
+    protocol.on('session_update', (u) => {
+      if (u.update?.sessionUpdate === 'agent_message_chunk') {
+        chunks.push(u.update.content.text);
+      }
+    });
+    protocol.on('acp_event', (ev) => {
+      if (ev.method === 'session/result') resultEvent = ev.params;
+    });
+
+    await step('sendPrompt → chunks + result', async () => {
+      await protocol.sendPrompt('please scaffold a minimal PRD for a TODO app');
+      if (chunks.length === 0) throw new Error('no chunks received');
+      if (!resultEvent) throw new Error('no session/result');
+      if (resultEvent.mock !== true) throw new Error('result.mock !== true');
+      if (resultEvent.stop_reason !== 'end_turn') throw new Error('bad stop_reason: ' + resultEvent.stop_reason);
+      return { detail: `${chunks.length} chunks, result.length=${resultEvent.result.length}` };
+    });
+
+    protocol.disconnect();
+
+    await step('commitAfterAgent (no changes → null)', async () => {
+      const r = await loopController.commitAfterAgent(pid, 'devloop-pm', 'no-op test');
+      if (r !== null) throw new Error('expected null, got: ' + JSON.stringify(r));
+      return { detail: 'no commit when clean' };
+    });
+
+    await step('write virtual PRD file + commitAfterAgent', async () => {
+      const root = pathResolver.resolveProjectRoot(pid);
+      const prdDir = path.join(root, 'prd');
+      fs.mkdirSync(prdDir, { recursive: true });
+      fs.writeFileSync(path.join(prdDir, 'index.html'), 'mock');
+      const r = await loopController.commitAfterAgent(pid, 'devloop-pm', 'mock PRD scaffold');
+      if (!r || !r.sha) throw new Error('expected commit, got: ' + JSON.stringify(r));
+      return { detail: `sha=${r.sha.slice(0, 7)}` };
+    });
+
+    await step('git log shows ≥2 commits', async () => {
+      const log = await gitVault.log(pid, { limit: 10 });
+      const commits = Array.isArray(log) ? log : (log.commits || []);
+      if (commits.length < 2) {
+        throw new Error('expected ≥2 commits, got: ' + commits.length);
+      }
+      return { detail: `${commits.length} commits` };
+    });
+
+    await step('timeline.ndjson has project.created + git.commit events', async () => {
+      const tl = pathResolver.resolveTimelineLog(pid);
+      if (!fs.existsSync(tl)) throw new Error('timeline.ndjson missing');
+      const lines = fs.readFileSync(tl, 'utf8').trim().split('\n').filter(Boolean);
+      const kinds = lines.map(l => { try { return JSON.parse(l).kind; } catch { return ''; }});
+      if (!kinds.includes('project.created')) throw new Error('no project.created in timeline');
+      if (!kinds.includes('git.commit')) throw new Error('no git.commit in timeline');
+      return { detail: `${lines.length} timeline events` };
+    });
+
+  } finally {
+    // cleanup: 删 sandbox 项目目录(本次测试专用)
+    if (pid) {
+      try {
+        const root = pathResolver.resolveProjectRoot(pid);
+        const companyDir = path.dirname(path.dirname(root));
+        if (companyDir.includes(cid)) {
+          fs.rmSync(companyDir, { recursive: true, force: true });
+        }
+      } catch { /* ignore */ }
+    }
+  }
+
+  let pass = 0, fail = 0;
+  console.log('\n┌─ DevLoop Phase 0 — Mock End-to-End Test ────────────');
+  for (const r of results) {
+    const tag = r.ok ? '✓' : '✗';
+    console.log(`│ ${tag} ${r.name.padEnd(50)} ${r.ok ? r.detail : '— ' + r.reason}`);
+    r.ok ? pass++ : fail++;
+  }
+  console.log(`└─ ${pass}/${pass + fail} steps passed${fail ? ' · ' + fail + ' failed' : ''}\n`);
+
+  process.exit(fail ? 1 : 0);
+}
+
+main().catch(err => {
+  console.error('\n[phase0-test-call] fatal:', err);
+  for (const r of results) {
+    console.log(' ', r.ok ? '✓' : '✗', r.name, r.ok ? r.detail : '— ' + r.reason);
+  }
+  process.exit(1);
+});
diff --git a/scripts/phase0-verify.js b/scripts/phase0-verify.js
new file mode 100644
index 0000000..2c80049
--- /dev/null
+++ b/scripts/phase0-verify.js
@@ -0,0 +1,191 @@
+#!/usr/bin/env node
+/**
+ * scripts/phase0-verify.js
+ *
+ * DevLoop P0 末端验收检查器(不发起任何子进程,只做静态校验)。
+ *
+ * Checkpoints:
+ *  [1] 35 个 devloop 工具全部 loadable
+ *  [2] 6 个 devloop-* agent 在 system/templates/_registry.json 注册
+ *  [3] 每个 devloop-* 目录有 prompt.md + config.json (含 cli.toolPolicy)
+ *  [4] bash-json-guard 可挂在 Bash preExecute (顶层 index.js 暴露)
+ *  [5] services/devloop 7 大件 + tool layer 7 大件 全部 require 成功
+ *  [6] mock fixtures 默认+pm 两个文件存在
+ *  [7] git-vault + path-resolver 接口存在
+ *
+ * 通过即 exit 0,任一失败 exit 1。
+ *
+ * 规格: docs/prd/newfunction/_spec/plan-migration.md · Quick-Start step 7
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const checks = [];
+
+function check(name, fn) {
+  try {
+    const r = fn();
+    if (r && r.ok === false) checks.push({ name, ok: false, reason: r.reason });
+    else checks.push({ name, ok: true, detail: r?.detail || '' });
+  } catch (e) {
+    checks.push({ name, ok: false, reason: e.message });
+  }
+}
+
+// ── [1] tools loadable ────────────────────────────────────────
+check('35 devloop tools loadable', () => {
+  const reg = require('../server/src/tools/devloop/tool-registry');
+  reg._resetForTesting();
+  const all = reg.loadAllTools();
+  const names = reg.listToolNames();
+  const expected = [
+    // persistence (5)
+    'write_todolist', 'update_todo_item', 'update_runtime_state',
+    'timeline_append', 'record_memory',
+    // doc (12)
+    'bootstrap_prd_site', 'bootstrap_sdd_site', 'write_chapter',
+    'write_mvcp_page', 'archive_doc', 'diff_html', 'diff_units',
+    'echo_diff', 'update_search_index', 'validate_prd_site',
+    'validate_sdd_consistency', 'hash_contract',
+    // coder (3)
+    'contract_bridge', 'sandbox_check', 'append_coder_log',
+    // tester (5)
+    'parse_vitest', 'hint_builder', 'bug_open', 'bug_transition', 'bug_list',
+    // integrator (6)
+    'port_picker', 'spawn_units', 'health_wait', 'run_case',
+    'parse_placeholder', 'collect_stderr',
+    // triager (4)
+    'extract_evidence', 'category_classifier',
+    'diff_contract_vs_impl', 'find_similar_bug',
+  ];
+  const missing = expected.filter(n => !names.includes(n));
+  if (missing.length) {
+    return { ok: false, reason: `missing tools: ${missing.join(', ')}` };
+  }
+  return { detail: `${all.size} tools registered (expected ≥35)` };
+});
+
+// ── [2] _registry.json has 6 devloop-* entries ─────────────────
+check('_registry.json has 6 devloop-* agents', () => {
+  const regPath = path.join(ROOT, 'system/templates/_registry.json');
+  const list = JSON.parse(fs.readFileSync(regPath, 'utf8'));
+  const expected = ['devloop-pm', 'devloop-architect', 'devloop-coder',
+                    'devloop-tester', 'devloop-integrator', 'devloop-triager'];
+  const ids = list.map(x => x.id);
+  const missing = expected.filter(e => !ids.includes(e));
+  if (missing.length) return { ok: false, reason: `missing: ${missing.join(', ')}` };
+  return { detail: `6/6 registered` };
+});
+
+// ── [3] per-agent dir has prompt.md + config.json ──────────────
+check('per-agent prompt.md + config.json (6 agents)', () => {
+  const agents = ['devloop-pm', 'devloop-architect', 'devloop-coder',
+                  'devloop-tester', 'devloop-integrator', 'devloop-triager'];
+  const issues = [];
+  for (const a of agents) {
+    const dir = path.join(ROOT, 'system/templates', a);
+    const promptOk = fs.existsSync(path.join(dir, 'prompt.md'));
+    const cfgPath = path.join(dir, 'config.json');
+    const cfgOk = fs.existsSync(cfgPath);
+    if (!promptOk) issues.push(`${a}: prompt.md missing`);
+    if (!cfgOk) issues.push(`${a}: config.json missing`);
+    if (cfgOk) {
+      try {
+        const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
+        const tp = cfg.cli?.toolPolicy?.allowedTools;
+        if (!Array.isArray(tp) || tp.length === 0) {
+          issues.push(`${a}: cli.toolPolicy.allowedTools missing`);
+        }
+        if (!cfg.sandbox?.writeAllowList) {
+          issues.push(`${a}: sandbox.writeAllowList missing`);
+        }
+      } catch (e) {
+        issues.push(`${a}: config.json invalid (${e.message})`);
+      }
+    }
+  }
+  if (issues.length) return { ok: false, reason: issues.join('; ') };
+  return { detail: '6 agents fully wired' };
+});
+
+// ── [4] bash-json-guard exported via index ─────────────────────
+check('bash-json-guard wired (devloop/index.js)', () => {
+  const idx = require('../server/src/tools/devloop');
+  if (!idx.bashHooks || !Array.isArray(idx.bashHooks.preExecute)) {
+    return { ok: false, reason: 'bashHooks.preExecute not exported' };
+  }
+  // 触发一次 inline JSON 应抛
+  const hook = idx.bashHooks.preExecute[0];
+  let threw = false;
+  try { hook(`curl -d '{"k":"v"}' http://x`); } catch { threw = true; }
+  if (!threw) return { ok: false, reason: 'guard did not block inline JSON' };
+  return { detail: 'jsonGuard blocks inline JSON ✓' };
+});
+
+// ── [5] services/devloop modules require successfully ─────────
+check('services/devloop 7 modules require', () => {
+  const mods = [
+    '../server/services/devloop/path-resolver',
+    '../server/services/devloop/doc-store',
+    '../server/services/devloop/unit-registry',
+    '../server/services/devloop/bug-tracker',
+    '../server/services/devloop/timeline-logger',
+    '../server/services/devloop/changelog-writer',
+    '../server/services/devloop/sse-broker',
+    '../server/services/devloop/git-vault',
+  ];
+  for (const m of mods) {
+    try { require(m); } catch (e) {
+      return { ok: false, reason: `${m}: ${e.message}` };
+    }
+  }
+  return { detail: `${mods.length}/${mods.length} services OK` };
+});
+
+// ── [6] mock fixtures present ─────────────────────────────────
+check('mock fixtures present', () => {
+  const dir = path.join(ROOT, 'fixtures/devloop-mock');
+  for (const f of ['default.json', 'devloop-pm.json']) {
+    if (!fs.existsSync(path.join(dir, f))) {
+      return { ok: false, reason: `missing fixtures/devloop-mock/${f}` };
+    }
+  }
+  return { detail: 'default.json + devloop-pm.json' };
+});
+
+// ── [7] git-vault interface present ────────────────────────────
+check('git-vault interface', () => {
+  const gv = require('../server/services/devloop/git-vault');
+  for (const fn of ['init', 'commit', 'status', 'log']) {
+    if (typeof gv[fn] !== 'function') {
+      return { ok: false, reason: `git-vault.${fn} missing` };
+    }
+  }
+  return { detail: 'init/commit/status/log present' };
+});
+
+// ── [8] loop-controller present + git hook callable ───────────
+check('loop-controller exposes createProject + commitAfterAgent', () => {
+  const lc = require('../server/src/core/devloop/loop-controller');
+  for (const fn of ['createProject', 'commitAfterAgent']) {
+    if (typeof lc[fn] !== 'function') {
+      return { ok: false, reason: `loop-controller.${fn} missing` };
+    }
+  }
+  return { detail: 'createProject + commitAfterAgent present' };
+});
+
+// ── 输出 ──────────────────────────────────────────────────────
+let pass = 0, fail = 0;
+console.log('\n┌─ DevLoop Phase 0 — Static Verification ─────────────');
+for (const c of checks) {
+  const tag = c.ok ? '✓' : '✗';
+  const line = `│ ${tag} ${c.name.padEnd(48)} ${c.ok ? c.detail : '— ' + c.reason}`;
+  console.log(line);
+  c.ok ? pass++ : fail++;
+}
+console.log(`└─ ${pass}/${pass + fail} checks passed${fail ? ' · ' + fail + ' failed' : ''}\n`);
+
+process.exit(fail ? 1 : 0);
diff --git a/server/src/core/devloop/loop-controller.js b/server/src/core/devloop/loop-controller.js
new file mode 100644
index 0000000..bce2f7a
--- /dev/null
+++ b/server/src/core/devloop/loop-controller.js
@@ -0,0 +1,125 @@
+/**
+ * DevLoop · LoopController (P0 最小骨架)
+ *
+ * 在 P0 阶段只暴露两个对外接口:
+ *   - createProject({ company_id, mode, target_path }) — 调 path-resolver + gitVault.init
+ *   - commitAfterAgent(pid, agentName, summary) — 每次 Agent 完成后 auto-commit
+ *
+ * 完整的 dispatch / unit fsm / retry 逻辑留待 P1 实现。
+ *
+ * 规格:
+ *   - docs/prd/newfunction/_spec/sdd-process.md · P-10 GitCommit
+ *   - docs/prd/newfunction/_spec/arch-git-versioning.md
+ *   - docs/prd/newfunction/_spec/arch-project-root.md
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const pathResolver = require('../../../services/devloop/path-resolver');
+const gitVault = require('../../../services/devloop/git-vault');
+const timelineLogger = require('../../../services/devloop/timeline-logger');
+
+/**
+ * 新建项目:
+ *   1. allocateProjectRoot (sandbox / linked-new)
+ *   2. 写 project.json (最小元数据)
+ *   3. gitVault.init + 写 .gitignore (gitVault 内置 DEFAULT_GITIGNORE)
+ *   4. 首次 commit "chore: init project"
+ */
+async function createProject({ company_id, mode = 'sandbox', target_path, idea = '' }) {
+  if (!company_id) throw new Error('company_id required');
+
+  const { pid, root_path } = pathResolver.allocateProjectRoot({
+    company_id, mode, target_path,
+  });
+
+  // project.json
+  const meta = {
+    pid,
+    company_id,
+    mode,
+    created_at: new Date().toISOString(),
+    idea_summary: (idea || '').slice(0, 200),
+  };
+  fs.writeFileSync(
+    pathResolver.resolveProjectJson(pid),
+    JSON.stringify(meta, null, 2)
+  );
+
+  // git init (内置首个 init commit) — gitVault.init 已自己做 commit
+  await gitVault.init(pid, {
+    summary: idea ? idea.slice(0, 80) : `init ${pid}`,
+  });
+
+  // timeline (写在 init 之后,作为"项目创建"事件)
+  try {
+    timelineLogger.append(pid, {
+      source: 'system',
+      kind: 'project.created',
+      ref: pid,
+      summary: `Project ${pid} created (${mode})`,
+      payload: { company_id, mode, root_path },
+    });
+  } catch (e) {
+    console.warn('[LoopController] timeline append failed:', e.message);
+  }
+
+  // 把 project.json + timeline.ndjson 一并提交进第二个 commit (工作树留干净)
+  const firstCommit = await gitVault.commit(pid, {
+    kind: 'chore',
+    summary: `chore: project metadata + first timeline entry for ${pid}`,
+    author: { name: 'devloop-system', email: 'devloop@local' },
+  });
+
+  return { pid, root_path, mode, first_commit: firstCommit };
+}
+
+/**
+ * Agent 结束后,把工作树新文件 auto-commit。
+ * 调用方一般在 stream-executor 完成或 agent-manager.post-task-success 时调。
+ *
+ * @param {string} pid
+ * @param {string} agentName  (devloop-pm / devloop-architect / ...)
+ * @param {string} summary    一句话描述本次产出 (做 commit message 副标题)
+ * @returns {{ sha, files_changed }} 或 null (无变更时)
+ */
+async function commitAfterAgent(pid, agentName, summary = '') {
+  // commit kind 推断
+  const kindMap = {
+    'devloop-pm': 'prd',
+    'devloop-architect': 'sdd',
+    'devloop-coder': 'code',
+    'devloop-tester': 'code',
+    'devloop-integrator': 'integ',
+    'devloop-triager': 'triage',
+  };
+  const kind = kindMap[agentName] || 'chore';
+  const msg = `${kind}(${agentName}): ${summary || 'agent update'}`;
+  const commit = await gitVault.commit(pid, {
+    kind,
+    summary: msg,
+    author: { name: agentName, email: `${agentName}@devloop.local` },
+  });
+
+  if (commit.skipped) {
+    return null;
+  }
+
+  try {
+    timelineLogger.append(pid, {
+      source: agentName,
+      kind: 'git.commit',
+      ref: commit.sha,
+      summary: msg.slice(0, 200),
+      payload: { sha: commit.sha },
+    });
+  } catch { /* swallow */ }
+
+  return commit;
+}
+
+module.exports = {
+  createProject,
+  commitAfterAgent,
+};
diff --git a/server/src/tools/devloop/index.js b/server/src/tools/devloop/index.js
new file mode 100644
index 0000000..6ca3444
--- /dev/null
+++ b/server/src/tools/devloop/index.js
@@ -0,0 +1,27 @@
+/**
+ * DevLoop tool layer · 总入口
+ *
+ * 对外暴露:
+ *   - tools (Map): 全部 devloop 工具(35 个)
+ *   - listToolNames / getDevLoopTools / dispatch: registry 接口
+ *   - bashHooks: 注入到 LemonBoss Bash 工具的 preExecute 数组
+ *
+ * 用法:
+ *   const { tools, bashHooks } = require('server/src/tools/devloop');
+ *   bashTool.preExecute.push(...bashHooks.preExecute);
+ */
+
+const registry = require('./tool-registry');
+const { bashHooks, jsonGuard } = require('./bash-json-guard');
+
+module.exports = {
+  // tool registry
+  tools: registry.loadAllTools(),
+  listToolNames: registry.listToolNames,
+  getDevLoopTools: registry.getDevLoopTools,
+  dispatch: registry.dispatch,
+
+  // bash guard
+  bashHooks,
+  jsonGuard,
+};
diff --git a/system/templates/_registry.json b/system/templates/_registry.json
index d394892..299d914 100644
--- a/system/templates/_registry.json
+++ b/system/templates/_registry.json
@@ -25,5 +25,59 @@
     "benchmarkScore": 90,
     "trainedAt": "2026-03-26",
     "usedCount": 0
+  },
+  {
+    "id": "devloop-pm",
+    "name": "DevLoop PM Agent",
+    "type": "devloop",
+    "description": "产出多页 HTML PRD 站点 (init / incremental / resume 三模式)。规格 docs/prd/newfunction/agents-pm.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
+  },
+  {
+    "id": "devloop-architect",
+    "name": "DevLoop Architect Agent",
+    "type": "devloop",
+    "description": "把 PRD 翻译成 SDD (MVCP) + units.json + integration spec。规格 docs/prd/newfunction/agents-architect.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
+  },
+  {
+    "id": "devloop-coder",
+    "name": "DevLoop Coder Agent",
+    "type": "devloop",
+    "description": "单 Unit 实现器 + retry-with-hint。规格 docs/prd/newfunction/agents-coder.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
+  },
+  {
+    "id": "devloop-tester",
+    "name": "DevLoop Tester Agent",
+    "type": "devloop",
+    "description": "运行 vitest + 生成 result.json + 开 bug。规格 docs/prd/newfunction/agents-tester.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
+  },
+  {
+    "id": "devloop-integrator",
+    "name": "DevLoop Integrator Agent",
+    "type": "devloop",
+    "description": "拉起多 Unit 进程 + 执行 integration spec。规格 docs/prd/newfunction/agents-integrator.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
+  },
+  {
+    "id": "devloop-triager",
+    "name": "DevLoop BugTriager Agent",
+    "type": "devloop",
+    "description": "分析 bug 根因 + 4 类粗判 + 路由给 Coder/Architect。规格 docs/prd/newfunction/agents-triager.html",
+    "benchmarkScore": null,
+    "trainedAt": "2026-05-13",
+    "usedCount": 0
   }
-]
\ No newline at end of file
+]
diff --git a/system/templates/devloop-architect/config.json b/system/templates/devloop-architect/config.json
new file mode 100644
index 0000000..443441d
--- /dev/null
+++ b/system/templates/devloop-architect/config.json
@@ -0,0 +1,29 @@
+{
+  "type": "devloop",
+  "status": "idle",
+  "promptFile": "prompt.md",
+  "cli": {
+    "toolPolicy": {
+      "mode": "restricted",
+      "allowedTools": [
+        "Read", "Bash",
+        "write_todolist", "update_todo_item", "update_runtime_state",
+        "timeline_append", "record_memory",
+        "bootstrap_sdd_site", "write_chapter", "write_mvcp_page",
+        "diff_html", "diff_units", "echo_diff", "update_search_index",
+        "validate_sdd_consistency", "archive_doc", "hash_contract"
+      ]
+    }
+  },
+  "sandbox": {
+    "writeAllowList": [
+      "sdd/**",
+      "units.json",
+      "integration/**",
+      "agent-state/**",
+      "timeline.ndjson",
+      "changelog.ndjson",
+      "history/**"
+    ]
+  }
+}
diff --git a/system/templates/devloop-architect/prompt.md b/system/templates/devloop-architect/prompt.md
new file mode 100644
index 0000000..70adad6
--- /dev/null
+++ b/system/templates/devloop-architect/prompt.md
@@ -0,0 +1,131 @@
+
+
+# ROLE
+你是 LemonBoss DevLoop 的 Architect Agent。你把 PM Agent 产出的 PRD 站点
+转化为 SDD 站点 + units.json + integration/spec.json,让下游 Coder/Tester/
+Integrator/BugTriager 都能照单干活。
+
+# MODES (输入参数 mode 决定行为)
+- "generate"   PRD 第一次出/重大变化 → 重新派生整套 SDD
+- "patch"      接 triage.json,精准应用 proposed_sdd_patch
+- "resume"     从 resume_token 恢复未完 todolist
+
+# OUTPUT FORMAT (硬约束 - DocStore 拒残)
+
+[A] 三件套必齐:
+    sdd/                              5 个 HTML + 3 个 asset
+    units.json                        含 DAG · contract_hash · l2 预算
+    integration/spec.json             端到端 cases
+
+[B] 每个 MVCP 定义五字段:
+    
+

X · {简短描述}

+ {预期落盘路径} +
{TypeBox / TS / Vue props / SQL DDL}
+
  • 前置/后置/不变式
+
  • 验收用例(给 Tester 转 vitest)
+
+ +[C] Unit id 严格命名: /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/ + 示例: auth-be · profile-fe · session-mw · todo-db + 禁止: AuthBE · auth_be · auth-backend · auth (无后缀) + +[D] 每个 Unit 必须有: + - contract (非空,OpenAPI/TS/schema) + - contract_hash (sha256:{16 chars},由 hash_contract 工具计算) + - depends_on (string[],可为空) + - 在 sdd/mvcp/*.html 中至少出现 1 次 + +[E] integration/spec.json 每 case 必须: + - 至少 2 步 + - involves 字段是步骤 unit 的并集 + - 引用的所有 unit 在 units.json 存在 + +# MANDATORY WORKFLOW + +## step 0 · 读取上下文 + ├─ 必读 prd/ 全部章节(尤其 prd/feature/feature-tree.html · prd/flow/user-flows.html) + ├─ 必读 changelog.ndjson 最后 5 条 + ├─ generate / patch 模式: 读现有 sdd/ + units.json + integration/spec.json (若存在) + ├─ patch 模式: 必读 integration/runs/{runId}/triage.json + └─ memory section 已在 prompt 顶部注入,不重复读 + +## step 1 · write_todolist + +### generate 模式典型 todolist (12 项): + step-001 解析 prd/feature/feature-tree.html → 列出 N 个 feature + step-002 对每个 feature 决定 be/fe/mw/db 拆分 + step-003 为每个 Unit 起草 contract (OpenAPI / TS interface / schema) + step-004 调 hash_contract 算 contract_hash + step-005 构造 DAG (depends_on) + 拓扑排序验环 + step-006 写 sdd/index.html + overview.html + step-007 写 sdd/mvcp/model.html (所有 data schema) + step-008 写 sdd/mvcp/view.html (所有 fe 组件) + step-009 写 sdd/mvcp/control.html (所有 be/mw 接口) + step-010 写 sdd/mvcp/process.html (长进程,可能空) + step-011 写 units.json (合并所有 Unit 元数据) + step-012 写 integration/spec.json (e2e cases) + step-013 update_search_index('sdd') + validate_sdd_consistency + step-014 timeline_append + record_memory (可选) + +### patch 模式典型 todolist (6 项): + step-001 读 triage.json + 定位影响的 sdd section + step-002 sdd-section-patcher 应用 proposed_sdd_patch + step-003 受影响 Unit 重算 contract_hash + step-004 更新 units.json (changed 数组标记) + step-005 (如 triage 暗示) 更新 integration/spec.json + step-006 validate + update_search_index + timeline + +## step N+1 · 收尾 + ├─ validate_sdd_consistency(pid) → 必须 ok=true + ├─ update_search_index(pid, 'sdd') + ├─ timeline_append('sdd_generated'|'sdd_patched', ref, summary) + └─ record_memory 如果发现可复用的架构模式(如 "项目偏好 better-sqlite3") + +# RULES (硬约束) + +1. 永远不读、不写 prd/。PRD 是 PM 的领地。 +2. 永远不写代码、不动 units/{id}/code/。代码是 Coder 的活。 +3. 同一 feature 涉及 BE + FE 必拆 2 个 Unit。不允许"全栈 Unit"。 +4. contract_hash 必须用 hash_contract 工具,不要手算/猜测。 +5. patch 模式只动 triage.affected_units 涉及的 section,其他原样保留。 +6. units.json 写入前必须 typebox 校验 + 拓扑排序验环。 +7. integration/spec.json 的 involves 必须是 cases.steps.unit 的精确并集。 +8. 任何带 JSON body 的 Bash 命令 → /tmp/*.json + -d @file (CLAUDE.md L91-99)。 +9. 沙箱: sdd/** · units.json · integration/spec.json · memory/devloop/architect/ +10. 不调 call_sub_agent。你不分派。 + +# DECISION HEURISTICS (写 SDD 时常用判断) + +### 何时拆 mw Unit + - 跨 ≥2 个 Unit 共享的能力(认证、限流、CORS) + - 引入新的请求/响应钩子 + - 否则不拆,内联在调用方的 Unit 里。 + +### 何时拆 db Unit + - 新表/列的 SQL DDL + - 跨多 Unit 共享的 schema (放 db 的好处:Coder 不必各自重复) + - 否则,schema 写在调用方 be Unit 内。 + +### 何时升 Unit 颗粒 + - 一个 Unit code 估算 > 400 行 → 拆 2 个 + - 单 Unit 测试用例 > 15 个 → 同上 + - 同 feature 内有显著独立的子能力(如 auth 含 login + register + token-refresh) + +# OUTPUT VOICE +- 中文写描述,英文写 token/code 标识符 +- 不写"完美""强大",数字 > 形容词 +- contract 段落要简洁但完整,不写无意义的注释 + +# SELF-CHECK +1. validate_sdd_consistency(pid) 返回 ok=true +2. units.json 通过 typebox 校验 +3. units.json DAG 无环 (topoSort 通过) +4. 每个 Unit 在 sdd/mvcp/ 至少出现 1 次 +5. 每个 Unit contract_hash 重算与文件中一致 +6. integration/spec.json involves 都在 units.json +7. agent-state/architect.json status=done +8. todolist 全 done +9. timeline 含 sdd_generated 或 sdd_patched +10. patch 模式: 仅 affected_units 涉及的 unit contract_hash 变化 +任一不过 → patch 后重跑 (≤2 次)。第 3 次仍不过 → 报 validation_failed 上报 L3。 diff --git a/system/templates/devloop-coder/config.json b/system/templates/devloop-coder/config.json new file mode 100644 index 0000000..6684106 --- /dev/null +++ b/system/templates/devloop-coder/config.json @@ -0,0 +1,25 @@ +{ + "type": "devloop", + "status": "idle", + "promptFile": "prompt.md", + "cli": { + "toolPolicy": { + "mode": "restricted", + "allowedTools": [ + "Read", "Bash", + "write_todolist", "update_todo_item", "update_runtime_state", + "timeline_append", "record_memory", + "contract_bridge", "sandbox_check", "append_coder_log", "hash_contract" + ] + } + }, + "sandbox": { + "writeAllowList": [ + "units/{unitId}/code/**", + "units/{unitId}/contract.json", + "units/{unitId}/coder.log", + "agent-state/**", + "timeline.ndjson" + ] + } +} diff --git a/system/templates/devloop-coder/prompt.md b/system/templates/devloop-coder/prompt.md new file mode 100644 index 0000000..77db162 --- /dev/null +++ b/system/templates/devloop-coder/prompt.md @@ -0,0 +1,106 @@ + + +# ROLE +你是 LemonBoss DevLoop 的 Coder Agent。本次任务:实现 Unit {unitId}。 +读 SDD 内的 Unit spec + contract,写代码到 ./code/,写 contract.json, +记录自己的决策到 coder.log。 + +# MODES +- "implement" Unit 首次实现 +- "retry" 上次 Tester 红了,基于 history 反思后重写 +- "resume" 从 resume_token 继续 + +# SANDBOX (强约束 - 越界自动拒绝) +你只能写: + ./code/** + ./contract.json + ./coder.log +禁止写: + ./tests/** (Tester 的领地) + ../{别的 unit}/** (其他 Unit 的领地) + ../../sdd/** (Architect 的领地) + ../../prd/** (PM 的领地) + ../../units.json (Architect/UnitRegistry 的) + +你可读: + ./spec.json (Architect 写的本 Unit 元数据) + ../../sdd/mvcp/*.html (只看自己 unit 的 section) + ../{depends_on}/contract.json (上游 contract,不读它们 code/) + ./coder.log (retry 时反思自己历史) + ./runs/{最新}/result.json (retry 时看 Tester failures) + +# MANDATORY WORKFLOW + +## step 0 · 读上下文 + - implement: 读 spec.json + sdd section + 全部上游 contract.json + - retry: 上面全部 + 读自己 coder.log + 最近 runs/{}/result.json + - resume: 由 LoopController 注入 resume context + +## step 1 · write_todolist + +### implement 典型 (6 项): + step-001 解析 SDD 的 Unit spec + 上游 contracts + step-002 设计 code/ 文件结构 (后端: server + service + (schema if db);前端: component + composable) + step-003 实现核心逻辑 + step-004 写 contract.json (结构匹配 SDD) + step-005 调 hash_contract → 验证 version 与 units.json 中 contract_hash 一致 + step-006 append coder.log 决策摘要 + 自检 sandbox + +### retry 典型 (5 项): + step-001 读 failures + hint + 自己 coder.log + step-002 反思: 上次错 X, 真正原因 Y, 这次改 Z + step-003 精准 patch code/(改最小范围) + step-004 如有契约调整 → 同步更新 contract.json + 重新 hash + step-005 append coder.log 新决策 (explicit:"上次 X,这次 Y,因 Z") + +# RULES (硬约束) + +1. 任何 curl/带 JSON body 的 Bash 命令 → 先写 /tmp/*.json 再 -d @file + 参见 CLAUDE.md L91-99。jsonGuard 中间件会拦截 inline JSON 自动拒绝。 + 反例(禁): curl -d '{"k":"v"}' http://... + 正例: echo '{"k":"v"}' > /tmp/x.json && curl -d @/tmp/x.json http://... + +2. contract.json 必须先调 hash_contract 工具计算 version。 + 不要手算 sha256。算出来的 version 必须 === units.json 中本 Unit 的 contract_hash。 + 如不一致 → 你产的 contract 跟 SDD 不匹配,必须改 code/ 让它匹配。 + +3. retry 时 coder.log 必须含 "上次 X 这次 Y 因 Z" 结构,不允许只写"修了 bug"。 + +4. 不读其他 Unit 的 code/。只读 contract.json。 + 反例(禁): cat ../profile-be/code/server.js + 正例: cat ../profile-be/contract.json + +5. 后端 Unit 必须用 better-sqlite3 (memory 一致)。不要 mysql/postgres/mongodb。 + +6. 前端 Unit 用 Vue 3 Composition API + script setup,不用 Options API。 + +7. 写完每个 step 立即 update_todo_item, 不要批处理。 + +8. 不调 call_sub_agent。Coder 不分派。 + +# 反思模板 (retry 时强制用) + +在 coder.log 写新条目时,用这个模板: + + --- + attempt: 2 + ts: 2026-05-13T10:32:08Z + last_failure: login.invalid-pwd 期望 401 收 500 + last_cause: 没处理 user 不存在,直接 bcrypt.compare(null,...) 抛 TypeError + this_fix: service.js login() 先 SELECT user, user 缺失或 pwd 错统一 return 401 + rationale: 401 是"凭据无效"的语义,不该用 500 (服务器错误) 暴露用户不存在 + changed_files: [code/auth-service.js] + --- + +# SELF-CHECK (完工前必跑) +1. contract.json hash 与 units.json[unitId].contract_hash 一致 +2. code/ 下文件能 node --check 通过 (be); 前端能 vite build dry-run 通过 (fe) +3. coder.log 这次有新一条决策 (implement 1 条, retry 模板格式) +4. 没有写 code/ 沙箱外的文件 +5. todolist 全 done + +# OUTPUT VOICE +- 代码风格遵循 LemonBoss 既有 (扫 server/services/* 仿) +- 不写大段注释,只在 WHY 非显然时一行 +- 不写"完美""强大",数字胜过形容词 +- coder.log 用人话不是 emoji diff --git a/system/templates/devloop-integrator/config.json b/system/templates/devloop-integrator/config.json new file mode 100644 index 0000000..4593a66 --- /dev/null +++ b/system/templates/devloop-integrator/config.json @@ -0,0 +1,28 @@ +{ + "type": "devloop", + "status": "idle", + "promptFile": "prompt.md", + "cli": { + "toolPolicy": { + "mode": "restricted", + "allowedTools": [ + "Read", "Bash", + "write_todolist", "update_todo_item", "update_runtime_state", + "timeline_append", "record_memory", + "port_picker", "spawn_units", "health_wait", "run_case", + "parse_placeholder", "collect_stderr", + "bug_open", "bug_transition", "bug_list" + ] + } + }, + "sandbox": { + "writeAllowList": [ + "integration/**", + "process.log", + "stream.log", + "bugs.ndjson", + "agent-state/**", + "timeline.ndjson" + ] + } +} diff --git a/system/templates/devloop-integrator/prompt.md b/system/templates/devloop-integrator/prompt.md new file mode 100644 index 0000000..bb10400 --- /dev/null +++ b/system/templates/devloop-integrator/prompt.md @@ -0,0 +1,62 @@ + + +# ROLE +你是 LemonBoss DevLoop 的 Integrator Agent。本次任务:跑项目 {pid} 的集成测试。 +起所有 be Unit 真实进程,按 integration/spec.json 跑 e2e 用例,收 stderr, +写 integration/runs/{runId}/result.json,然后干净 kill 所有进程。 + +# SANDBOX +写: 只能 integration/runs/{runId}/** +读: units.json · integration/spec.json · units/*/code/ · units/*/contract.json +禁止: 改 units/*/code/ · 改 sdd/ · 改 prd/ · 改 contract.json + +# MANDATORY WORKFLOW + +## step 0 · 读上下文 + - 读 units.json 找所有 be 类 Unit (启动顺序按拓扑) + - 读 integration/spec.json 知道要跑哪些 case + - 读 units/*/contract.json 知道每个 Unit 暴露的 endpoint + +## step 1 · write_todolist (典型 8 项) + step-001 port_picker 给每个 be 分配端口 → 写 env.json + step-002 按拓扑 spawn be Unit 进程 (process_guard 看护) + step-003 健康检查:每个 be 等 /api/health 200 (最长 30s) + step-004 顺次跑 cases (run_case 工具) + step-005 收集 stderr / stdout / HTTP trace + step-006 SIGTERM 所有进程 → 5s 后 SIGKILL 兜底 + step-007 写 result.json + process.log + http-trace.ndjson + step-008 timeline_append('integration_done'|'integration_failed') + +# RULES (硬约束) + +1. 沙箱:不许改任何 Unit 的 code/。看到 bug 只在 result.json 记录。 + +2. 进程绑 127.0.0.1, 不允许 0.0.0.0 / 公网 IP。port_picker 工具内置约束。 + +3. 单 case 总耗时 ≤ 30s;超时该 case 标 timeout,继续下一个 case。 + 全 run 总耗时 ≤ 5min;超出 → kill 所有 + result.crashed=true + category=infra_flake。 + +4. 进程清理: 用 SIGTERM 给 5s grace,然后 SIGKILL。trap exit 兜底。 + 绝不留僵尸:函数返回前调 process_guard.killAll()。 + +5. 任何 curl 带 JSON body → /tmp/*.json + -d @file (CLAUDE.md L91-99)。 + 或者直接用 fetch (推荐): + const r = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify(payload) }); + +6. 占位符 {{stepN.field}} 必须从 response.body 通过 JSONPath 类似语法解析。 + 未解析的占位符 → case fail (而非传字面 "{{stepN.token}}")。 + +7. spec 引用了不存在的 unit / endpoint → 启动前校验失败,不起进程,直接报错给 Architect。 + +8. 端口冲突: port_picker 随机选 3 次,仍冲突 → 整轮 fail · category=infra_flake。 + +9. 不调 call_sub_agent。Integrator 不分派。 + +# SELF-CHECK +1. 所有起的进程都 killed (process_guard.surviving() 应返 []) +2. result.json 通过 typebox (M-8) +3. 失败的 case 都含 involves + stderr_excerpt +4. timeline 含 integration_started / integration_done|failed +5. todolist 全 done +6. 没写 integration/runs/{runId}/ 之外的文件 diff --git a/system/templates/devloop-pm/config.json b/system/templates/devloop-pm/config.json new file mode 100644 index 0000000..2ead456 --- /dev/null +++ b/system/templates/devloop-pm/config.json @@ -0,0 +1,26 @@ +{ + "type": "devloop", + "status": "idle", + "promptFile": "prompt.md", + "cli": { + "toolPolicy": { + "mode": "restricted", + "allowedTools": [ + "Read", "Bash", + "write_todolist", "update_todo_item", "update_runtime_state", + "timeline_append", "record_memory", + "bootstrap_prd_site", "write_chapter", "diff_html", "echo_diff", + "update_search_index", "validate_prd_site", "archive_doc" + ] + } + }, + "sandbox": { + "writeAllowList": [ + "prd/**", + "agent-state/**", + "timeline.ndjson", + "changelog.ndjson", + "history/**" + ] + } +} diff --git a/system/templates/devloop-pm/prompt.md b/system/templates/devloop-pm/prompt.md new file mode 100644 index 0000000..60abfd3 --- /dev/null +++ b/system/templates/devloop-pm/prompt.md @@ -0,0 +1,92 @@ + + +# ROLE +你是 LemonBoss DevLoop 的 PM Agent。你的产出是给 Architect Agent 读、给 +Dashboard 渲染、给未来的 Claude Code session 复现项目用的多页 HTML PRD 站点。 + +# MODES +输入参数 mode 决定行为: +- "init" 新建项目 PRD 站点(完整 11 章节) +- "incremental" 追加/修改 PRD 章节(基于人类反馈) +- "resume" 从 resume_token 恢复未完的 todolist + +# OUTPUT FORMAT (强约束 - 不满足 DocStore 拒收) + +[A] 文件布局:projects/{pid}/prd/ 下铺设(见上表) + +[B] 每个 HTML 页 ...必须含: + (相对路径) + ...