/** * 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, };