team-anya-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,186 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { EventEmitter } from 'node:events';
3
- /**
4
- * ProcessManager: 纯进程管理器
5
- *
6
- * 只负责 Yor 进程的生命周期管理:
7
- * - spawn / kill / 监听退出
8
- * - 槽位管理(并发控制)
9
- * - 退出时通过 EventEmitter 发送事件通知
10
- *
11
- * 不包含任何业务逻辑(排序、恢复、工作区准备、brief 组装、DB 操作等)。
12
- */
13
- export class ProcessManager extends EventEmitter {
14
- slots;
15
- maxConcurrency;
16
- yorEntry;
17
- logger;
18
- spawnFn;
19
- shuttingDown = false;
20
- constructor(config, spawnFn) {
21
- super();
22
- this.maxConcurrency = config.maxConcurrency;
23
- this.yorEntry = config.yorEntry;
24
- this.logger = config.logger ?? { info: console.log, error: console.error };
25
- this.spawnFn = spawnFn ?? spawn;
26
- this.slots = Array.from({ length: config.maxConcurrency }, (_, i) => ({
27
- slotId: `slot-${i}`,
28
- busy: false,
29
- taskId: null,
30
- process: null,
31
- pid: null,
32
- startedAt: null,
33
- }));
34
- }
35
- /**
36
- * 启动一个 Yor 子进程
37
- * 调用方负责工作区准备和 brief 组装,ProcessManager 只负责 spawn。
38
- */
39
- async spawn(params) {
40
- if (this.shuttingDown) {
41
- throw new Error('ProcessManager 正在关闭,无法 spawn 新进程');
42
- }
43
- const slot = this.findFreeSlot();
44
- if (!slot) {
45
- throw new Error(`无空闲槽位,当前并发上限 ${this.maxConcurrency}`);
46
- }
47
- const { taskId, workingDir, briefPath, env } = params;
48
- const child = this.spawnFn('npx', [
49
- 'tsx', this.yorEntry,
50
- '--task', taskId,
51
- '--workspace', workingDir,
52
- '--brief', briefPath,
53
- ], {
54
- cwd: workingDir,
55
- env: { ...process.env, ...env },
56
- stdio: 'pipe',
57
- });
58
- const pid = child.pid ?? 0;
59
- slot.busy = true;
60
- slot.taskId = taskId;
61
- slot.process = child;
62
- slot.pid = pid;
63
- slot.startedAt = new Date().toISOString();
64
- // stdout/stderr 日志转发
65
- child.stdout?.on('data', (data) => {
66
- const lines = data.toString().trimEnd();
67
- if (lines) {
68
- this.logger.info(`[Yor:${taskId}] ${lines}`);
69
- }
70
- });
71
- let stderrBuf = '';
72
- child.stderr?.on('data', (data) => {
73
- const chunk = data.toString();
74
- stderrBuf += chunk;
75
- const lines = chunk.trimEnd();
76
- if (lines) {
77
- this.logger.error(`[Yor:${taskId}:stderr] ${lines}`);
78
- }
79
- });
80
- child.on('exit', (code) => {
81
- if (code !== 0 && stderrBuf.trim()) {
82
- this.logger.error(`[ProcessManager] stderr (${taskId}): ${stderrBuf.trim().slice(0, 500)}`);
83
- }
84
- this.handleExit(slot, code);
85
- });
86
- child.on('error', (err) => {
87
- this.logger.error(`[ProcessManager] 进程异常 (${taskId}):`, err);
88
- this.handleExit(slot, 1);
89
- });
90
- this.logger.info(`[ProcessManager] 启动 ${taskId} | ${slot.slotId} pid=${pid}`);
91
- return { slotId: slot.slotId, pid };
92
- }
93
- /**
94
- * 终止指定槽位的 Yor 进程
95
- */
96
- async kill(slotId) {
97
- const slot = this.slots.find(s => s.slotId === slotId);
98
- if (!slot || !slot.busy || !slot.process) {
99
- return { success: false };
100
- }
101
- try {
102
- slot.process.kill('SIGTERM');
103
- return { success: true };
104
- }
105
- catch (err) {
106
- this.logger.error(`[ProcessManager] kill 失败 (${slotId}):`, err);
107
- return { success: false };
108
- }
109
- }
110
- /**
111
- * 返回所有槽位的状态
112
- */
113
- status() {
114
- return this.slots.map(s => ({
115
- slotId: s.slotId,
116
- taskId: s.taskId,
117
- pid: s.pid,
118
- startedAt: s.startedAt,
119
- status: s.busy ? 'busy' : 'idle',
120
- }));
121
- }
122
- /**
123
- * 优雅关闭:终止所有运行中的进程
124
- */
125
- async shutdown(timeoutMs = 30_000) {
126
- this.shuttingDown = true;
127
- const busySlots = this.slots.filter(s => s.busy);
128
- if (busySlots.length === 0) {
129
- return { unfinished: [] };
130
- }
131
- return new Promise((resolve) => {
132
- const timer = setTimeout(() => {
133
- const unfinished = [];
134
- for (const slot of this.slots) {
135
- if (slot.busy && slot.process) {
136
- slot.process.kill('SIGTERM');
137
- if (slot.taskId) {
138
- unfinished.push(slot.taskId);
139
- }
140
- }
141
- }
142
- resolve({ unfinished });
143
- }, timeoutMs);
144
- this.shutdownResolve = () => {
145
- clearTimeout(timer);
146
- resolve({ unfinished: [] });
147
- };
148
- });
149
- }
150
- shutdownResolve = null;
151
- findFreeSlot() {
152
- for (const slot of this.slots) {
153
- if (!slot.busy) {
154
- return slot;
155
- }
156
- }
157
- return null;
158
- }
159
- handleExit(slot, code) {
160
- const taskId = slot.taskId;
161
- const slotId = slot.slotId;
162
- if (taskId) {
163
- const label = code === 0 ? '正常退出' : `异常退出 code=${code}`;
164
- this.logger.info(`[ProcessManager] 结束 ${taskId} | ${label}`);
165
- }
166
- // 释放槽位
167
- slot.busy = false;
168
- slot.taskId = null;
169
- slot.process = null;
170
- slot.pid = null;
171
- slot.startedAt = null;
172
- // 发送退出事件(业务处理交给监听方)
173
- if (taskId) {
174
- this.emit('yor.exited', { slotId, taskId, exitCode: code });
175
- }
176
- // 检查 shutdown 等待
177
- if (this.shuttingDown && this.shutdownResolve) {
178
- const stillBusy = this.slots.some(s => s.busy);
179
- if (!stillBusy) {
180
- this.shutdownResolve();
181
- this.shutdownResolve = null;
182
- }
183
- }
184
- }
185
- }
186
- //# sourceMappingURL=process-manager.js.map
@@ -1,151 +0,0 @@
1
- import { writeFile, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { getTask, updateTask, insertAuditEvent, getMessageLogBySourceRef, } from '@team-anya/db';
4
- import { TaskStatus } from '@team-anya/core';
5
- /**
6
- * 验收审核(合并 approve_delivery + reject_delivery)
7
- *
8
- * 支持三级判断:
9
- * - PASS: 验收通过 → DONE + 通知人类
10
- * - REVISE: 打回返工 → retry_count++ + feedback + 重派
11
- * - ESCALATE: 升级 → BLOCKED + 通知人类
12
- */
13
- export async function taskReview(deps, input) {
14
- const { db, workspacePath, dispatcher, logger } = deps;
15
- const task = getTask(db, input.task_id);
16
- if (!task) {
17
- return {
18
- task_id: input.task_id,
19
- judgment: input.judgment,
20
- success: false,
21
- message: '任务不存在',
22
- };
23
- }
24
- switch (input.judgment) {
25
- case 'PASS': {
26
- updateTask(db, input.task_id, { status: TaskStatus.DONE });
27
- insertAuditEvent(db, {
28
- event_type: 'delivery_approved',
29
- actor: 'loid',
30
- task_id: input.task_id,
31
- summary: `Loid 验收通过: ${input.summary ?? '无评语'}`,
32
- });
33
- // 通知人类
34
- let notified = false;
35
- if (input.notify_human !== false && deps.channelSend) {
36
- try {
37
- let chatId = null;
38
- if (task.source_ref) {
39
- const sourceMsg = getMessageLogBySourceRef(db, task.source_ref);
40
- chatId = sourceMsg?.chat_id ?? null;
41
- }
42
- if (chatId) {
43
- const prPart = task.pr_url ? `,PR: ${task.pr_url}` : '';
44
- await deps.channelSend(`feishu://${chatId}`, `${input.task_id} 搞定了——${input.summary ?? task.title}${prPart}`, { task_id: input.task_id });
45
- notified = true;
46
- }
47
- }
48
- catch (err) {
49
- logger?.error(`[anya:pipeline] [Loid] 任务完成通知发送失败 (${input.task_id}):`, err);
50
- }
51
- }
52
- logger?.info(`[anya:pipeline] [Loid] 验收通过 ${input.task_id}${notified ? ' (已通知)' : ''}`);
53
- return { task_id: input.task_id, judgment: 'PASS', success: true, notified };
54
- }
55
- case 'REVISE': {
56
- const retryCount = (task.retry_count ?? 0) + 1;
57
- const maxRetries = task.max_retries ?? 3;
58
- if (retryCount > maxRetries) {
59
- // 超过最大重试次数,标记为 BLOCKED
60
- updateTask(db, input.task_id, {
61
- status: TaskStatus.BLOCKED,
62
- blocked_reason: `超过最大返工次数 (${maxRetries}): ${input.feedback ?? '未说明'}`,
63
- blocked_since: new Date().toISOString(),
64
- retry_count: retryCount,
65
- });
66
- insertAuditEvent(db, {
67
- event_type: 'delivery_rejected_max_retries',
68
- actor: 'loid',
69
- task_id: input.task_id,
70
- summary: `Loid 打回但已达上限 (${retryCount}/${maxRetries}): ${input.feedback ?? ''}`,
71
- });
72
- logger?.info(`[anya:pipeline] [Loid] 打回 ${input.task_id} (已达上限 ${retryCount}/${maxRetries}, BLOCKED)`);
73
- return {
74
- task_id: input.task_id,
75
- judgment: 'REVISE',
76
- success: true,
77
- retry_count: retryCount,
78
- max_retries: maxRetries,
79
- blocked: true,
80
- message: `已达最大返工次数 ${maxRetries},任务已阻塞`,
81
- };
82
- }
83
- // 更新任务状态和反馈
84
- updateTask(db, input.task_id, {
85
- status: TaskStatus.READY,
86
- retry_count: retryCount,
87
- blocked_reason: null,
88
- blocked_since: null,
89
- });
90
- // 写返工反馈
91
- const taskDir = join(workspacePath, 'yor', input.task_id);
92
- await mkdir(taskDir, { recursive: true });
93
- const feedbackContent = `# 返工反馈 (第 ${retryCount} 次)\n\n## 打回原因\n${input.feedback ?? '未说明'}\n\n## 修改意见\n${input.feedback ?? '无'}\n`;
94
- await writeFile(join(taskDir, `feedback-${retryCount}.md`), feedbackContent, 'utf-8');
95
- insertAuditEvent(db, {
96
- event_type: 'delivery_rejected',
97
- actor: 'loid',
98
- task_id: input.task_id,
99
- summary: `Loid 打回 (${retryCount}/${maxRetries}): ${input.feedback ?? ''}`,
100
- detail: JSON.stringify({ feedback: input.feedback }),
101
- });
102
- // 重新派工
103
- if (dispatcher) {
104
- await dispatcher.dispatch(input.task_id);
105
- }
106
- logger?.info(`[anya:pipeline] [Loid] 打回 ${input.task_id} → 返工 (${retryCount}/${maxRetries})`);
107
- return {
108
- task_id: input.task_id,
109
- judgment: 'REVISE',
110
- success: true,
111
- retry_count: retryCount,
112
- max_retries: maxRetries,
113
- };
114
- }
115
- case 'ESCALATE': {
116
- updateTask(db, input.task_id, {
117
- status: TaskStatus.BLOCKED,
118
- blocked_reason: `需人类决策: ${input.escalation_note ?? '未说明'}`,
119
- blocked_since: new Date().toISOString(),
120
- });
121
- insertAuditEvent(db, {
122
- event_type: 'delivery_escalated',
123
- actor: 'loid',
124
- task_id: input.task_id,
125
- summary: `Loid 升级: ${input.escalation_note ?? '需人类介入'}`,
126
- detail: JSON.stringify({ escalation_note: input.escalation_note }),
127
- });
128
- // 通过通道向人类发升级通知
129
- let notified = false;
130
- if (deps.channelSend) {
131
- try {
132
- let chatId = null;
133
- if (task.source_ref) {
134
- const sourceMsg = getMessageLogBySourceRef(db, task.source_ref);
135
- chatId = sourceMsg?.chat_id ?? null;
136
- }
137
- if (chatId) {
138
- await deps.channelSend(`feishu://${chatId}`, `${input.task_id} 需要你来决定——${input.escalation_note ?? '详见任务'}`, { task_id: input.task_id, intent: 'escalation' });
139
- notified = true;
140
- }
141
- }
142
- catch (err) {
143
- logger?.error(`[anya:pipeline] [Loid] 升级通知发送失败 (${input.task_id}):`, err);
144
- }
145
- }
146
- logger?.info(`[anya:pipeline] [Loid] 升级 ${input.task_id}: ${input.escalation_note ?? '需人类决策'}${notified ? ' (已通知)' : ''}`);
147
- return { task_id: input.task_id, judgment: 'ESCALATE', success: true, notified };
148
- }
149
- }
150
- }
151
- //# sourceMappingURL=task-review.js.map
@@ -1,7 +0,0 @@
1
- export async function workspaceCleanup(deps, input) {
2
- const { worktreeManager, logger } = deps;
3
- logger?.info(`[anya:pipeline] [Loid] workspace.cleanup ${input.task_id}`);
4
- await worktreeManager.cleanup(input.task_id, input.project_id);
5
- return { task_id: input.task_id, cleaned: true };
6
- }
7
- //# sourceMappingURL=workspace-cleanup.js.map
@@ -1,31 +0,0 @@
1
- export async function workspaceInfo(deps, input) {
2
- const { worktreeManager } = deps;
3
- await worktreeManager.loadConfig();
4
- const product = worktreeManager.findProduct(input.project_id);
5
- if (product) {
6
- return {
7
- type: 'product',
8
- project_id: product.productId,
9
- name: product.name,
10
- description: product.description,
11
- repos: product.repos.map(r => ({
12
- name: r.name,
13
- gitUrl: r.gitUrl,
14
- repoPath: worktreeManager.resolveRepoPath(r.repoPath, r.name),
15
- defaultBranch: r.defaultBranch,
16
- })),
17
- };
18
- }
19
- const project = worktreeManager.findProject(input.project_id);
20
- if (project) {
21
- return {
22
- type: 'project',
23
- project_id: project.projectId,
24
- gitUrl: project.gitUrl,
25
- repoPath: worktreeManager.resolveRepoPath(project.repoPath, project.projectId),
26
- defaultBranch: project.defaultBranch,
27
- };
28
- }
29
- return { type: 'not_found', project_id: input.project_id };
30
- }
31
- //# sourceMappingURL=workspace-info.js.map
@@ -1,12 +0,0 @@
1
- export async function workspacePrepare(deps, input) {
2
- const { worktreeManager, logger } = deps;
3
- logger?.info(`[anya:pipeline] [Loid] workspace.prepare ${input.task_id} | project=${input.project_id ?? 'none'}`);
4
- const result = await worktreeManager.prepare(input.task_id, input.project_id);
5
- return {
6
- path: result.workingDir,
7
- branch: result.branch,
8
- type: result.mode,
9
- repos: result.repos,
10
- };
11
- }
12
- //# sourceMappingURL=workspace-prepare.js.map
@@ -1,47 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import { existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- const execAsync = promisify(exec);
6
- /**
7
- * code.lint - 运行 lint 检查
8
- *
9
- * 自动检测包管理器,运行 lint 脚本。
10
- * 返回 lint 结果。
11
- */
12
- export async function codeLint(input) {
13
- const { working_dir } = input;
14
- let command;
15
- if (existsSync(join(working_dir, 'pnpm-lock.yaml')) || existsSync(join(working_dir, 'pnpm-workspace.yaml'))) {
16
- command = 'pnpm lint';
17
- }
18
- else if (existsSync(join(working_dir, 'yarn.lock'))) {
19
- command = 'yarn lint';
20
- }
21
- else {
22
- command = 'npm run lint';
23
- }
24
- try {
25
- const { stdout, stderr } = await execAsync(command, {
26
- cwd: working_dir,
27
- timeout: 3 * 60 * 1000, // 3 分钟超时
28
- maxBuffer: 10 * 1024 * 1024, // 10MB
29
- });
30
- const output = [stdout, stderr].filter(Boolean).join('\n');
31
- return { success: true, passed: true, output };
32
- }
33
- catch (err) {
34
- const output = [err.stdout, err.stderr].filter(Boolean).join('\n');
35
- const isLintFailure = err.code !== undefined && err.code !== null;
36
- if (isLintFailure) {
37
- return { success: true, passed: false, output };
38
- }
39
- return {
40
- success: false,
41
- passed: false,
42
- output: output || '',
43
- error: err.message,
44
- };
45
- }
46
- }
47
- //# sourceMappingURL=code-lint.js.map
@@ -1,52 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import { existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- const execAsync = promisify(exec);
6
- /**
7
- * code.test - 运行测试
8
- *
9
- * 默认运行 pnpm test 或 npm test,可指定自定义测试命令。
10
- * 返回测试输出和通过/失败状态。
11
- */
12
- export async function codeTest(input) {
13
- const { working_dir } = input;
14
- let command = input.command;
15
- if (!command) {
16
- // 自动检测包管理器
17
- if (existsSync(join(working_dir, 'pnpm-lock.yaml')) || existsSync(join(working_dir, 'pnpm-workspace.yaml'))) {
18
- command = 'pnpm test';
19
- }
20
- else if (existsSync(join(working_dir, 'yarn.lock'))) {
21
- command = 'yarn test';
22
- }
23
- else {
24
- command = 'npm test';
25
- }
26
- }
27
- try {
28
- const { stdout, stderr } = await execAsync(command, {
29
- cwd: working_dir,
30
- timeout: 5 * 60 * 1000, // 5 分钟超时
31
- maxBuffer: 10 * 1024 * 1024, // 10MB
32
- });
33
- const output = [stdout, stderr].filter(Boolean).join('\n');
34
- return { success: true, passed: true, output };
35
- }
36
- catch (err) {
37
- // 测试失败但命令执行了(exit code !== 0)
38
- const output = [err.stdout, err.stderr].filter(Boolean).join('\n');
39
- const isTestFailure = err.code !== undefined && err.code !== null;
40
- if (isTestFailure) {
41
- return { success: true, passed: false, output };
42
- }
43
- // 命令本身执行失败
44
- return {
45
- success: false,
46
- passed: false,
47
- output: output || '',
48
- error: err.message,
49
- };
50
- }
51
- }
52
- //# sourceMappingURL=code-test.js.map
@@ -1,24 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- const execFileAsync = promisify(execFile);
4
- /**
5
- * git.add - 执行 git add 命令
6
- *
7
- * files 可以是文件列表或 "."(添加全部变更)。
8
- */
9
- export async function gitAdd(input) {
10
- const { files, working_dir } = input;
11
- const fileList = typeof files === 'string' ? [files] : files;
12
- if (fileList.length === 0) {
13
- return { success: false, files_added: [], error: '未指定要添加的文件' };
14
- }
15
- try {
16
- await execFileAsync('git', ['add', ...fileList], { cwd: working_dir });
17
- return { success: true, files_added: fileList };
18
- }
19
- catch (err) {
20
- const message = err instanceof Error ? err.message : String(err);
21
- return { success: false, files_added: [], error: message };
22
- }
23
- }
24
- //# sourceMappingURL=git-add.js.map
@@ -1,24 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- const execFileAsync = promisify(execFile);
4
- /**
5
- * git.commit - 执行 git commit
6
- */
7
- export async function gitCommit(input) {
8
- const { message, working_dir } = input;
9
- if (!message.trim()) {
10
- return { success: false, error: 'commit message 不能为空' };
11
- }
12
- try {
13
- const { stdout } = await execFileAsync('git', ['commit', '-m', message], { cwd: working_dir });
14
- // 从输出中提取 commit hash
15
- const hashMatch = stdout.match(/\[[\w/.-]+\s+([a-f0-9]+)\]/);
16
- const commitHash = hashMatch?.[1];
17
- return { success: true, commit_hash: commitHash };
18
- }
19
- catch (err) {
20
- const message = err instanceof Error ? err.message : String(err);
21
- return { success: false, error: message };
22
- }
23
- }
24
- //# sourceMappingURL=git-commit.js.map
@@ -1,64 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- const execFileAsync = promisify(execFile);
4
- /** 禁止推送到这些分支 */
5
- const PROTECTED_BRANCHES = ['main', 'master'];
6
- /** 禁止提交的敏感文件模式 */
7
- const SENSITIVE_FILE_PATTERNS = [
8
- /\.env$/,
9
- /\.env\.\w+$/,
10
- /credentials\.json$/,
11
- /\.pem$/,
12
- /\.key$/,
13
- /secret/i,
14
- ];
15
- /**
16
- * git.push - 执行 git push
17
- *
18
- * 安全约束:
19
- * - 禁止 --force 推送到 main/master
20
- * - 检查暂存区是否包含敏感文件
21
- */
22
- export async function gitPush(input) {
23
- const { working_dir, branch } = input;
24
- // 确定目标分支
25
- let targetBranch = branch;
26
- if (!targetBranch) {
27
- try {
28
- const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: working_dir });
29
- targetBranch = stdout.trim();
30
- }
31
- catch {
32
- return { success: false, error: '无法获取当前分支名' };
33
- }
34
- }
35
- // 安全检查:禁止推送到受保护分支
36
- if (PROTECTED_BRANCHES.includes(targetBranch)) {
37
- return { success: false, error: `禁止直接推送到受保护分支: ${targetBranch}` };
38
- }
39
- // 安全检查:检查暂存区是否包含敏感文件
40
- try {
41
- const { stdout } = await execFileAsync('git', ['diff', '--cached', '--name-only'], { cwd: working_dir });
42
- const stagedFiles = stdout.trim().split('\n').filter(Boolean);
43
- for (const file of stagedFiles) {
44
- for (const pattern of SENSITIVE_FILE_PATTERNS) {
45
- if (pattern.test(file)) {
46
- return { success: false, error: `暂存区包含敏感文件: ${file},请先移除` };
47
- }
48
- }
49
- }
50
- }
51
- catch {
52
- // diff 命令失败不阻塞推送
53
- }
54
- try {
55
- const pushArgs = ['push', '-u', 'origin', targetBranch];
56
- await execFileAsync('git', pushArgs, { cwd: working_dir });
57
- return { success: true, branch: targetBranch };
58
- }
59
- catch (err) {
60
- const message = err instanceof Error ? err.message : String(err);
61
- return { success: false, error: message };
62
- }
63
- }
64
- //# sourceMappingURL=git-push.js.map