team-anya-cli 0.1.0
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.
- package/README.md +38 -0
- package/anya/prompts/execution-guides/git-delivery.md +38 -0
- package/anya/prompts/execution-guides/testing-and-self-heal.md +28 -0
- package/anya/prompts/protocols/brief-assembly.md +55 -0
- package/anya/prompts/protocols/report.md +175 -0
- package/anya/prompts/protocols/review.md +90 -0
- package/anya/prompts/task-claude-md.template.md +32 -0
- package/apps/server/dist/broker/cc-broker.js +257 -0
- package/apps/server/dist/cli.js +296 -0
- package/apps/server/dist/config.js +76 -0
- package/apps/server/dist/daemon.js +51 -0
- package/apps/server/dist/gateway/chat-sync.js +135 -0
- package/apps/server/dist/gateway/command-router.js +114 -0
- package/apps/server/dist/gateway/commands/cancel.js +32 -0
- package/apps/server/dist/gateway/commands/help.js +16 -0
- package/apps/server/dist/gateway/commands/index.js +26 -0
- package/apps/server/dist/gateway/commands/restart.js +34 -0
- package/apps/server/dist/gateway/commands/status.js +34 -0
- package/apps/server/dist/gateway/commands/tasks.js +33 -0
- package/apps/server/dist/gateway/feishu-sender.js +346 -0
- package/apps/server/dist/gateway/feishu-ws.js +254 -0
- package/apps/server/dist/gateway/http.js +994 -0
- package/apps/server/dist/gateway/media-downloader.js +149 -0
- package/apps/server/dist/gateway/message-events.js +10 -0
- package/apps/server/dist/gateway/message-intake.js +50 -0
- package/apps/server/dist/gateway/message-queue.js +104 -0
- package/apps/server/dist/gateway/session-reader.js +142 -0
- package/apps/server/dist/gateway/ws-push.js +115 -0
- package/apps/server/dist/loid/brain.js +104 -0
- package/apps/server/dist/loid/clarifier.js +162 -0
- package/apps/server/dist/loid/context-builder.js +413 -0
- package/apps/server/dist/loid/mcp-server.js +104 -0
- package/apps/server/dist/loid/memory-settler.js +189 -0
- package/apps/server/dist/loid/opportunity-manager.js +148 -0
- package/apps/server/dist/loid/profile-updater.js +179 -0
- package/apps/server/dist/loid/reporter.js +148 -0
- package/apps/server/dist/loid/schemas.js +117 -0
- package/apps/server/dist/loid/self-calibrator.js +314 -0
- package/apps/server/dist/loid/session-manager.js +217 -0
- package/apps/server/dist/loid/session.js +271 -0
- package/apps/server/dist/loid/worktree-manager.js +191 -0
- package/apps/server/dist/main.js +337 -0
- package/apps/server/dist/tracing/index.js +2 -0
- package/apps/server/dist/tracing/trace-context.js +92 -0
- package/apps/server/dist/types/message.js +2 -0
- package/apps/server/dist/yor/yor-mcp-server.js +104 -0
- package/apps/server/dist/yor/yor-orchestrator.js +233 -0
- package/apps/web/dist/assets/index-CHIT0Dya.css +1 -0
- package/apps/web/dist/assets/index-CJzAjoVH.js +798 -0
- package/apps/web/dist/index.html +13 -0
- package/package.json +42 -0
- package/packages/cc-client/dist/claude-code-backend.js +664 -0
- package/packages/cc-client/dist/index.js +2 -0
- package/packages/cc-client/package.json +11 -0
- package/packages/core/dist/constants.js +59 -0
- package/packages/core/dist/errors.js +35 -0
- package/packages/core/dist/index.js +7 -0
- package/packages/core/dist/office-init.js +97 -0
- package/packages/core/dist/scope/checker.js +114 -0
- package/packages/core/dist/scope/defaults.js +40 -0
- package/packages/core/dist/scope/index.js +3 -0
- package/packages/core/dist/state-machine.js +85 -0
- package/packages/core/dist/types/audit.js +12 -0
- package/packages/core/dist/types/backend.js +2 -0
- package/packages/core/dist/types/commitment.js +17 -0
- package/packages/core/dist/types/communication.js +18 -0
- package/packages/core/dist/types/index.js +8 -0
- package/packages/core/dist/types/opportunity.js +27 -0
- package/packages/core/dist/types/org.js +26 -0
- package/packages/core/dist/types/task.js +46 -0
- package/packages/core/package.json +10 -0
- package/packages/db/dist/client.js +69 -0
- package/packages/db/dist/index.js +603 -0
- package/packages/db/dist/schema/audit-events.js +13 -0
- package/packages/db/dist/schema/cc-sessions.js +14 -0
- package/packages/db/dist/schema/chats.js +33 -0
- package/packages/db/dist/schema/commitments.js +18 -0
- package/packages/db/dist/schema/communication-events.js +14 -0
- package/packages/db/dist/schema/index.js +12 -0
- package/packages/db/dist/schema/message-log.js +20 -0
- package/packages/db/dist/schema/opportunities.js +23 -0
- package/packages/db/dist/schema/org.js +36 -0
- package/packages/db/dist/schema/projects.js +23 -0
- package/packages/db/dist/schema/tasks.js +46 -0
- package/packages/db/dist/schema/trace-spans.js +19 -0
- package/packages/db/package.json +12 -0
- package/packages/db/src/migrations/0000_simple_magneto.sql +148 -0
- package/packages/db/src/migrations/0001_nifty_morph.sql +42 -0
- package/packages/db/src/migrations/0002_common_joshua_kane.sql +20 -0
- package/packages/db/src/migrations/0003_add_cc_sessions.sql +13 -0
- package/packages/db/src/migrations/0004_jittery_triathlon.sql +1 -0
- package/packages/db/src/migrations/meta/0000_snapshot.json +987 -0
- package/packages/db/src/migrations/meta/0001_snapshot.json +1280 -0
- package/packages/db/src/migrations/meta/0002_snapshot.json +1417 -0
- package/packages/db/src/migrations/meta/0004_snapshot.json +1505 -0
- package/packages/db/src/migrations/meta/_journal.json +41 -0
- package/packages/mcp-tools/dist/index.js +41 -0
- package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
- package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
- package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
- package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
- package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
- package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
- package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
- package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
- package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
- package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
- package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
- package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
- package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
- package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
- package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
- package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
- package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
- package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
- package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
- package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
- package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +177 -0
- package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +38 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
- package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
- package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
- package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
- package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +191 -0
- package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
- package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
- package/packages/mcp-tools/dist/layer3/channel-send.js +90 -0
- package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
- package/packages/mcp-tools/dist/registry.js +779 -0
- package/packages/mcp-tools/package.json +13 -0
- package/workspace/.claude/settings.local.json +9 -0
- package/workspace/.mcp.json +12 -0
- package/workspace/CHARTER.md +73 -0
- package/workspace/CLAUDE.md +49 -0
- package/workspace/PROTOCOL.md +126 -0
- package/workspace/TOOLS.md +464 -0
- package/workspace/audit/.gitkeep +0 -0
- package/workspace/loid/CLAUDE.md +12 -0
- package/workspace/loid/PLAYBOOK.md +198 -0
- package/workspace/loid/PROFILE.md +78 -0
- package/workspace/memory/commitments/.gitkeep +0 -0
- package/workspace/memory/execution/.gitkeep +0 -0
- package/workspace/memory/people/.gitkeep +0 -0
- package/workspace/memory/projects/.gitkeep +0 -0
- package/workspace/memory/self/.gitkeep +0 -0
- package/workspace/reference/identity/.gitkeep +0 -0
- package/workspace/reference/org/escalation.yaml +24 -0
- package/workspace/reference/org/ownership.yaml +28 -0
- package/workspace/reports/.gitkeep +0 -0
- package/workspace/yor/CLAUDE.md +22 -0
- package/workspace/yor/PLAYBOOK.md +73 -0
- package/workspace/yor/PROFILE.md +52 -0
- package/workspace/yor/SELF-HEAL.md +39 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fileUpload } from '../../layer3/file-upload.js';
|
|
2
|
+
/**
|
|
3
|
+
* delivery.upload - 上传文件产物
|
|
4
|
+
*
|
|
5
|
+
* 委托给 file.upload 实现。target 参数暂不使用(预留给后续上传到指定位置的场景)。
|
|
6
|
+
*/
|
|
7
|
+
export async function deliveryUpload(deps, input) {
|
|
8
|
+
const result = await fileUpload(deps, {
|
|
9
|
+
file_path: input.file_path,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
status: 'uploaded',
|
|
13
|
+
url: result.url,
|
|
14
|
+
file_name: result.file_name,
|
|
15
|
+
file_size: result.file_size,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=delivery-upload.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getProject, softDeleteProject, insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
export async function projectRemove(db, _workspacePath, input) {
|
|
3
|
+
const existing = getProject(db, input.project_id);
|
|
4
|
+
if (!existing) {
|
|
5
|
+
return { removed: false, message: `Project ${input.project_id} not found` };
|
|
6
|
+
}
|
|
7
|
+
softDeleteProject(db, input.project_id);
|
|
8
|
+
insertAuditEvent(db, {
|
|
9
|
+
event_type: 'project_removed',
|
|
10
|
+
actor: 'loid',
|
|
11
|
+
summary: `Project ${input.project_id} soft-deleted: ${input.reason}`,
|
|
12
|
+
detail: JSON.stringify({ project_id: input.project_id, reason: input.reason }),
|
|
13
|
+
});
|
|
14
|
+
return { removed: true, message: `Project ${input.project_id} archived` };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=project-remove.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getProject, upsertProject, syncProjectRepos, insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
export async function projectUpsert(db, _workspacePath, input) {
|
|
3
|
+
const existing = getProject(db, input.project_id);
|
|
4
|
+
const action = existing ? 'updated' : 'created';
|
|
5
|
+
upsertProject(db, {
|
|
6
|
+
project_id: input.project_id,
|
|
7
|
+
name: input.name,
|
|
8
|
+
description: input.description ?? null,
|
|
9
|
+
platform: input.platform ?? 'github',
|
|
10
|
+
claude_md: input.claude_md ?? null,
|
|
11
|
+
});
|
|
12
|
+
const repos = input.repos ?? [];
|
|
13
|
+
if (repos.length > 0) {
|
|
14
|
+
syncProjectRepos(db, input.project_id, repos.map(r => ({
|
|
15
|
+
name: r.name,
|
|
16
|
+
git_url: r.git_url,
|
|
17
|
+
repo_path: r.repo_path,
|
|
18
|
+
default_branch: r.default_branch ?? 'main',
|
|
19
|
+
})));
|
|
20
|
+
}
|
|
21
|
+
insertAuditEvent(db, {
|
|
22
|
+
event_type: `project_${action}`,
|
|
23
|
+
actor: 'loid',
|
|
24
|
+
summary: `Project ${input.project_id} ${action}`,
|
|
25
|
+
detail: JSON.stringify({
|
|
26
|
+
project_id: input.project_id,
|
|
27
|
+
platform: input.platform ?? 'github',
|
|
28
|
+
repo_count: repos.length,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
return { project_id: input.project_id, action };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=project-upsert.js.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { createTask, updateTask, insertAuditEvent, getTodayMaxSequence, } from '@team-anya/db';
|
|
6
|
+
import { TaskStatus, generateTaskId, assertTransition } from '@team-anya/core';
|
|
7
|
+
const execFile = promisify(execFileCb);
|
|
8
|
+
/**
|
|
9
|
+
* 创建任务、写 brief、自动准备工作区
|
|
10
|
+
*
|
|
11
|
+
* 派工两步走:task.dispatch(MCP) → yor.spawn(MCP)
|
|
12
|
+
* dispatch 内部自动根据 project mode 准备隔离工作区(git worktree / mkdir)。
|
|
13
|
+
* 工作区准备失败时标记 BLOCKED 并返回 error,不抛异常。
|
|
14
|
+
*/
|
|
15
|
+
export async function taskDispatch(deps, input) {
|
|
16
|
+
const { db, workspacePath, logger } = deps;
|
|
17
|
+
const seq = getTodayMaxSequence(db) + 1;
|
|
18
|
+
const taskId = generateTaskId(seq);
|
|
19
|
+
// 1. 创建任务
|
|
20
|
+
createTask(db, {
|
|
21
|
+
task_id: taskId,
|
|
22
|
+
title: input.title,
|
|
23
|
+
status: TaskStatus.NEW,
|
|
24
|
+
source_type: 'feishu',
|
|
25
|
+
source_ref: input.source_message_id ?? null,
|
|
26
|
+
objective: input.objective,
|
|
27
|
+
acceptance_criteria: input.acceptance_criteria ? JSON.stringify(input.acceptance_criteria) : undefined,
|
|
28
|
+
context: input.context,
|
|
29
|
+
project_id: input.project_id,
|
|
30
|
+
created_by: input.created_by ?? null,
|
|
31
|
+
source_chat_id: input.source_chat_id ?? null,
|
|
32
|
+
});
|
|
33
|
+
// 2. 写 brief 到 workspace
|
|
34
|
+
const taskDir = join(workspacePath, 'yor', 'tasks', taskId);
|
|
35
|
+
await mkdir(taskDir, { recursive: true });
|
|
36
|
+
const briefPath = join(taskDir, 'brief.md');
|
|
37
|
+
await writeFile(briefPath, input.brief, 'utf-8');
|
|
38
|
+
// 3. 审计日志
|
|
39
|
+
insertAuditEvent(db, {
|
|
40
|
+
event_type: 'task_dispatched',
|
|
41
|
+
actor: 'loid',
|
|
42
|
+
task_id: taskId,
|
|
43
|
+
summary: `Loid 派工: ${input.title}`,
|
|
44
|
+
detail: JSON.stringify({ brief_length: input.brief.length, project: input.project_id }),
|
|
45
|
+
});
|
|
46
|
+
// 4. 获取项目配置
|
|
47
|
+
const project = deps.getProjectConfig
|
|
48
|
+
? await deps.getProjectConfig(input.project_id)
|
|
49
|
+
: undefined;
|
|
50
|
+
logger?.info(`[anya:pipeline] [Loid] 创建任务 ${taskId} "${input.title}"${input.project_id ? ` | project=${input.project_id} mode=${project?.mode}` : ''}`);
|
|
51
|
+
// 5. 自动准备工作区
|
|
52
|
+
const workspaceResult = await prepareWorkspace(taskId, taskDir, project, logger);
|
|
53
|
+
if (!workspaceResult.ok) {
|
|
54
|
+
// 工作区准备失败 → NEW → BLOCKED(经状态机校验)
|
|
55
|
+
assertTransition(TaskStatus.NEW, TaskStatus.BLOCKED);
|
|
56
|
+
updateTask(db, taskId, { status: TaskStatus.BLOCKED });
|
|
57
|
+
insertAuditEvent(db, {
|
|
58
|
+
event_type: 'task_status_changed',
|
|
59
|
+
actor: 'loid',
|
|
60
|
+
task_id: taskId,
|
|
61
|
+
summary: `NEW → BLOCKED: 工作区准备失败: ${workspaceResult.error}`,
|
|
62
|
+
detail: JSON.stringify({ from: 'NEW', to: 'BLOCKED', reason: workspaceResult.error }),
|
|
63
|
+
});
|
|
64
|
+
return { task_id: taskId, status: 'blocked', brief_path: briefPath, error: workspaceResult.error };
|
|
65
|
+
}
|
|
66
|
+
// 6. 工作区就绪 → NEW → READY,同时回写工作区信息
|
|
67
|
+
assertTransition(TaskStatus.NEW, TaskStatus.READY);
|
|
68
|
+
const branchName = project?.mode === 'project' ? `feat/anya-${taskId}` : undefined;
|
|
69
|
+
updateTask(db, taskId, {
|
|
70
|
+
status: TaskStatus.READY,
|
|
71
|
+
workspace_path: workspaceResult.workingDir,
|
|
72
|
+
assignee: 'yor',
|
|
73
|
+
...(branchName ? { branch: branchName } : {}),
|
|
74
|
+
});
|
|
75
|
+
insertAuditEvent(db, {
|
|
76
|
+
event_type: 'task_status_changed',
|
|
77
|
+
actor: 'loid',
|
|
78
|
+
task_id: taskId,
|
|
79
|
+
summary: 'NEW → READY: 工作区准备完成',
|
|
80
|
+
detail: JSON.stringify({ from: 'NEW', to: 'READY', workspace_path: workspaceResult.workingDir, branch: branchName }),
|
|
81
|
+
});
|
|
82
|
+
return { task_id: taskId, status: 'dispatched', brief_path: briefPath, working_dir: workspaceResult.workingDir };
|
|
83
|
+
}
|
|
84
|
+
async function prepareWorkspace(taskId, taskDir, project, logger) {
|
|
85
|
+
const mode = project?.mode ?? 'adhoc';
|
|
86
|
+
try {
|
|
87
|
+
switch (mode) {
|
|
88
|
+
case 'project': {
|
|
89
|
+
const repos = project.repos;
|
|
90
|
+
const workingDir = taskDir;
|
|
91
|
+
const createdWorktrees = [];
|
|
92
|
+
try {
|
|
93
|
+
for (const repo of repos) {
|
|
94
|
+
const defaultBranch = repo.default_branch ?? 'main';
|
|
95
|
+
const wtPath = join(workingDir, repo.name);
|
|
96
|
+
await execFile('git', ['-C', repo.repo_path, 'fetch', 'origin']);
|
|
97
|
+
await execFile('git', ['-C', repo.repo_path, 'worktree', 'add', wtPath, '-b', `feat/anya-${taskId}`, `origin/${defaultBranch}`]);
|
|
98
|
+
createdWorktrees.push({ repoPath: repo.repo_path, wtPath });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// 回滚已创建的 worktree
|
|
103
|
+
for (const wt of createdWorktrees) {
|
|
104
|
+
try {
|
|
105
|
+
await execFile('git', ['-C', wt.repoPath, 'worktree', 'remove', wt.wtPath, '--force']);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// 回滚失败忽略
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
112
|
+
return { ok: false, error: `worktree 准备失败: ${msg}` };
|
|
113
|
+
}
|
|
114
|
+
// git init 为 CC 提供项目边界(防止全局搜索)
|
|
115
|
+
await execFile('git', ['init', workingDir]);
|
|
116
|
+
// .gitignore 忽略所有 worktree 子目录(它们各自有独立的 .git)
|
|
117
|
+
const ignoreContent = repos.map(r => `/${r.name}/`).join('\n') + '\n';
|
|
118
|
+
await writeFile(join(workingDir, '.gitignore'), ignoreContent, 'utf-8');
|
|
119
|
+
// 写入 CLAUDE.md 帮助 CC 理解工作区结构
|
|
120
|
+
const claudeMd = buildProjectClaudeMd(taskId, repos, project.claudeMd);
|
|
121
|
+
await writeFile(join(workingDir, 'CLAUDE.md'), claudeMd, 'utf-8');
|
|
122
|
+
// 提交到 git,否则 CC 不会加载 untracked 的 CLAUDE.md
|
|
123
|
+
await execFile('git', ['-C', workingDir, 'add', 'CLAUDE.md', '.gitignore']);
|
|
124
|
+
await execFile('git', ['-C', workingDir, 'commit', '-m', 'init: workspace config']);
|
|
125
|
+
logger?.info(`[anya:pipeline] [Loid] worktrees 已创建 (${repos.length} repos): ${workingDir}`);
|
|
126
|
+
return { ok: true, workingDir };
|
|
127
|
+
}
|
|
128
|
+
case 'adhoc':
|
|
129
|
+
default: {
|
|
130
|
+
const workingDir = taskDir;
|
|
131
|
+
await mkdir(join(workingDir, 'adhoc'), { recursive: true });
|
|
132
|
+
// 在任务级目录 git init,版本管理整个任务目录
|
|
133
|
+
await execFile('git', ['init', workingDir]);
|
|
134
|
+
await writeFile(join(workingDir, '.gitignore'), '*.tmp\nnode_modules/\n.DS_Store\n', 'utf-8');
|
|
135
|
+
await execFile('git', ['-C', workingDir, 'add', '.']);
|
|
136
|
+
await execFile('git', ['-C', workingDir, 'commit', '-m', `chore: init task ${taskId}`]);
|
|
137
|
+
logger?.info(`[anya:pipeline] [Loid] adhoc 目录已创建并 git init: ${workingDir}`);
|
|
138
|
+
return { ok: true, workingDir };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
144
|
+
return { ok: false, error: `工作区准备失败 (${mode}): ${msg}` };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── 工作区 CLAUDE.md 生成 ──
|
|
148
|
+
function buildProjectClaudeMd(taskId, repos, projectClaudeMd) {
|
|
149
|
+
const lines = [
|
|
150
|
+
`# 任务工作区 ${taskId}`,
|
|
151
|
+
'',
|
|
152
|
+
'本目录是任务工作区根目录。每个子目录是一个独立的 git worktree。',
|
|
153
|
+
'',
|
|
154
|
+
'## 仓库列表',
|
|
155
|
+
'',
|
|
156
|
+
'| 目录 | 分支 |',
|
|
157
|
+
'|------|------|',
|
|
158
|
+
];
|
|
159
|
+
for (const repo of repos) {
|
|
160
|
+
lines.push(`| \`${repo.name}/\` | \`feat/anya-${taskId}\` (基于 ${repo.default_branch}) |`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push('## 重要');
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push('- 每个子目录是独立的 git 仓库(worktree),搜索代码时请进入对应子目录');
|
|
166
|
+
lines.push('- git 操作(add/commit/push)必须在对应子目录内执行');
|
|
167
|
+
lines.push('- 不要在根目录执行 git commit');
|
|
168
|
+
lines.push('- 任务产物请读取 `brief.md` 了解详情');
|
|
169
|
+
if (projectClaudeMd) {
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('## 项目技术栈');
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push(projectClaudeMd);
|
|
174
|
+
}
|
|
175
|
+
return lines.join('\n') + '\n';
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=task-dispatch.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getTask, getTasksByStatus } from '@team-anya/db';
|
|
2
|
+
import { TaskStatus } from '@team-anya/core';
|
|
3
|
+
/**
|
|
4
|
+
* 查询任务状态和历史
|
|
5
|
+
*
|
|
6
|
+
* 可按 task_id 或 status 过滤。不传参数时返回所有活跃任务。
|
|
7
|
+
*/
|
|
8
|
+
export async function taskLookup(db, input) {
|
|
9
|
+
let tasks;
|
|
10
|
+
if (input.task_id) {
|
|
11
|
+
const task = getTask(db, input.task_id);
|
|
12
|
+
tasks = task ? [task] : [];
|
|
13
|
+
}
|
|
14
|
+
else if (input.status) {
|
|
15
|
+
tasks = getTasksByStatus(db, input.status);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// 默认返回活跃任务
|
|
19
|
+
tasks = [
|
|
20
|
+
...getTasksByStatus(db, TaskStatus.IN_PROGRESS),
|
|
21
|
+
...getTasksByStatus(db, TaskStatus.READY),
|
|
22
|
+
...getTasksByStatus(db, TaskStatus.NEED_CLARIFICATION),
|
|
23
|
+
...getTasksByStatus(db, TaskStatus.DELIVERING),
|
|
24
|
+
...getTasksByStatus(db, TaskStatus.BLOCKED),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
tasks: tasks.map(t => ({
|
|
29
|
+
task_id: t.task_id,
|
|
30
|
+
title: t.title,
|
|
31
|
+
status: t.status,
|
|
32
|
+
assignee: t.assignee,
|
|
33
|
+
created_at: t.created_at,
|
|
34
|
+
pr_url: t.pr_url ?? null,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=task-lookup.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function yorApprove(deps, input) {
|
|
2
|
+
const { yorOrchestrator, logger } = deps;
|
|
3
|
+
logger?.info(`[anya:pipeline] [Loid] yor.approve task=${input.task_id}`);
|
|
4
|
+
const message = input.message ?? '审核通过,任务完成。你可以做最后的清理工作。';
|
|
5
|
+
yorOrchestrator.approveAndShutdown(input.task_id, message);
|
|
6
|
+
return { task_id: input.task_id, approved: true };
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=yor-approve.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export async function yorKill(deps, input) {
|
|
2
|
+
const { yorOrchestrator, logger } = deps;
|
|
3
|
+
logger?.info(`[anya:pipeline] [Loid] yor.kill task=${input.task_id}`);
|
|
4
|
+
const result = await yorOrchestrator.kill(input.task_id);
|
|
5
|
+
return { task_id: input.task_id, killed: result.success };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=yor-kill.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function yorRework(deps, input) {
|
|
2
|
+
const { yorOrchestrator, logger } = deps;
|
|
3
|
+
logger?.info(`[anya:pipeline] [Loid] yor.rework task=${input.task_id}`);
|
|
4
|
+
yorOrchestrator.sendRework(input.task_id, input.feedback);
|
|
5
|
+
return { task_id: input.task_id, sent: true };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=yor-rework.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function yorSpawn(deps, input) {
|
|
2
|
+
const { yorOrchestrator, logger } = deps;
|
|
3
|
+
logger?.info(`[anya:pipeline] [Loid] yor.spawn ${input.task_id} | dir=${input.working_dir}`);
|
|
4
|
+
const result = await yorOrchestrator.spawnAndExecute({
|
|
5
|
+
taskId: input.task_id,
|
|
6
|
+
workingDir: input.working_dir,
|
|
7
|
+
briefPath: input.brief_path,
|
|
8
|
+
env: input.env,
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
instance_id: result.instanceId,
|
|
12
|
+
task_id: input.task_id,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=yor-spawn.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 报告阻塞
|
|
3
|
+
*
|
|
4
|
+
* Yor 遇到无法解决的问题时调用。
|
|
5
|
+
* 增加 category 字段与 PROTOCOL.md 对齐。
|
|
6
|
+
*/
|
|
7
|
+
export async function taskBlock(collector, input) {
|
|
8
|
+
collector.onBlockerReport(input);
|
|
9
|
+
return { received: true, type: 'blocker' };
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=task-block.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFile = promisify(execFileCb);
|
|
4
|
+
/**
|
|
5
|
+
* 提交交付成果
|
|
6
|
+
*
|
|
7
|
+
* Yor 完成任务后调用,报告变更文件、测试结果、PR 链接。
|
|
8
|
+
* 结果通过 collector 回调传给 dispatcher。
|
|
9
|
+
*
|
|
10
|
+
* 交付前检查 git 状态:如果有未提交变更,拒绝交付。
|
|
11
|
+
*/
|
|
12
|
+
export async function taskDeliver(collector, input, workingDir) {
|
|
13
|
+
// 检查 git 状态
|
|
14
|
+
if (workingDir) {
|
|
15
|
+
try {
|
|
16
|
+
// 检查是否是 git 仓库
|
|
17
|
+
await execFile('git', ['-C', workingDir, 'rev-parse', '--git-dir']);
|
|
18
|
+
// 检查是否有未提交变更
|
|
19
|
+
const { stdout } = await execFile('git', ['-C', workingDir, 'status', '--porcelain']);
|
|
20
|
+
if (stdout.trim().length > 0) {
|
|
21
|
+
return {
|
|
22
|
+
received: false,
|
|
23
|
+
type: 'delivery_rejected',
|
|
24
|
+
reason: `❌ 交付被拒绝:工作目录有未提交的变更。\n请先执行 git add + git commit,然后重新调用 task.deliver。\n未提交文件:\n${stdout.trim()}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// 不是 git 仓库或 git 命令失败,跳过检查(warn 级别)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
collector.onDeliverySubmit(input);
|
|
33
|
+
return { received: true, type: 'delivery' };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=task-deliver.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
/**
|
|
3
|
+
* 中间进度汇报
|
|
4
|
+
*
|
|
5
|
+
* 长任务中 Yor 向 Loid 汇报里程碑,不改变任务状态。
|
|
6
|
+
* 写 audit event(yor_progress),Loid 可通过 audit.query 查看。
|
|
7
|
+
*/
|
|
8
|
+
export async function taskProgress(db, taskId, input) {
|
|
9
|
+
insertAuditEvent(db, {
|
|
10
|
+
event_type: 'yor_progress',
|
|
11
|
+
actor: 'yor',
|
|
12
|
+
task_id: taskId,
|
|
13
|
+
summary: input.milestone,
|
|
14
|
+
detail: JSON.stringify({
|
|
15
|
+
detail: input.detail,
|
|
16
|
+
next_step: input.next_step,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
return { received: true };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=task-progress.js.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// ── 媒体类型检测 ──
|
|
2
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico', '.tiff', '.tif']);
|
|
3
|
+
const AUDIO_EXTS = new Set(['.opus']);
|
|
4
|
+
const FILE_TYPE_MAP = {
|
|
5
|
+
'.opus': 'opus', '.mp4': 'mp4', '.pdf': 'pdf',
|
|
6
|
+
'.doc': 'doc', '.docx': 'doc', '.xls': 'xls', '.xlsx': 'xls',
|
|
7
|
+
'.ppt': 'ppt', '.pptx': 'ppt',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 根据文件扩展名检测媒体类型
|
|
11
|
+
*/
|
|
12
|
+
export function detectMediaKind(filePath) {
|
|
13
|
+
const dot = filePath.lastIndexOf('.');
|
|
14
|
+
if (dot === -1)
|
|
15
|
+
return 'file';
|
|
16
|
+
const ext = filePath.slice(dot).toLowerCase();
|
|
17
|
+
if (IMAGE_EXTS.has(ext))
|
|
18
|
+
return 'image';
|
|
19
|
+
if (AUDIO_EXTS.has(ext))
|
|
20
|
+
return 'audio';
|
|
21
|
+
return 'file';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 根据文件扩展名获取飞书 file_type 参数
|
|
25
|
+
*/
|
|
26
|
+
export function resolveFileType(filePath) {
|
|
27
|
+
const dot = filePath.lastIndexOf('.');
|
|
28
|
+
if (dot === -1)
|
|
29
|
+
return 'stream';
|
|
30
|
+
const ext = filePath.slice(dot).toLowerCase();
|
|
31
|
+
return FILE_TYPE_MAP[ext] ?? 'stream';
|
|
32
|
+
}
|
|
33
|
+
// ── Markdown 检测 ──
|
|
34
|
+
/**
|
|
35
|
+
* 检测文本是否包含 Markdown 语法
|
|
36
|
+
*
|
|
37
|
+
* 匹配常见 Markdown 元素:标题、粗体、斜体、代码块、链接、列表、引用、分割线、表格等。
|
|
38
|
+
* 短文本(<50 字符)且仅含单个 markdown 元素时倾向纯文本,避免误判。
|
|
39
|
+
*/
|
|
40
|
+
const MD_PATTERNS = [
|
|
41
|
+
/^#{1,6}\s/m, // 标题: # / ## / ###
|
|
42
|
+
/\*\*[^*]+\*\*/, // 粗体: **text**
|
|
43
|
+
/\*[^*]+\*/, // 斜体: *text*
|
|
44
|
+
/`[^`]+`/, // 行内代码: `code`
|
|
45
|
+
/```[\s\S]*?```/, // 代码块: ```...```
|
|
46
|
+
/\[.+?\]\(.+?\)/, // 链接: [text](url)
|
|
47
|
+
/^\s*[-*+]\s/m, // 无序列表: - item / * item
|
|
48
|
+
/^\s*\d+\.\s/m, // 有序列表: 1. item
|
|
49
|
+
/^\s*>/m, // 引用: > text
|
|
50
|
+
/^-{3,}$/m, // 分割线: ---
|
|
51
|
+
/^\|.+\|$/m, // 表格: | col | col |
|
|
52
|
+
];
|
|
53
|
+
export function isMarkdown(text) {
|
|
54
|
+
let matchCount = 0;
|
|
55
|
+
for (const pattern of MD_PATTERNS) {
|
|
56
|
+
if (pattern.test(text))
|
|
57
|
+
matchCount++;
|
|
58
|
+
if (matchCount >= 2)
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// 单个匹配:仅当文本较长时才认为是 markdown
|
|
62
|
+
return matchCount === 1 && text.length >= 80;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 将 markdown 内容转换为飞书交互式卡片格式
|
|
66
|
+
*/
|
|
67
|
+
function markdownToFeishuCard(content) {
|
|
68
|
+
return {
|
|
69
|
+
schema: '2.0',
|
|
70
|
+
config: { wide_screen_mode: true },
|
|
71
|
+
body: {
|
|
72
|
+
elements: [
|
|
73
|
+
{ tag: 'markdown', content },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 飞书通道适配器
|
|
80
|
+
*
|
|
81
|
+
* 将 FeishuSender 包装为 ChannelAdapter 接口。
|
|
82
|
+
* target 格式:
|
|
83
|
+
* - "chat_id" → sendText to chat
|
|
84
|
+
* - "reply/msg_id" → sendReply(向后兼容)
|
|
85
|
+
*/
|
|
86
|
+
export class FeishuAdapter {
|
|
87
|
+
scheme = 'feishu';
|
|
88
|
+
sender;
|
|
89
|
+
constructor(sender) {
|
|
90
|
+
this.sender = sender;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 将 mentions 注入到文本中(飞书 @ 格式)
|
|
94
|
+
*/
|
|
95
|
+
injectMentions(text, mentions) {
|
|
96
|
+
if (!mentions || mentions.length === 0)
|
|
97
|
+
return text;
|
|
98
|
+
const atTags = mentions.map(uid => `<at user_id="${uid}"></at>`).join(' ');
|
|
99
|
+
return `${atTags} ${text}`;
|
|
100
|
+
}
|
|
101
|
+
async send(target, message, options) {
|
|
102
|
+
const text = this.injectMentions(message, options?.mentions);
|
|
103
|
+
// 检测是否为 markdown 内容,且 sender 支持卡片发送
|
|
104
|
+
const useCard = isMarkdown(text) && !!this.sender.sendCard;
|
|
105
|
+
// 优先使用 options.replyTo
|
|
106
|
+
const replyTo = options?.replyTo;
|
|
107
|
+
// 向后兼容:target 以 "reply/" 开头
|
|
108
|
+
const resolvedReplyTo = replyTo || (target.startsWith('reply/') ? target.slice('reply/'.length) : undefined);
|
|
109
|
+
const chatTarget = target.startsWith('reply/') ? undefined : target;
|
|
110
|
+
if (useCard) {
|
|
111
|
+
const card = markdownToFeishuCard(text);
|
|
112
|
+
const id = await this.sender.sendCard({
|
|
113
|
+
receiveIdType: 'chat_id',
|
|
114
|
+
receiveId: chatTarget ?? '',
|
|
115
|
+
card,
|
|
116
|
+
replyToMessageId: resolvedReplyTo,
|
|
117
|
+
});
|
|
118
|
+
return { id };
|
|
119
|
+
}
|
|
120
|
+
// 纯文本发送
|
|
121
|
+
if (resolvedReplyTo) {
|
|
122
|
+
const id = await this.sender.sendReply({
|
|
123
|
+
text,
|
|
124
|
+
replyToMessageId: resolvedReplyTo,
|
|
125
|
+
});
|
|
126
|
+
return { id };
|
|
127
|
+
}
|
|
128
|
+
const id = await this.sender.sendText({
|
|
129
|
+
receiveIdType: 'chat_id',
|
|
130
|
+
receiveId: chatTarget ?? target,
|
|
131
|
+
text,
|
|
132
|
+
});
|
|
133
|
+
return { id };
|
|
134
|
+
}
|
|
135
|
+
async sendFile(target, file, message, options) {
|
|
136
|
+
const path = await import('node:path');
|
|
137
|
+
const fileName = file.fileName || path.basename(file.filePath);
|
|
138
|
+
// 确定回复目标
|
|
139
|
+
const replyTo = options?.replyTo || (target.startsWith('reply/') ? target.slice('reply/'.length) : undefined);
|
|
140
|
+
const chatTarget = target.startsWith('reply/') ? undefined : target;
|
|
141
|
+
// 自动检测媒体类型
|
|
142
|
+
const mediaKind = detectMediaKind(file.filePath);
|
|
143
|
+
let mediaMessageId;
|
|
144
|
+
if (mediaKind === 'image') {
|
|
145
|
+
// 图片:走 image.create 上传 → image 消息
|
|
146
|
+
if (!this.sender.uploadImage || !this.sender.sendImage) {
|
|
147
|
+
throw new Error('FeishuSender 未实现图片上传能力');
|
|
148
|
+
}
|
|
149
|
+
const imageKey = await this.sender.uploadImage(file.filePath);
|
|
150
|
+
mediaMessageId = await this.sender.sendImage({
|
|
151
|
+
receiveIdType: 'chat_id',
|
|
152
|
+
receiveId: chatTarget ?? '',
|
|
153
|
+
imageKey,
|
|
154
|
+
replyToMessageId: replyTo,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// 文件/音频:走 file.create 上传 → file/audio 消息
|
|
159
|
+
if (!this.sender.uploadFile || !this.sender.sendFile) {
|
|
160
|
+
throw new Error('FeishuSender 未实现文件上传能力');
|
|
161
|
+
}
|
|
162
|
+
const fileType = file.fileType || resolveFileType(file.filePath);
|
|
163
|
+
const fileKey = await this.sender.uploadFile({
|
|
164
|
+
filePath: file.filePath,
|
|
165
|
+
fileName,
|
|
166
|
+
fileType,
|
|
167
|
+
duration: file.duration,
|
|
168
|
+
});
|
|
169
|
+
const msgType = mediaKind === 'audio' ? 'audio' : 'file';
|
|
170
|
+
mediaMessageId = await this.sender.sendFile({
|
|
171
|
+
receiveIdType: 'chat_id',
|
|
172
|
+
receiveId: chatTarget ?? '',
|
|
173
|
+
fileKey,
|
|
174
|
+
msgType,
|
|
175
|
+
replyToMessageId: replyTo,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// 如果有附带文本,额外发一条文本消息
|
|
179
|
+
if (message) {
|
|
180
|
+
const text = this.injectMentions(message, options?.mentions);
|
|
181
|
+
if (replyTo) {
|
|
182
|
+
await this.sender.sendReply({ text, replyToMessageId: replyTo });
|
|
183
|
+
}
|
|
184
|
+
else if (chatTarget) {
|
|
185
|
+
await this.sender.sendText({ receiveIdType: 'chat_id', receiveId: chatTarget, text });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { id: mediaMessageId };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=feishu-adapter.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道适配器接口
|
|
3
|
+
*
|
|
4
|
+
* 统一飞书/Slack/Web/CLI 等不同通道的消息发送。
|
|
5
|
+
* channel.send 根据 channel 字段路由到对应 adapter。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 通道注册表
|
|
9
|
+
*
|
|
10
|
+
* 管理所有已注册的通道适配器。
|
|
11
|
+
* channel.send 通过 channel 字段查找对应的 adapter。
|
|
12
|
+
*/
|
|
13
|
+
export class ChannelRegistry {
|
|
14
|
+
adapters = new Map();
|
|
15
|
+
register(adapter) {
|
|
16
|
+
this.adapters.set(adapter.scheme, adapter);
|
|
17
|
+
}
|
|
18
|
+
get(scheme) {
|
|
19
|
+
return this.adapters.get(scheme);
|
|
20
|
+
}
|
|
21
|
+
has(scheme) {
|
|
22
|
+
return this.adapters.has(scheme);
|
|
23
|
+
}
|
|
24
|
+
getSchemes() {
|
|
25
|
+
return Array.from(this.adapters.keys());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.js.map
|