实施指南
实施指南 代码示例 最佳实践
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 上下文
排查步骤:
- 检查
.trellis/spec/目录是否存在 - 验证 Spec 文件格式(必须有有效的 Markdown)
- 查看
.trellis/trellis.log日志 - 确认配置文件
.trellis/config.yaml正确
问题 2:任务创建失败
症状:创建任务时报错
排查步骤:
- 检查 git 仓库是否初始化
- 确认 worktree 功能可用(
git worktree --version) - 验证
.trellis/tasks/目录权限 - 查看任务管理器日志
问题 3:Journal 条目丢失
症状:Journal 条目未保存或消失
排查步骤:
- 检查
journal.md文件权限 - 确认归档逻辑没有误删
- 查看会话文件是否正常创建
- 验证文件系统是否正常
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