loop-controller.js 3.72 KB
/**
 * 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,
};