refactor: merge promoter into tester — meta workflow now coder → tester → END
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
This commit is contained in:
@@ -19,7 +19,6 @@ import { createWorkflowTicker } from '../workflows/index.js';
|
||||
import { createMetaWorkflow } from '../workflows/meta.js';
|
||||
import { createCursorRunner } from '../workflows/roles/agent-executor.js';
|
||||
import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js';
|
||||
import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js';
|
||||
import { createMetaTesterRole } from '../workflows/roles/meta-tester.js';
|
||||
import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js';
|
||||
|
||||
@@ -79,11 +78,6 @@ const codingRule = createWorkflowRule(codingWf, store, logStore);
|
||||
const metaWf = createMetaWorkflow({
|
||||
coder: createMetaCoderRole(cursorRunner, llm, ENGINE_DIR),
|
||||
tester: createMetaTesterRole({ repoDir: ENGINE_DIR }),
|
||||
promoter: createMetaPromoterRole({
|
||||
repoDir: ENGINE_DIR,
|
||||
remote: 'origin',
|
||||
branch: 'main',
|
||||
}),
|
||||
});
|
||||
const metaRule = createWorkflowRule(metaWf, store, logStore);
|
||||
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/**
|
||||
* Meta Workflow e2e — let meta-architect analyze, then meta-coder (Cursor) implement.
|
||||
* Optimizes the existing coding workflow based on architect's spec.
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { mkdtempSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createOpenAiLlmClient } from '../llm-client.js';
|
||||
import { createStore } from '../store.js';
|
||||
import { createMetaWorkflow } from '../workflows/meta.js';
|
||||
import { createCursorRunner } from '../workflows/roles/agent-executor.js';
|
||||
import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js';
|
||||
import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js';
|
||||
|
||||
const REPO_DIR = join(import.meta.dir, '../../../..');
|
||||
|
||||
async function main() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'meta-e2e-'));
|
||||
console.log(`📁 Temp dir: ${dir}`);
|
||||
|
||||
const apiKey = process.env.DASHSCOPE_API_KEY;
|
||||
if (!apiKey) throw new Error('DASHSCOPE_API_KEY required');
|
||||
|
||||
const cursorKey = process.env.CURSOR_API_KEY;
|
||||
if (!cursorKey) throw new Error('CURSOR_API_KEY required');
|
||||
|
||||
const llm = createOpenAiLlmClient({
|
||||
apiKey,
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
model: 'qwen-plus',
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
|
||||
const store = createStore({
|
||||
eventsDbPath: join(dir, 'events.db'),
|
||||
objectsDir: join(dir, 'objects'),
|
||||
});
|
||||
|
||||
const cursorRunner = createCursorRunner({
|
||||
agentBin: `${process.env.HOME}/.local/bin/agent`,
|
||||
});
|
||||
const coder = createMetaCoderRole(cursorRunner, llm, REPO_DIR);
|
||||
|
||||
// Stub roles we won't run yet
|
||||
const stubRole = async () => ({
|
||||
content: 'stub - not reached',
|
||||
meta: {} as any,
|
||||
});
|
||||
|
||||
const wf = createMetaWorkflow({
|
||||
coder,
|
||||
tester: stubRole as any,
|
||||
promoter: stubRole as any,
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
|
||||
// Read current source for context
|
||||
const codingSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/coding.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const architectSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/roles/architect-llm.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const coderSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/roles/coder-cursor.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const reviewerSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/roles/reviewer-cursor.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const taskDescription = `# 优化 coding workflow
|
||||
|
||||
## 问题
|
||||
1. architect 只输出笼统 analysis + 猜测 targetFiles,coder 几乎不看
|
||||
2. reviewer 不参考 architect 的设计意图
|
||||
3. reviewer rejected 后回到 coder,但 coder 看不到 rejection 原因
|
||||
4. 没有 retry 上限
|
||||
|
||||
## 现有源码
|
||||
|
||||
### coding.ts (moderator + types)
|
||||
\`\`\`typescript
|
||||
${codingSrc}
|
||||
\`\`\`
|
||||
|
||||
### architect-llm.ts
|
||||
\`\`\`typescript
|
||||
${architectSrc}
|
||||
\`\`\`
|
||||
|
||||
### coder-cursor.ts
|
||||
\`\`\`typescript
|
||||
${coderSrc}
|
||||
\`\`\`
|
||||
|
||||
### reviewer-cursor.ts
|
||||
\`\`\`typescript
|
||||
${reviewerSrc}
|
||||
\`\`\`
|
||||
|
||||
## 要求
|
||||
1. ArchitectMeta 扩充:targetFiles + changes (per-file) + verification (验收标准)
|
||||
2. architect prompt 输出结构化 JSON
|
||||
3. coder 消费 architect 的 changes + verification,rejection 时展示 review 反馈
|
||||
4. reviewer 对照 architect 的 changes + verification 审查
|
||||
5. ReviewerMeta 加 rejectionReason + retryCount
|
||||
6. moderator 加 retry 上限 3 次
|
||||
7. 更新 mock 默认实现匹配新 meta 类型
|
||||
8. 所有引用 ArchitectMeta/ReviewerMeta 的文件都要更新(如 e2e 文件)
|
||||
|
||||
## 约束
|
||||
- 不改 workflow-rule-adapter.ts 和 workflow-type.ts
|
||||
- 28 测试必须全过
|
||||
- commit author: 小橘 <xiaoju@shazhou.work>
|
||||
`;
|
||||
|
||||
const hash = await store.putObject(taskDescription);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'optimize-coding',
|
||||
hash,
|
||||
});
|
||||
|
||||
// Step 1: Architect
|
||||
console.log('🏗️ Step 1: meta-architect (LLM) analyzing...\n');
|
||||
const r1 = await rule.tick();
|
||||
if (r1.executed.length > 0) {
|
||||
console.log('✅ Architect done');
|
||||
console.log('Spec:', r1.executed[0].content.slice(0, 500), '...\n');
|
||||
} else {
|
||||
console.log('❌ Architect failed');
|
||||
await store.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Coder (Cursor Agent)
|
||||
console.log('💻 Step 2: meta-coder (Cursor Agent) implementing...\n');
|
||||
const r2 = await rule.tick();
|
||||
if (r2.executed.length > 0) {
|
||||
console.log('✅ Coder done');
|
||||
console.log('Output:', r2.executed[0].content.slice(0, 500));
|
||||
console.log('Meta:', JSON.stringify(r2.executed[0].meta, null, 2));
|
||||
} else {
|
||||
console.log('❌ Coder not triggered');
|
||||
}
|
||||
|
||||
await store.close();
|
||||
console.log('\n🏁 Done');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* Meta Workflow full e2e — simplified coder → tester → promoter flow.
|
||||
*
|
||||
* Flow: coder(Agent) → tester(code) → promoter(git)
|
||||
* On failure: revert coding changes, fix meta, rerun.
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createOpenAiLlmClient } from '../llm-client.js';
|
||||
import { createStore } from '../store.js';
|
||||
import { createMetaWorkflow } from '../workflows/meta.js';
|
||||
import { createCursorRunner } from '../workflows/roles/agent-executor.js';
|
||||
import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js';
|
||||
import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js';
|
||||
import { createMetaTesterRole } from '../workflows/roles/meta-tester.js';
|
||||
import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js';
|
||||
|
||||
const REPO_DIR = join(import.meta.dir, '../../../..');
|
||||
|
||||
function readSrc(relPath: string): string {
|
||||
return readFileSync(join(REPO_DIR, relPath), 'utf-8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'meta-full-'));
|
||||
console.log(`📁 Temp dir: ${dir}`);
|
||||
console.log(`📂 Repo: ${REPO_DIR}\n`);
|
||||
|
||||
const apiKey = process.env.DASHSCOPE_API_KEY;
|
||||
if (!apiKey) throw new Error('DASHSCOPE_API_KEY required');
|
||||
|
||||
const llm = createOpenAiLlmClient({
|
||||
apiKey,
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
model: 'qwen-plus',
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
|
||||
const store = createStore({
|
||||
eventsDbPath: join(dir, 'events.db'),
|
||||
objectsDir: join(dir, 'objects'),
|
||||
});
|
||||
|
||||
const cursorRunner = createCursorRunner({
|
||||
agentBin: `${process.env.HOME}/.local/bin/agent`,
|
||||
});
|
||||
|
||||
const wf = createMetaWorkflow({
|
||||
coder: createMetaCoderRole(cursorRunner, llm, REPO_DIR),
|
||||
tester: createMetaTesterRole({ repoDir: REPO_DIR }),
|
||||
promoter: createMetaPromoterRole({
|
||||
repoDir: REPO_DIR,
|
||||
remote: 'gitflare',
|
||||
branch: 'main',
|
||||
}),
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
|
||||
// Build task description with all relevant source
|
||||
const taskDescription = `# 优化 coding workflow
|
||||
|
||||
## 问题
|
||||
1. architect 只输出笼统 analysis + 猜测 targetFiles,coder 几乎不看
|
||||
2. reviewer 不参考 architect 的设计意图
|
||||
3. reviewer rejected 后回到 coder,但 coder 看不到 rejection 原因
|
||||
4. 没有 retry 上限
|
||||
|
||||
## 现有源码
|
||||
|
||||
### coding.ts (moderator + types + mock roles)
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/coding.ts')}
|
||||
\`\`\`
|
||||
|
||||
### architect-llm.ts
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/roles/architect-llm.ts')}
|
||||
\`\`\`
|
||||
|
||||
### coder-cursor.ts
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/roles/coder-cursor.ts')}
|
||||
\`\`\`
|
||||
|
||||
### reviewer-cursor.ts
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/roles/reviewer-cursor.ts')}
|
||||
\`\`\`
|
||||
|
||||
## 要求
|
||||
1. ArchitectMeta 扩充:targetFiles + changes(per-file改动列表) + verification(验收标准)
|
||||
2. architect prompt 输出结构化 JSON,用 tool_choice required
|
||||
3. coder 消费 architect 的 changes + verification;rejection 时展示上次 review 反馈
|
||||
4. reviewer 对照 architect 的 changes + verification 审查
|
||||
5. ReviewerMeta 加 rejectionReason(string[]) + retryCount(number)
|
||||
6. moderator 加 retry 上限 3 次(retryCount >= 3 强制进 closer)
|
||||
7. 更新所有 mock 默认实现匹配新 meta 类型
|
||||
8. 所有引用 ArchitectMeta/ReviewerMeta 的文件都要更新
|
||||
|
||||
## 约束
|
||||
- 不改 workflow-rule-adapter.ts 和 workflow-type.ts
|
||||
- 28 个 workflow 测试必须全过(toMatchObject 断言)
|
||||
- commit author: 小橘 <xiaoju@shazhou.work>
|
||||
`;
|
||||
|
||||
const hash = await store.putObject(taskDescription);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'optimize-coding',
|
||||
hash,
|
||||
});
|
||||
|
||||
// Run the full pipeline — adapter handles role chaining
|
||||
const MAX_TICKS = 10; // safety valve
|
||||
for (let i = 0; i < MAX_TICKS; i++) {
|
||||
console.log(`\n── tick ${i + 1} ──────────────────────────`);
|
||||
const result = await rule.tick();
|
||||
|
||||
if (result.executed.length === 0) {
|
||||
console.log('⏹️ No more roles to execute. Workflow complete or stuck.');
|
||||
break;
|
||||
}
|
||||
|
||||
for (const ex of result.executed) {
|
||||
console.log(`✅ ${ex.role} done`);
|
||||
console.log(` content: ${ex.content.slice(0, 200)}...`);
|
||||
console.log(` meta: ${JSON.stringify(ex.meta)}`);
|
||||
}
|
||||
|
||||
// Check if we hit END
|
||||
const events = await store.getAfter(0);
|
||||
const lastEvent = events[events.length - 1];
|
||||
if (lastEvent?.kind === 'meta.promoter') {
|
||||
console.log('\n🎉 Workflow completed — promoter finished!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await store.close();
|
||||
console.log('\n🏁 Done');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Meta Workflow live test — optimize coding workflow.
|
||||
*
|
||||
* Uses meta-architect (LLM) to analyze the problem and design improvements.
|
||||
* Skips coder/reviewer/tester/promoter (just architect for now).
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { mkdtempSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createOpenAiLlmClient } from '../llm-client.js';
|
||||
import { createStore } from '../store.js';
|
||||
import type {
|
||||
MetaCoderMeta,
|
||||
MetaPromoterMeta,
|
||||
MetaTesterMeta,
|
||||
} from '../workflows/meta.js';
|
||||
import { createMetaWorkflow } from '../workflows/meta.js';
|
||||
import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js';
|
||||
|
||||
const REPO_DIR = join(import.meta.dir, '../../../..');
|
||||
|
||||
async function main() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'meta-live-'));
|
||||
console.log(`📁 Temp dir: ${dir}`);
|
||||
|
||||
const store = createStore({
|
||||
eventsDbPath: join(dir, 'events.db'),
|
||||
objectsDir: join(dir, 'objects'),
|
||||
});
|
||||
|
||||
const llm = createOpenAiLlmClient({
|
||||
apiKey: process.env.DASHSCOPE_API_KEY!,
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
model: 'qwen-plus',
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
|
||||
const stubRole = async () => ({
|
||||
content: 'stub',
|
||||
meta: {} as any,
|
||||
});
|
||||
|
||||
const wf = createMetaWorkflow({
|
||||
coder: stubRole as any,
|
||||
tester: stubRole as any,
|
||||
promoter: stubRole as any,
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
|
||||
// Read current coding workflow source for context
|
||||
const codingSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/coding.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const architectSrc = readFileSync(
|
||||
join(REPO_DIR, 'packages/pulse/src/workflows/roles/architect-llm.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const taskDescription = `# 优化 coding workflow 的 architect role
|
||||
|
||||
## 问题
|
||||
当前 architect role 几乎没用:
|
||||
1. 只输出一个笼统的 analysis 文本 + 猜测的 targetFiles
|
||||
2. coder 几乎不看 architect 的输出——直接读 task description
|
||||
3. reviewer 也不参考 architect 的设计意图
|
||||
4. 没有 retry 机制:reviewer rejected 后回到 coder,但 coder 看不到 rejection 原因
|
||||
|
||||
## 现有源码
|
||||
|
||||
### coding.ts (moderator + types)
|
||||
\`\`\`typescript
|
||||
${codingSrc}
|
||||
\`\`\`
|
||||
|
||||
### architect-llm.ts (architect role)
|
||||
\`\`\`typescript
|
||||
${architectSrc}
|
||||
\`\`\`
|
||||
|
||||
## 期望
|
||||
1. architect 输出结构化的改动方案(哪些文件、改什么、验证标准)
|
||||
2. coder 消费 architect 的方案
|
||||
3. reviewer 对照 architect 的方案审查
|
||||
4. reviewer rejected 时 coder 能看到具体 rejection 原因
|
||||
5. 可选:加 retry 上限(防止无限循环)
|
||||
`;
|
||||
|
||||
const hash = await store.putObject(taskDescription);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'optimize-coding',
|
||||
hash,
|
||||
});
|
||||
|
||||
console.log('🚀 Running meta-architect...\n');
|
||||
|
||||
const result = await rule.tick();
|
||||
|
||||
if (result.executed.length > 0) {
|
||||
const arch = result.executed[0];
|
||||
console.log(
|
||||
`\n✅ Architect executed: role=${arch.role}, topic=${arch.topicId}`,
|
||||
);
|
||||
console.log('\n📋 Workflow Spec:');
|
||||
console.log(arch.content);
|
||||
console.log('\n📊 Meta:', JSON.stringify(arch.meta, null, 2));
|
||||
} else {
|
||||
console.log('❌ No role executed');
|
||||
}
|
||||
|
||||
await store.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
* Meta Workflow e2e — TDD 重构 coding workflow
|
||||
*
|
||||
* 让 meta workflow (architect→coder→reviewer→tester→promoter) 把 coding workflow
|
||||
* 从现有流程改造为 TDD 驱动流程。
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { mkdtempSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createOpenAiLlmClient } from '../llm-client.js';
|
||||
import { createStore } from '../store.js';
|
||||
import { createMetaWorkflow } from '../workflows/meta.js';
|
||||
import { createCursorRunner } from '../workflows/roles/agent-executor.js';
|
||||
import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js';
|
||||
import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js';
|
||||
import { createMetaTesterRole } from '../workflows/roles/meta-tester.js';
|
||||
import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js';
|
||||
|
||||
const REPO_DIR = join(import.meta.dir, '../../../..');
|
||||
|
||||
function readSrc(relPath: string): string {
|
||||
return readFileSync(join(REPO_DIR, relPath), 'utf-8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'meta-tdd-'));
|
||||
console.log(`📁 Temp dir: ${dir}`);
|
||||
console.log(`📂 Repo: ${REPO_DIR}\n`);
|
||||
|
||||
const apiKey = process.env.DASHSCOPE_API_KEY;
|
||||
if (!apiKey) throw new Error('DASHSCOPE_API_KEY required');
|
||||
|
||||
const llm = createOpenAiLlmClient({
|
||||
apiKey,
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
model: 'qwen-plus',
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
|
||||
const store = createStore({
|
||||
eventsDbPath: join(dir, 'events.db'),
|
||||
objectsDir: join(dir, 'objects'),
|
||||
});
|
||||
|
||||
const cursorRunner = createCursorRunner({
|
||||
agentBin: `${process.env.HOME}/.local/bin/agent`,
|
||||
});
|
||||
|
||||
const wf = createMetaWorkflow({
|
||||
coder: createMetaCoderRole(cursorRunner, llm, REPO_DIR),
|
||||
tester: createMetaTesterRole({ repoDir: REPO_DIR }),
|
||||
promoter: createMetaPromoterRole({
|
||||
repoDir: REPO_DIR,
|
||||
remote: 'gitflare',
|
||||
branch: 'main',
|
||||
}),
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store, undefined);
|
||||
|
||||
const taskDescription = `# 重构 coding workflow 为 TDD 驱动流程
|
||||
|
||||
## 目标
|
||||
将现有 coding workflow (architect→coder→reviewer→closer) 重构为完整的 TDD 驱动开发流程。
|
||||
|
||||
## 新流程设计
|
||||
\`\`\`
|
||||
START
|
||||
→ test-planner (LLM) # 从需求出发写测试方案文档
|
||||
→ test-reviewer (LLM) # review 测试方案是否覆盖需求
|
||||
→ rejected → test-planner(带反馈修改)
|
||||
→ approved ↓
|
||||
→ test-coder (Cursor) # 按测试方案写自动化测试用例(红灯)
|
||||
→ coder (Cursor) # 写功能代码 + 部署/使用说明(绿灯)
|
||||
→ auto-tester (代码) # bun test 跑自动化测试
|
||||
→ fail → coder(迭代代码)
|
||||
→ pass ↓
|
||||
→ manual-tester (Agent) # Agent 读测试文档照着操作验证
|
||||
→ fail → coder(迭代代码)
|
||||
→ pass ↓
|
||||
→ reviewer (Cursor) # review 功能代码 + 测试用例
|
||||
→ rejected → coder(迭代)
|
||||
→ approved ↓
|
||||
→ closer → END
|
||||
\`\`\`
|
||||
|
||||
## 核心设计原则
|
||||
1. **测试文档是一等公民** — test-planner 产出的测试文档贯穿全流程
|
||||
2. **测试先行** — test-coder 先写红灯测试,coder 再写代码让它绿
|
||||
3. **三处消费测试文档** — test-coder(写自动化测试)、manual-tester(手工验证)、reviewer(审查测试质量)
|
||||
4. **coder 产出含部署说明** — manual-tester 需要知道怎么操作
|
||||
5. **Agent 实操验证** — manual-tester 不是 LLM 读代码,是真正的 Agent 按文档操作
|
||||
|
||||
## 新 Role 定义
|
||||
|
||||
### test-planner (LLM, tool_choice: required)
|
||||
- 输入:需求描述(从 __start__ content)
|
||||
- 输出 meta:{ testPlan: string(markdown 测试方案), scenarios: string[](测试场景列表)}
|
||||
- 产出完整测试方案文档,包含:功能测试点、边界条件、异常场景、验收标准
|
||||
|
||||
### test-reviewer (LLM)
|
||||
- 输入:test-planner 的测试方案 + 原始需求
|
||||
- 输出 meta:{ verdict: 'approved'|'rejected', feedback: string }
|
||||
- 检查测试方案是否充分覆盖需求
|
||||
|
||||
### test-coder (Cursor Agent)
|
||||
- 输入:approved 的测试方案
|
||||
- 输出 meta:{ testFiles: string[], testCount: number }
|
||||
- 按测试方案写自动化测试用例(此时功能代码未实现,测试应全部失败 = 红灯)
|
||||
|
||||
### coder (Cursor Agent)
|
||||
- 输入:测试方案 + 红灯测试代码 + rejection feedback(如有)
|
||||
- 输出 meta:{ filesChanged: string[], deploymentGuide: string }
|
||||
- 写功能代码让测试变绿 + 写部署/使用说明供 manual-tester 使用
|
||||
|
||||
### auto-tester (代码)
|
||||
- 输入:coder 完成后
|
||||
- 输出 meta:{ pass: boolean, failedTests: string[], output: string }
|
||||
- 执行 \`bun test\`,解析结果
|
||||
|
||||
### manual-tester (Agent/LLM)
|
||||
- 输入:test-planner 的测试文档 + coder 的部署说明
|
||||
- 输出 meta:{ pass: boolean, issues: string[] }
|
||||
- 按测试文档逐项验证(当前阶段用 LLM 静态分析,未来接 OC Agent 实操)
|
||||
|
||||
### reviewer (Cursor Agent)
|
||||
- 输入:功能代码 + 测试代码 + 测试方案
|
||||
- 输出 meta:{ verdict: 'approved'|'rejected', comments: string, codeQuality: string, testQuality: string }
|
||||
- 同时 review 功能代码质量和测试用例质量
|
||||
|
||||
### closer (代码)
|
||||
- 输入:reviewer approved
|
||||
- 输出 meta:null
|
||||
- 生成完成报告
|
||||
|
||||
## Moderator 状态机
|
||||
\`\`\`typescript
|
||||
START → 'test-planner'
|
||||
'test-planner' → 'test-reviewer'
|
||||
'test-reviewer' → verdict === 'approved' ? 'test-coder' : 'test-planner'
|
||||
'test-coder' → 'coder'
|
||||
'coder' → 'auto-tester'
|
||||
'auto-tester' → pass ? 'manual-tester' : 'coder'
|
||||
'manual-tester' → pass ? 'reviewer' : 'coder'
|
||||
'reviewer' → verdict === 'approved' ? 'closer' : 'coder'
|
||||
'closer' → END
|
||||
|
||||
// 紧急收敛:remainingRounds <= 1 时跳 closer
|
||||
\`\`\`
|
||||
|
||||
## limits
|
||||
\`\`\`typescript
|
||||
limits: { maxRounds: 25 }
|
||||
\`\`\`
|
||||
|
||||
## 现有源码(参考)
|
||||
|
||||
### coding.ts (当前 workflow 定义)
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/coding.ts')}
|
||||
\`\`\`
|
||||
|
||||
### coding.test.ts (当前测试)
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/coding.test.ts')}
|
||||
\`\`\`
|
||||
|
||||
### workflow-type.ts (接口定义,不要修改)
|
||||
\`\`\`typescript
|
||||
${readSrc('packages/pulse/src/workflows/workflow-type.ts')}
|
||||
\`\`\`
|
||||
|
||||
## 实现约束
|
||||
- 不修改 workflow-rule-adapter.ts 和 workflow-type.ts
|
||||
- 新文件:coding-tdd.ts + coding-tdd.test.ts(不覆盖原 coding.ts)
|
||||
- Role 先用 mock 实现(后续再接真实 LLM/Cursor)
|
||||
- 测试用 coding.test.ts 为模板,覆盖所有状态转换
|
||||
- Build: \`cd packages/pulse && bun run build\`
|
||||
- Test: \`bun test packages/pulse/src/workflows/\`
|
||||
- Commit author: 小橘 <xiaoju@shazhou.work>
|
||||
- Commit msg: \`feat: TDD coding workflow (coding-tdd) with test-first pipeline\`
|
||||
- Push: \`git push gitflare main --no-verify\`
|
||||
|
||||
## 验收标准
|
||||
1. coding-tdd.ts 导出 createTddCodingWorkflow()
|
||||
2. 8 个 role 全部有 mock 实现
|
||||
3. moderator 状态机正确(含 test-reviewer rejection 回环、auto-tester/manual-tester fail 回环、reviewer rejection 回环)
|
||||
4. remainingRounds <= 1 紧急收敛
|
||||
5. limits.maxRounds = 25
|
||||
6. coding-tdd.test.ts 覆盖所有转换路径
|
||||
7. 所有现有测试不 break
|
||||
8. bun run build 通过
|
||||
`;
|
||||
|
||||
const hash = await store.putObject(taskDescription);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'tdd-coding-workflow',
|
||||
hash,
|
||||
});
|
||||
|
||||
const MAX_TICKS = 12;
|
||||
for (let i = 0; i < MAX_TICKS; i++) {
|
||||
console.log(`\n── tick ${i + 1} ──────────────────────────`);
|
||||
const result = await rule.tick();
|
||||
|
||||
if (result.executed.length === 0) {
|
||||
console.log('⏹️ No more roles to execute. Workflow complete or stuck.');
|
||||
break;
|
||||
}
|
||||
|
||||
for (const ex of result.executed) {
|
||||
console.log(`✅ ${ex.role} done`);
|
||||
console.log(` content: ${ex.content.slice(0, 300)}...`);
|
||||
console.log(` meta: ${JSON.stringify(ex.meta)}`);
|
||||
}
|
||||
|
||||
const events = await store.getAfter(0);
|
||||
const lastEvent = events[events.length - 1];
|
||||
if (lastEvent?.kind === 'meta.promoter') {
|
||||
console.log('\n🎉 Workflow completed — promoter finished!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await store.close();
|
||||
console.log('\n🏁 Done');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
} from './coding.js';
|
||||
export type {
|
||||
MetaCoderMeta,
|
||||
MetaPromoterMeta,
|
||||
MetaTesterMeta,
|
||||
} from './meta.js';
|
||||
export { createMetaWorkflow } from './meta.js';
|
||||
@@ -36,7 +35,6 @@ export type {
|
||||
} from './roles/llm-role-factory.js';
|
||||
export { createLlmRole, createToolRole } from './roles/llm-role-factory.js';
|
||||
export { createMetaCoderRole } from './roles/meta-coder-cursor.js';
|
||||
export { createMetaPromoterRole } from './roles/meta-promoter.js';
|
||||
export { createMetaTesterRole } from './roles/meta-tester.js';
|
||||
export { createRendererRole } from './roles/renderer-template.js';
|
||||
export { createReviewerRole } from './roles/reviewer-cursor.js';
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createStore } from '../store.js';
|
||||
import {
|
||||
createMetaWorkflow,
|
||||
type MetaCoderMeta,
|
||||
type MetaPromoterMeta,
|
||||
type MetaTesterMeta,
|
||||
} from './meta.js';
|
||||
import { createWorkflowRule } from './workflow-rule-adapter.js';
|
||||
@@ -26,7 +25,7 @@ function mockStore() {
|
||||
}
|
||||
|
||||
describe('Meta Workflow', () => {
|
||||
test('moderator routes: START→coder→tester(pass)→promoter→END', () => {
|
||||
test('moderator routes: START→coder→tester(pass)→END', () => {
|
||||
const wf = createMetaWorkflow({
|
||||
coder: async () => ({
|
||||
content: 'ok',
|
||||
@@ -36,10 +35,6 @@ describe('Meta Workflow', () => {
|
||||
content: 'pass',
|
||||
meta: { pass: true, reason: '编译通过,测试全绿' },
|
||||
}),
|
||||
promoter: async () => ({
|
||||
content: 'done',
|
||||
meta: { commitHash: 'abc123', pushed: true },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(wf.moderator({ role: START, meta: null }, 'x')).toBe('coder');
|
||||
@@ -54,19 +49,13 @@ describe('Meta Workflow', () => {
|
||||
{ role: 'tester', meta: { pass: true, reason: '通过' } },
|
||||
'x',
|
||||
),
|
||||
).toBe('promoter');
|
||||
).toBe(END);
|
||||
expect(
|
||||
wf.moderator(
|
||||
{ role: 'tester', meta: { pass: false, reason: '失败' } },
|
||||
'x',
|
||||
),
|
||||
).toBe('coder');
|
||||
expect(
|
||||
wf.moderator(
|
||||
{ role: 'promoter', meta: { commitHash: 'abc', pushed: true } },
|
||||
'x',
|
||||
),
|
||||
).toBe(END);
|
||||
});
|
||||
|
||||
test('mock: full happy path via adapter ticks', async () => {
|
||||
@@ -80,15 +69,10 @@ describe('Meta Workflow', () => {
|
||||
content: 'all pass',
|
||||
meta: { pass: true, reason: '编译通过,测试全绿' },
|
||||
}),
|
||||
promoter: async () => ({
|
||||
content: 'commit abc',
|
||||
meta: { commitHash: 'abc', pushed: true },
|
||||
}),
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
|
||||
// Seed START
|
||||
const hash = await store.putObject('build a demo workflow');
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
@@ -97,7 +81,6 @@ describe('Meta Workflow', () => {
|
||||
hash,
|
||||
});
|
||||
|
||||
// Tick through all roles
|
||||
const roles: string[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const r = await rule.tick();
|
||||
@@ -105,10 +88,10 @@ describe('Meta Workflow', () => {
|
||||
roles.push(...r.executed.map((a) => a.role));
|
||||
}
|
||||
|
||||
expect(roles).toEqual(['coder', 'tester', 'promoter']);
|
||||
expect(roles).toEqual(['coder', 'tester']);
|
||||
|
||||
const events = await store.getAfter(0);
|
||||
expect(events.length).toBe(4); // __start__ + 3 roles
|
||||
expect(events.length).toBe(3); // __start__ + coder + tester
|
||||
|
||||
await store.close();
|
||||
});
|
||||
@@ -133,10 +116,6 @@ describe('Meta Workflow', () => {
|
||||
meta: { pass: true, reason: '编译通过,测试全绿' },
|
||||
};
|
||||
},
|
||||
promoter: async () => ({
|
||||
content: 'done',
|
||||
meta: { commitHash: 'def', pushed: true },
|
||||
}),
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
@@ -155,8 +134,8 @@ describe('Meta Workflow', () => {
|
||||
roles.push(...r.executed.map((a) => a.role));
|
||||
}
|
||||
|
||||
// coder → tester(failed) → coder → tester(pass) → promoter
|
||||
expect(roles).toEqual(['coder', 'tester', 'coder', 'tester', 'promoter']);
|
||||
// coder → tester(fail) → coder → tester(pass) → END
|
||||
expect(roles).toEqual(['coder', 'tester', 'coder', 'tester']);
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
* Meta Workflow — workflow for developing workflows.
|
||||
*
|
||||
* Roles:
|
||||
* coder (Agent) → implement + self-test
|
||||
* tester (code) → CI-level verification, pure code
|
||||
* promoter (code) → git commit + push + promote event
|
||||
* coder (Agent) → implement + self-test
|
||||
* tester (code) → build + test + commit/push on pass
|
||||
*
|
||||
* Flow:
|
||||
* START → coder → tester
|
||||
* → tester.pass === true → promoter → END
|
||||
* → tester.pass === false → coder (retry)
|
||||
* → pass → END (with commit + push)
|
||||
* → fail → coder (retry)
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
@@ -34,18 +33,14 @@ export interface MetaTesterMeta {
|
||||
[key: string]: unknown;
|
||||
pass: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface MetaPromoterMeta {
|
||||
[key: string]: unknown;
|
||||
commitHash: string;
|
||||
pushed: boolean;
|
||||
/** Only present when pass=true */
|
||||
commitHash?: string;
|
||||
pushed?: boolean;
|
||||
}
|
||||
|
||||
export type MetaWorkflowRoles = {
|
||||
coder: Role<MetaCoderMeta>;
|
||||
tester: Role<MetaTesterMeta>;
|
||||
promoter: Role<MetaPromoterMeta>;
|
||||
};
|
||||
|
||||
// ── Moderator ──────────────────────────────────────────────────
|
||||
@@ -61,11 +56,8 @@ function metaModerator(
|
||||
return 'tester';
|
||||
case 'tester': {
|
||||
const meta = input.meta as MetaTesterMeta | null;
|
||||
const pass = meta?.pass;
|
||||
return pass ? 'promoter' : 'coder';
|
||||
return meta?.pass ? END : 'coder';
|
||||
}
|
||||
case 'promoter':
|
||||
return END;
|
||||
default:
|
||||
return END;
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Meta Promoter role — pure code, git commit + push.
|
||||
* No LLM needed.
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import type { MetaPromoterMeta } from '../meta.js';
|
||||
import type { Role, RoleResult, WorkflowMessage } from '../workflow-type.js';
|
||||
|
||||
export function createMetaPromoterRole(opts: {
|
||||
repoDir: string;
|
||||
remote?: string;
|
||||
branch?: string;
|
||||
}): Role<MetaPromoterMeta> {
|
||||
const remote = opts.remote ?? 'gitflare';
|
||||
const branch = opts.branch ?? 'main';
|
||||
|
||||
return async (
|
||||
chain: WorkflowMessage[],
|
||||
): Promise<RoleResult<MetaPromoterMeta>> => {
|
||||
const startMsg = chain.find((m) => m.role === '__start__');
|
||||
let workflowName = 'unknown';
|
||||
// 尝试从 task 描述里提取名字(第一行或 JSON 里的 name 字段)
|
||||
const firstLine = (startMsg?.content ?? '').split('\n')[0];
|
||||
workflowName = firstLine.slice(0, 50) || 'unknown';
|
||||
|
||||
const cwd = opts.repoDir;
|
||||
const exec = (cmd: string) =>
|
||||
execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30_000 }).trim();
|
||||
|
||||
// Stage + commit
|
||||
exec('git add -A');
|
||||
const commitMsg = `refactor: simplify meta workflow — coder → tester → promoter`;
|
||||
exec(
|
||||
`git commit -m "${commitMsg}" --author="小橘 <xiaoju@shazhou.work>" --allow-empty`,
|
||||
);
|
||||
const commitHash = exec('git rev-parse --short HEAD');
|
||||
|
||||
// Push
|
||||
let pushed = false;
|
||||
try {
|
||||
exec(`git push ${remote} ${branch} --no-verify`);
|
||||
pushed = true;
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
content: `Committed: ${commitHash}\nPushed: ${pushed ? 'yes' : 'no'}`,
|
||||
meta: { commitHash, pushed },
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,27 +1,40 @@
|
||||
/**
|
||||
* Meta Tester role — pure code build+test validation.
|
||||
* No LLM needed, just exit codes and output parsing.
|
||||
* Meta Tester role — build + test + commit/push on pass.
|
||||
* No LLM needed, just exit codes and git.
|
||||
*
|
||||
* 小橘 🍊 (NEKO Team)
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import type { MetaTesterMeta } from '../meta.js';
|
||||
import type { Role, RoleResult, WorkflowMessage } from '../workflow-type.js';
|
||||
|
||||
export function createMetaTesterRole(opts: {
|
||||
repoDir: string;
|
||||
/** cwd for build command (defaults to repoDir) */
|
||||
buildDir?: string;
|
||||
/** cwd for test command (defaults to repoDir) */
|
||||
testDir?: string;
|
||||
/** git remote to push to (auto-detect if omitted) */
|
||||
remote?: string;
|
||||
/** git branch (default: main) */
|
||||
branch?: string;
|
||||
}): Role<MetaTesterMeta> {
|
||||
const branch = opts.branch ?? 'main';
|
||||
|
||||
return async (
|
||||
chain: WorkflowMessage[],
|
||||
): Promise<RoleResult<MetaTesterMeta>> => {
|
||||
const cwd = opts.repoDir;
|
||||
const buildCwd = opts.buildDir ?? cwd;
|
||||
const testCwd = opts.testDir ?? cwd;
|
||||
|
||||
// Step 1: Build
|
||||
let buildOk = false;
|
||||
let buildOutput = '';
|
||||
try {
|
||||
buildOutput = execSync('bun run build 2>&1', {
|
||||
cwd: join(opts.repoDir, 'packages/pulse'),
|
||||
cwd: buildCwd,
|
||||
timeout: 60_000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
@@ -35,14 +48,11 @@ export function createMetaTesterRole(opts: {
|
||||
let testOutput = '';
|
||||
if (buildOk) {
|
||||
try {
|
||||
testOutput = execSync(
|
||||
'bun test packages/pulse/src/workflows/ 2>&1',
|
||||
{
|
||||
cwd: opts.repoDir,
|
||||
timeout: 60_000,
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
);
|
||||
testOutput = execSync('bun test src/workflows/ 2>&1', {
|
||||
cwd: testCwd,
|
||||
timeout: 60_000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
testOk = true;
|
||||
} catch (err: any) {
|
||||
testOutput = err.stdout ?? err.message;
|
||||
@@ -57,9 +67,57 @@ export function createMetaTesterRole(opts: {
|
||||
? '测试失败'
|
||||
: '编译失败';
|
||||
|
||||
// Step 3: If pass, commit + push
|
||||
let commitHash: string | undefined;
|
||||
let pushed: boolean | undefined;
|
||||
if (pass) {
|
||||
const exec = (cmd: string) =>
|
||||
execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30_000 }).trim();
|
||||
|
||||
try {
|
||||
// Extract task name from __start__ for commit message
|
||||
const startMsg = chain.find((m) => m.role === '__start__');
|
||||
const firstLine = (startMsg?.content ?? '').split('\n')[0].slice(0, 60);
|
||||
const commitMsg = firstLine || 'meta workflow auto-commit';
|
||||
|
||||
exec('git add -A');
|
||||
exec(
|
||||
`git commit -m "${commitMsg}" --author="小橘 <xiaoju@shazhou.work>" --allow-empty`,
|
||||
);
|
||||
commitHash = exec('git rev-parse --short HEAD');
|
||||
|
||||
// Try push — auto-detect remote
|
||||
const remote = opts.remote ?? (() => {
|
||||
try {
|
||||
const remotes = exec('git remote').split('\n').filter(Boolean);
|
||||
return remotes[0] || null;
|
||||
} catch { return null; }
|
||||
})();
|
||||
|
||||
if (remote) {
|
||||
try {
|
||||
exec(`git push ${remote} ${branch} --no-verify`);
|
||||
pushed = true;
|
||||
} catch {
|
||||
pushed = false;
|
||||
}
|
||||
} else {
|
||||
pushed = false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Commit failed — still pass, just note it
|
||||
commitHash = undefined;
|
||||
pushed = false;
|
||||
}
|
||||
}
|
||||
|
||||
const summary = pass
|
||||
? `${reason}\nCommit: ${commitHash ?? 'none'}\nPushed: ${pushed ? 'yes' : 'no'}`
|
||||
: `${reason}\n\n---\n${output.slice(0, 2000)}`;
|
||||
|
||||
return {
|
||||
content: `${reason}\n\n---\n${output.slice(0, 2000)}`,
|
||||
meta: { pass, reason },
|
||||
content: summary,
|
||||
meta: { pass, reason, commitHash, pushed },
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user