Logo
热心市民王先生

实施指南

实施指南 代码示例 最佳实践

Trellis 集成的实施指南,包含配置文件、代码示例、最佳实践和调试方法

1. 快速开始

1.1 安装与初始化

前提条件

  • Node.js >= 18.0.0
  • pi-mono >= 2.0.0(需要验证具体版本要求)
  • Git >= 2.28.0(支持 worktree 功能)

安装步骤

# 1. 安装 Trellis 集成包(假设发布为 npm 包)
npm install @pi-mono/trellis-integration

# 2. 在项目根目录初始化
npx trellis-integration init

# 3. 或者带参数初始化
npx trellis-integration init --platforms=claude-code,cursor --user=alice

生成的目录结构

project/
├── .trellis/                    # Trellis 配置目录
│   ├── spec/                    # Spec 目录
│   │   ├── project-spec.md      # 项目规范
│   │   ├── coding-standards.md  # 编码标准
│   │   └── workflow.md          # 工作流
│   ├── tasks/                   # 任务目录
│   ├── workspace/               # Workspace 目录
│   │   └── alice/               # 用户 workspace
│   │       ├── journal.md       # Journal
│   │       └── preferences.md   # 偏好设置
│   └── config.yaml              # Trellis 配置文件
├── .claude/                     # Claude Code 适配
│   └── CLAUDE.md               # 从 Trellis Spec 生成
├── .cursor/                     # Cursor 适配
│   └── rules.md                # 从 Trellis Spec 生成
└── AGENTS.md                    # OpenCode 适配

1.2 配置文件

.trellis/config.yaml

# Trellis 集成配置
version: "1.0.0"

# 项目信息
project:
  name: "My Project"
  root: "."

# 启用的平台
platforms:
  - claude-code
  - cursor
  - opencode

# Spec 配置
spec:
  # 基础 Spec(始终注入)
  base:
    - project-spec.md
    - coding-standards.md
  
  # 按任务类型的 Spec 映射
  context:
    feature:
      - architecture/patterns.md
      - domain/api-conventions.md
    bugfix:
      - architecture/error-handling.md
    refactor:
      - coding-standards.md
      - architecture/refactoring-guide.md

# Workspace 配置
workspace:
  # Journal 最大条目数(超过后自动归档)
  maxJournalEntries: 1000
  
  # 会话超时(分钟)
  sessionTimeout: 60
  
  # 自动保存间隔(秒)
  autoSaveInterval: 30

# 任务配置
tasks:
  # 默认分支前缀
  branchPrefix: "task/"
  
  # 是否启用 worktree
  enableWorktree: true
  
  # worktree 基础路径
  worktreeBase: ".git/worktrees"

# 日志配置
logging:
  level: "info"  # debug | info | warn | error
  file: ".trellis/trellis.log"

2. Spec 系统实施

2.1 创建第一个 Spec

.trellis/spec/project-spec.md

# 项目规范

## 项目概述

这是一个基于 pi-mono 的 Agent 助手项目,集成了 Trellis 的结构化工作流。

## 技术栈

- **运行时**: Node.js 18+
- **框架**: pi-mono 2.0+
- **语言**: TypeScript 5.0+
- **包管理**: pnpm / npm

## 目录结构

src/ ├── core/ # 核心逻辑 ├── agents/ # Agent 实现 ├── integrations/ # 外部集成 └── utils/ # 工具函数


## 开发流程

1. 从任务 PRD 开始
2. 加载相关 Spec
3. 实现功能
4. 运行检查清单
5. 提交并更新 Journal

2.2 编码标准 Spec

.trellis/spec/coding-standards.md

# 编码标准

## TypeScript 规范

### 类型系统

- ✅ 始终使用严格类型,避免 `any`
- ✅ 为函数参数和返回值定义明确的类型
- ✅ 使用接口定义对象形状

```typescript
// ✅ 好的做法
interface User {
  id: string;
  name: string;
  email: string;
}

async function getUser(id: string): Promise<User> {
  // 实现
}

// ❌ 不好的做法
async function getUser(id: any): Promise<any> {
  // 实现
}

错误处理

  • ✅ 使用自定义 Error 类
  • ✅ 始终捕获并记录错误
  • ✅ 提供有意义的错误信息
class TaskNotFoundError extends Error {
  constructor(taskId: string) {
    super(`Task not found: ${taskId}`);
    this.name = 'TaskNotFoundError';
  }
}

try {
  const task = await loadTask(taskId);
} catch (error) {
  if (error instanceof TaskNotFoundError) {
    logger.warn(`Task ${taskId} not found`);
  }
  throw error;
}

代码风格

命名规范

  • 变量和函数:camelCase
  • 类和类型:PascalCase
  • 常量和枚举:UPPER_SNAKE_CASE
  • 文件和目录:kebab-case

注释规范

  • ✅ 为公共 API 编写 JSDoc
  • ✅ 解释”为什么”而不是”是什么”
  • ✅ 保持注释与代码同步
/**
 * 加载任务上下文
 * 
 * 从文件系统和数据库加载任务的完整上下文,
 * 包括相关文件、决策记录和代码片段。
 * 
 * @param taskId - 任务 ID
 * @returns 任务上下文
 * @throws {TaskNotFoundError} 当任务不存在时
 */
async function loadTaskContext(taskId: string): Promise<TaskContext> {
  // 实现
}

### 2.3 Spec 注入代码示例

**`src/integrations/trellis/spec-manager.ts`**:

```typescript
import { promises as fs } from 'fs';
import path from 'path';
import { createHash } from 'crypto';

interface Spec {
  id: string;
  name: string;
  path: string;
  type: 'base' | 'architecture' | 'domain' | 'integration';
  content: string;
  metadata: {
    version: string;
    lastUpdated: Date;
    requiredFor: string[];
  };
}

interface InjectedContext {
  specs: Spec[];
  combined: string;
  checksum: string;
  injectedAt: Date;
}

export class SpecManager {
  private specDir: string;
  private cache: Map<string, Spec> = new Map();

  constructor(projectRoot: string) {
    this.specDir = path.join(projectRoot, '.trellis', 'spec');
  }

  async initialize(): Promise<void> {
    await fs.mkdir(this.specDir, { recursive: true });
    await this.loadAllSpecs();
  }

  private async loadAllSpecs(): Promise<void> {
    const files = await fs.readdir(this.specDir, { recursive: true });
    
    for (const file of files) {
      if (file.endsWith('.md')) {
        const spec = await this.loadSpec(file);
        this.cache.set(spec.id, spec);
      }
    }
  }

  private async loadSpec(filePath: string): Promise<Spec> {
    const fullPath = path.join(this.specDir, filePath);
    const content = await fs.readFile(fullPath, 'utf-8');
    
    // 从 frontmatter 提取元数据
    const metadata = this.parseFrontmatter(content);
    
    return {
      id: this.generateId(filePath),
      name: path.basename(filePath, '.md'),
      path: filePath,
      type: this.detectSpecType(filePath),
      content,
      metadata
    };
  }

  async getBaseSpecs(): Promise<Spec[]> {
    return Array.from(this.cache.values())
      .filter(spec => spec.type === 'base');
  }

  async selectContextSpecs(taskType: string): Promise<Spec[]> {
    // 根据任务类型选择 Spec(从 config.yaml 读取映射)
    const config = await this.loadConfig();
    const requiredSpecs = config.spec.context[taskType] || [];
    
    return Array.from(this.cache.values())
      .filter(spec => requiredSpecs.includes(spec.path));
  }

  async injectSpecs(taskType: string, explicitSpecIds?: string[]): Promise<InjectedContext> {
    const baseSpecs = await this.getBaseSpecs();
    const contextSpecs = await this.selectContextSpecs(taskType);
    const explicitSpecs = explicitSpecIds 
      ? explicitSpecIds.map(id => this.cache.get(id)).filter(Boolean) as Spec[]
      : [];

    const allSpecs = this.deduplicate([...baseSpecs, ...contextSpecs, ...explicitSpecs]);
    const combined = this.combineToMarkdown(allSpecs);
    const checksum = this.generateChecksum(allSpecs);

    return {
      specs: allSpecs,
      combined,
      checksum,
      injectedAt: new Date()
    };
  }

  private combineToMarkdown(specs: Spec[]): string {
    return specs.map(spec => {
      return `# ${spec.name}\n\n${spec.content}\n`;
    }).join('\n---\n\n');
  }

  private deduplicate(specs: Spec[]): Spec[] {
    const seen = new Set<string>();
    return specs.filter(spec => {
      if (seen.has(spec.id)) {
        return false;
      }
      seen.add(spec.id);
      return true;
    });
  }

  private generateChecksum(specs: Spec[]): string {
    const hash = createHash('sha256');
    specs.forEach(spec => hash.update(spec.content));
    return hash.digest('hex');
  }

  private generateId(filePath: string): string {
    return path.basename(filePath, '.md')
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .toLowerCase();
  }

  private detectSpecType(filePath: string): string {
    if (filePath.includes('architecture/')) return 'architecture';
    if (filePath.includes('domain/')) return 'domain';
    if (filePath.includes('integration/')) return 'integration';
    return 'base';
  }

  private parseFrontmatter(content: string): any {
    // 简单的 frontmatter 解析(实际应使用 yaml 库)
    const match = content.match(/^---\n([\s\S]*?)\n---/);
    if (!match) return {};
    
    const lines = match[1].split('\n');
    const metadata: any = {};
    
    for (const line of lines) {
      const [key, value] = line.split(':').map(s => s.trim());
      metadata[key] = value;
    }
    
    return metadata;
  }

  private async loadConfig(): Promise<any> {
    const configPath = path.join(path.dirname(this.specDir), 'config.yaml');
    const content = await fs.readFile(configPath, 'utf-8');
    // 实际应使用 yaml 库解析
    return JSON.parse(content); // 简化示例
  }
}

3. 任务管理实施

3.1 创建任务

src/integrations/trellis/task-manager.ts

import { promises as fs } from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

interface TaskPRD {
  title: string;
  description: string;
  acceptanceCriteria: string[];
  priority: 'high' | 'medium' | 'low';
  estimatedHours: number;
}

interface Task {
  id: string;
  prd: TaskPRD;
  type: 'feature' | 'bugfix' | 'refactor' | 'docs';
  status: 'pending' | 'in_progress' | 'blocked' | 'completed' | 'cancelled';
  createdAt: Date;
  updatedAt: Date;
  branch?: string;
  worktree?: string;
}

export class TaskManager {
  private tasksDir: string;
  private config: any;

  constructor(projectRoot: string, config: any) {
    this.tasksDir = path.join(projectRoot, '.trellis', 'tasks');
    this.config = config;
  }

  async initialize(): Promise<void> {
    await fs.mkdir(this.tasksDir, { recursive: true });
  }

  async createTask(prd: TaskPRD, type: string = 'feature'): Promise<Task> {
    const taskId = await this.generateTaskId();
    const taskDir = path.join(this.tasksDir, taskId);
    
    // 创建任务目录
    await fs.mkdir(taskDir, { recursive: true });
    await fs.mkdir(path.join(taskDir, 'context'), { recursive: true });
    
    // 创建任务
    const task: Task = {
      id: taskId,
      prd,
      type: type as any,
      status: 'pending',
      createdAt: new Date(),
      updatedAt: new Date()
    };
    
    // 写入 PRD
    await fs.writeFile(
      path.join(taskDir, 'prd.md'),
      this.formatPRD(prd)
    );
    
    // 写入初始状态
    await this.saveTaskState(taskId, {
      currentStep: 0,
      checkpoints: [],
      errors: [],
      metrics: {
        estimatedHours: prd.estimatedHours,
        actualHours: 0
      }
    });
    
    // 如果启用 worktree,创建工作树
    if (this.config.tasks.enableWorktree) {
      await this.createWorktree(task);
    }
    
    return task;
  }

  private async generateTaskId(): Promise<string> {
    const timestamp = Date.now().toString(36);
    const random = Math.random().toString(36).substring(2, 6);
    return `task-${timestamp}-${random}`;
  }

  private async createWorktree(task: Task): Promise<void> {
    const branch = `${this.config.tasks.branchPrefix}${task.id}`;
    const worktreePath = path.join(
      path.dirname(this.tasksDir),
      '.git',
      'worktrees',
      task.id
    );
    
    // 创建分支
    await execAsync(`git checkout -b ${branch}`);
    
    // 创建 worktree
    await execAsync(
      `git worktree add -b ${branch} ${worktreePath} main`
    );
    
    task.branch = branch;
    task.worktree = worktreePath;
  }

  private formatPRD(prd: TaskPRD): string {
    return `# ${prd.title}

## 描述

${prd.description}

## 验收标准

${prd.acceptanceCriteria.map(c => `- [ ] ${c}`).join('\n')}

## 优先级

${prd.priority}

## 预估工时

${prd.estimatedHours} 小时
`;
  }

  private async saveTaskState(taskId: string, state: any): Promise<void> {
    await fs.writeFile(
      path.join(this.tasksDir, taskId, 'state.json'),
      JSON.stringify(state, null, 2)
    );
  }
}

3.2 任务使用示例

// 初始化任务管理器
const taskManager = new TaskManager(projectRoot, config);
await taskManager.initialize();

// 创建新任务
const task = await taskManager.createTask({
  title: "实现 Spec 自动注入功能",
  description: "基于 Trellis 的 Spec 系统,实现按任务类型智能注入上下文",
  acceptanceCriteria: [
    "基础 Spec 始终注入",
    "根据任务类型选择上下文 Spec",
    "支持显式声明需要的 Spec",
    "注入延迟 < 100ms"
  ],
  priority: 'high',
  estimatedHours: 8
}, 'feature');

console.log(`Created task: ${task.id}`);

4. Journal 系统实施

4.1 Journal 管理

src/integrations/trellis/journal-manager.ts

import { promises as fs } from 'fs';
import path from 'path';

interface JournalEntry {
  id: string;
  taskId: string;
  sessionId: string;
  type: 'decision' | 'issue' | 'solution' | 'note';
  content: string;
  tags: string[];
  createdAt: Date;
  metadata?: {
    file?: string;
    code?: string;
    references?: string[];
  };
}

export class JournalManager {
  private workspaceDir: string;
  private maxEntries: number;

  constructor(projectRoot: string, userId: string, maxEntries: number = 1000) {
    this.workspaceDir = path.join(projectRoot, '.trellis', 'workspace', userId);
    this.maxEntries = maxEntries;
  }

  async initialize(): Promise<void> {
    await fs.mkdir(this.workspaceDir, { recursive: true });
    await fs.mkdir(path.join(this.workspaceDir, 'sessions'), { recursive: true });
  }

  async appendJournal(entry: Omit<JournalEntry, 'id'>): Promise<JournalEntry> {
    const journalEntry: JournalEntry = {
      ...entry,
      id: this.generateEntryId()
    };
    
    // 追加到 journal.md
    await this.appendToJournalFile(journalEntry);
    
    // 如果关联会话,添加到会话文件
    if (entry.sessionId) {
      await this.addToSessionFile(entry.sessionId, journalEntry);
    }
    
    // 检查是否需要归档
    await this.checkArchive();
    
    return journalEntry;
  }

  private async appendToJournalFile(entry: JournalEntry): Promise<void> {
    const journalPath = path.join(this.workspaceDir, 'journal.md');
    const formatted = this.formatEntry(entry);
    
    // 追加写入
    await fs.appendFile(journalPath, formatted);
  }

  private formatEntry(entry: JournalEntry): string {
    const date = entry.createdAt.toISOString();
    const tags = entry.tags.length > 0 ? ` #${entry.tags.join(' #')}` : '';
    
    return `
---
entry: ${entry.id}
task: ${entry.taskId}
session: ${entry.sessionId}
type: ${entry.type}
date: ${date}
tags: ${tags}
---

${entry.content}

${entry.metadata?.file ? `📁 文件:${entry.metadata.file}` : ''}
${entry.metadata?.code ? `\`\`\`\n${entry.metadata.code}\n\`\`\`` : ''}

---
`;
  }

  private async addToSessionFile(sessionId: string, entry: JournalEntry): Promise<void> {
    const sessionPath = path.join(
      this.workspaceDir,
      'sessions',
      `${sessionId}.md`
    );
    
    // 如果会话文件不存在,创建它
    try {
      await fs.access(sessionPath);
    } catch {
      await fs.writeFile(
        sessionPath,
        `# Session ${sessionId}\n\n`
      );
    }
    
    // 追加条目
    await fs.appendFile(sessionPath, this.formatEntry(entry));
  }

  private async checkArchive(): Promise<void> {
    // 读取 journal 条目数
    const journalPath = path.join(this.workspaceDir, 'journal.md');
    const content = await fs.readFile(journalPath, 'utf-8');
    const count = (content.match(/^---$/gm) || []).length / 2;
    
    // 如果超过限制,归档旧条目
    if (count > this.maxEntries) {
      await this.archiveOldEntries(count - this.maxEntries);
    }
  }

  private async archiveOldEntries(count: number): Promise<void> {
    // 实现归档逻辑(简化示例)
    console.log(`Archiving ${count} old journal entries...`);
  }

  async getRecentEntries(sessionCount: number): Promise<JournalEntry[]> {
    const journalPath = path.join(this.workspaceDir, 'journal.md');
    
    try {
      const content = await fs.readFile(journalPath, 'utf-8');
      return this.parseEntries(content).slice(-sessionCount);
    } catch (error) {
      return []; // journal 不存在或为空
    }
  }

  private parseEntries(content: string): JournalEntry[] {
    // 解析 journal.md 中的条目(简化示例)
    // 实际应使用更健壮的解析器
    return [];
  }

  private generateEntryId(): string {
    const timestamp = Date.now().toString(36);
    const random = Math.random().toString(36).substring(2, 6);
    return `journal-${timestamp}-${random}`;
  }
}

5. 平台适配器实施

5.1 Claude Code 适配器

src/integrations/trellis/adapters/claude-code.ts

import { promises as fs } from 'fs';
import { PlatformAdapter, SpecFormat, InjectedContext, Session } from './types';

export class ClaudeCodeAdapter implements PlatformAdapter {
  getName(): string {
    return 'Claude Code';
  }

  getVersion(): string {
    return '1.0.0';
  }

  getSpecFormat(): SpecFormat {
    return {
      type: 'markdown',
      location: '.claude/CLAUDE.md',
      format: 'full-file',
      encoding: 'utf-8'
    };
  }

  async injectContext(context: InjectedContext): Promise<void> {
    const specPath = '.claude/CLAUDE.md';
    
    // 尝试读取现有内容
    let existingContent = '';
    try {
      const content = await fs.readFile(specPath, 'utf-8');
      // 保留用户自定义部分(在 USER_CUSTOM 标记后)
      const match = content.match(/<!-- USER_CUSTOM:([\s\S]*)/);
      if (match) {
        existingContent = match[1].trim();
      }
    } catch (error) {
      // 文件不存在,忽略
    }
    
    // 组合 Spec
    const combined = this.combineSpecs(context.combined, existingContent);
    
    // 确保目录存在
    await fs.mkdir('.claude', { recursive: true });
    
    // 写入文件
    await fs.writeFile(specPath, combined, 'utf-8');
  }

  private combineSpecs(trellisSpec: string, userCustom: string): string {
    return `# Claude Code Spec (Managed by Trellis)

<!-- TRELLIS_MANAGED: DO NOT EDIT MANUALLY -->
<!-- This file is auto-generated. Edit .trellis/spec/*.md instead. -->

${trellisSpec}

<!-- USER_CUSTOM: Your custom rules below -->
${userCustom}
`;
  }

  getSessionConfig(): any {
    return {
      maxTokens: 4096,
      temperature: 0.7,
      systemPrompt: 'You are a helpful coding assistant.'
    };
  }

  async onSessionStart?(session: Session): Promise<void> {
    console.log(`Claude Code session started: ${session.id}`);
  }

  async onSessionEnd?(session: Session): Promise<void> {
    console.log(`Claude Code session ended: ${session.id}`);
  }
}

5.2 适配器注册

src/integrations/trellis/adapter-registry.ts

import { PlatformAdapter } from './types';
import { ClaudeCodeAdapter } from './adapters/claude-code';
import { CursorAdapter } from './adapters/cursor';
import { OpenCodeAdapter } from './adapters/opencode';

export class AdapterRegistry {
  private adapters: Map<string, PlatformAdapter> = new Map();

  constructor() {
    this.registerBuiltInAdapters();
  }

  private registerBuiltInAdapters(): void {
    this.register(new ClaudeCodeAdapter());
    this.register(new CursorAdapter());
    this.register(new OpenCodeAdapter());
  }

  register(adapter: PlatformAdapter): void {
    const name = adapter.getName().toLowerCase();
    this.adapters.set(name, adapter);
  }

  get(platform: string): PlatformAdapter | undefined {
    return this.adapters.get(platform.toLowerCase());
  }

  list(): string[] {
    return Array.from(this.adapters.keys());
  }
}

// 使用示例
const registry = new AdapterRegistry();
const claudeAdapter = registry.get('claude-code');
if (claudeAdapter) {
  await claudeAdapter.injectContext(context);
}

6. 最佳实践

6.1 Spec 编写最佳实践

DO(推荐)

  • ✅ 使用清晰的分层结构(base/architecture/domain/integration)
  • ✅ 为每个 Spec 编写明确的元数据
  • ✅ 保持 Spec 的原子性(一个 Spec 一个主题)
  • ✅ 使用版本控制管理 Spec 变更
  • ✅ 定期评审和更新 Spec

DON’T(避免)

  • ❌ 把所有规则写进一个大文件
  • ❌ 使用模糊的、主观的规则描述
  • ❌ 忘记更新过时的 Spec
  • ❌ 在 Spec 中混入具体实现细节

6.2 Journal 记录最佳实践

应该记录的内容

  • ✅ 关键决策和原因
  • ✅ 遇到的问题和解决方案
  • ✅ 意外的发现和洞见
  • ✅ 待办和后续行动项

不应该记录的内容

  • ❌ 所有细碎的对话
  • ❌ 临时的、未经验证的想法
  • ❌ 敏感的、私密的信息

6.3 任务管理最佳实践

任务粒度

  • 理想大小:4-8 小时
  • 最大不超过:1 天
  • 最小不低于:1 小时

任务命名

  • ✅ “实现用户登录 API”
  • ✅ “修复内存泄漏问题”
  • ❌ “更新代码”(太模糊)
  • ❌ “修复所有 bug 并实现新功能”(太大)

7. 调试与故障排除

7.1 常见问题

问题 1:Spec 注入失败

症状:Agent 无法加载 Spec 上下文

排查步骤:

  1. 检查 .trellis/spec/ 目录是否存在
  2. 验证 Spec 文件格式(必须有有效的 Markdown)
  3. 查看 .trellis/trellis.log 日志
  4. 确认配置文件 .trellis/config.yaml 正确

问题 2:任务创建失败

症状:创建任务时报错

排查步骤:

  1. 检查 git 仓库是否初始化
  2. 确认 worktree 功能可用(git worktree --version
  3. 验证 .trellis/tasks/ 目录权限
  4. 查看任务管理器日志

问题 3:Journal 条目丢失

症状:Journal 条目未保存或消失

排查步骤:

  1. 检查 journal.md 文件权限
  2. 确认归档逻辑没有误删
  3. 查看会话文件是否正常创建
  4. 验证文件系统是否正常

7.2 日志配置

启用详细日志:

# .trellis/config.yaml
logging:
  level: "debug"  # debug | info | warn | error
  file: ".trellis/trellis.log"

查看日志:

# 实时查看日志
tail -f .trellis/trellis.log

# 搜索错误
grep ERROR .trellis/trellis.log

参考资料