Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# code-bee · 持续集成工作流
#
# 核心功能:
# 1. 在代码推送或 PR 提交时自动运行编译、测试与静态分析
# 2. 复用 Makefile 中已有的 build / test / lint 目标,保持本地与 CI 行为一致
#
# 触发时机:
# - push 到 main / master 分支
# - 针对 main / master 的 Pull Request
#
# 维护: AI Assistant
# 创建时间: 2026-07-04
name: CI

on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master

# 同一分支的新提交会取消正在跑的旧任务,节省资源
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Build & Test & Lint
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout 代码
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true

- name: 下载依赖
run: go mod download

- name: 校验 go.mod 同步
run: |
go mod tidy
if [ -n "$(git status --porcelain go.mod go.sum)" ]; then
echo "::error::go.mod 或 go.sum 不同步,请在本地运行 go mod tidy 后提交"
git --no-pager diff -- go.mod go.sum
exit 1
fi

- name: 编译
run: make build

- name: 单元测试
run: make test

- name: 静态分析
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --config .golangci.yml
8 changes: 7 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ linters-settings:
statements: 40

mnd:
ignored-numbers: "0,1,2,10,100,1000"
ignored-numbers:
- "0"
- "1"
- "2"
- "10"
- "100"
- "1000"

gocyclo:
min-complexity: 15
Expand Down
97 changes: 67 additions & 30 deletions cmd/worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"context"
"flag"
"fmt"
"io"
"os"
"os/signal"

Expand All @@ -24,6 +25,11 @@ import (
"github.com/JiGuangWorker/code-bee/pkg/version"
)

// dispatchService 是调度器的抽象接口,使 CLI 逻辑可独立于具体的 pipeline.Service 进行测试。
type dispatchService interface {
Dispatch(context.Context, *config.Config) (*pipeline.Result, error)
}

// main 是 code-bee 的程序入口。
//
// 核心逻辑:
Expand All @@ -34,63 +40,94 @@ import (
// 调用注意事项:
// - 需要本机已安装 reasonix,以便生成回执和执行编码任务
func main() {
repo := flag.String("repo", "", "仓库地址,如 owner/repo")
issueNumber := flag.Int("issue", 0, "Issue 编号")
showVersion := flag.Bool("version", false, "输出版本信息")
flag.Parse()
os.Exit(runCLI(os.Args[1:], os.Stdout, os.Stderr, func() dispatchService {
return pipeline.NewService(
platformgithub.NewClient(),
agent.New(),
)
}))
}

// usageError 当参数缺失或解析失败时,向 stderr 输出统一的使用说明。
func usageError(stderr io.Writer) int {
fmt.Fprintf(stderr, "用法: code-bee --repo <owner/repo> --issue <number>\n\n")
fmt.Fprintf(stderr, "示例:\n")
fmt.Fprintf(stderr, " code-bee --repo owner/repo --issue 42\n")
return 1
}

// runCLI 是 CLI 入口的可测试版本,接收注入的 io.Writer 和 serviceFactory。
//
// 返回值:
// - 0: 成功完成
// - 1: 参数错误、调度失败或需要人工介入
func runCLI(args []string, stdout, stderr io.Writer, serviceFactory func() dispatchService) int {
fs := flag.NewFlagSet("code-bee", flag.ContinueOnError)
fs.SetOutput(stderr)

repo := fs.String("repo", "", "仓库地址,如 owner/repo")
issueNumber := fs.Int("issue", 0, "Issue 编号")
showVersion := fs.Bool("version", false, "输出版本信息")

if err := fs.Parse(args); err != nil {
return usageError(stderr)
}

if *showVersion {
fmt.Println(version.Info())
return
fmt.Fprintln(stdout, version.Info())
return 0
}

if *repo == "" || *issueNumber <= 0 {
fmt.Fprintf(os.Stderr, "用法: code-bee --repo <owner/repo> --issue <number>\n\n")
fmt.Fprintf(os.Stderr, "示例:\n")
fmt.Fprintf(os.Stderr, " code-bee --repo owner/repo --issue 42\n")
os.Exit(1)
return usageError(stderr)
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

fmt.Printf("🐝 code-bee %s\n", version.Version)
fmt.Printf("📦 仓库: %s | Issue: #%d\n\n", *repo, *issueNumber)
fmt.Fprintf(stdout, "🐝 code-bee %s\n", version.Version)
fmt.Fprintf(stdout, "📦 仓库: %s | Issue: #%d\n\n", *repo, *issueNumber)

cfg := config.New(*repo, *issueNumber)
service := pipeline.NewService(
platformgithub.NewClient(),
agent.New(),
)
service := serviceFactory()

fmt.Println("🚀 正在执行四阶段 harness:Issue 处理 -> 编码 -> 审查 -> Issue 提交...")
fmt.Fprintln(stdout, "🚀 正在执行四阶段 harness:Issue 处理 -> 编码 -> 审查 -> Issue 提交...")

result, err := service.Dispatch(ctx, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "\n❌ 执行失败: %v\n", err)
os.Exit(1)
fmt.Fprintf(stderr, "\n❌ 执行失败: %v\n", err)
return 1
}

return reportDispatchResult(result, stdout, stderr)
}

// reportDispatchResult 将调度结果映射为用户可见消息与退出码。
//
// 返回值:
// - 0: 任务顺利完成
// - 1: 阻塞、需要人工介入或智能体失败
func reportDispatchResult(result *pipeline.Result, stdout, stderr io.Writer) int {
if result.Success && result.Completed {
fmt.Println("\n✅ reviewer 已明确 PASS,且最终 Issue 回复已提交,任务执行完成")
return
fmt.Fprintln(stdout, "\n✅ reviewer 已明确 PASS,且最终 Issue 回复已提交,任务执行完成")
return 0
}

if result.Success && result.Blocked {
fmt.Fprintf(os.Stderr, "\n⏸️ 流程已阻塞,需要补充信息或人工介入:\n%s\n", result.Output)
os.Exit(1)
fmt.Fprintf(stderr, "\n⏸️ 流程已阻塞,需要补充信息或人工介入:\n%s\n", result.Output)
return 1
}

if result.Success {
if result.ManualRequired {
fmt.Fprintf(os.Stderr, "\n🧑‍🔧 自动循环已触达兜底阈值,需要人工介入:\n%s\n", result.Output)
os.Exit(1)
fmt.Fprintf(stderr, "\n🧑‍🔧 自动循环已触达兜底阈值,需要人工介入:\n%s\n", result.Output)
return 1
}

fmt.Fprintf(os.Stderr, "\n⏳ 自动循环结束但 reviewer 未 PASS:\n%s\n", result.Output)
os.Exit(1)
} else {
fmt.Fprintf(os.Stderr, "\n❌ 智能体返回失败:\n%s\n", result.Output)
os.Exit(1)
fmt.Fprintf(stderr, "\n⏳ 自动循环结束但 reviewer 未 PASS:\n%s\n", result.Output)
return 1
}

fmt.Fprintf(stderr, "\n❌ 智能体返回失败:\n%s\n", result.Output)
return 1
}
10 changes: 8 additions & 2 deletions internal/agent/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (
"os/exec"
)

// execCommandContext 是 exec.CommandContext 的可替换别名,便于单元测试注入模拟命令执行器。
var execCommandContext = exec.CommandContext

// RunResult 表示编码智能体的执行结果。
type RunResult struct {
// Success 表示本次智能体调用是否成功完成。
Expand All @@ -37,8 +40,11 @@ const (
// TaskKindReview 表示“审查是否满足要求”的任务阶段。
TaskKindReview TaskKind = "review"

// TaskKindIssuePost 表示校验结果文件并提交 Issue 评论的任务阶段。
// TaskKindIssuePost 表示"校验结果文件并提交 Issue 评论"的任务阶段。
TaskKindIssuePost TaskKind = "issue-post"

// TaskKindLoopJudge 表示"价值评估"的任务阶段,用于判断 coder-reviewer loop 是否还有继续价值。
TaskKindLoopJudge TaskKind = "loop-judge"
)

// Runner 管理编码智能体的调用。
Expand Down Expand Up @@ -70,7 +76,7 @@ func New() *Runner {
// 调用注意事项:
// - task 既可以是 Issue 处理任务,也可以是编码或审查任务
func (r *Runner) Run(ctx context.Context, kind TaskKind, task string) (*RunResult, error) {
cmd := exec.CommandContext(ctx,
cmd := execCommandContext(ctx,
"reasonix", "run", task,
)

Expand Down
36 changes: 30 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ package config

import "os"

// 默认值常量,集中维护避免散落硬编码。
const (
// defaultMaxCodingReviewRounds 是 coder-reviewer loop 默认最大轮数。
defaultMaxCodingReviewRounds = 3

// defaultLoopJudgeStartRound 是价值评估员默认触发起始轮。
// 0 表示始终触发。
defaultLoopJudgeStartRound = 0
)

// Config 表示 code-bee 的完整运行时配置。
type Config struct {
// Repo 是目标仓库,格式 owner/repo。
Expand All @@ -27,6 +37,17 @@ type Config struct {
// IssuePostAgent 是专门负责 Issue 提交的智能体。
// 该角色不直接编码,而是负责校验结果文件并完成最终评论提交。
IssuePostAgent string

// LoopJudgeAgent 是 coder-reviewer loop 中默认的价值评估智能体。
// 负责判断当前循环是否还有继续价值。
LoopJudgeAgent string

// LoopJudgeStartRound 表示从第几轮开始触发价值评估员。
// 0 表示始终触发;大于 0 则表示从该轮开始才触发。
LoopJudgeStartRound int

// MaxCodingReviewRounds 表示 coder-reviewer loop 的最大执行轮数。
MaxCodingReviewRounds int
}

// AgentRole 定义了一个可调度的智能体角色。
Expand Down Expand Up @@ -78,11 +99,14 @@ func AgentNames() []string {
// New 创建一个带有默认值的 Config。
func New(repo string, issueNumber int) *Config {
return &Config{
Repo: repo,
IssueNumber: issueNumber,
GitHubToken: os.Getenv("GITHUB_TOKEN"),
DefaultAgent: "开发者",
ReviewerAgent: "QA负责人",
IssuePostAgent: "产品经理",
Repo: repo,
IssueNumber: issueNumber,
GitHubToken: os.Getenv("GITHUB_TOKEN"),
DefaultAgent: "开发者",
ReviewerAgent: "QA负责人",
IssuePostAgent: "产品经理",
LoopJudgeAgent: "技术负责人",
MaxCodingReviewRounds: defaultMaxCodingReviewRounds,
LoopJudgeStartRound: defaultLoopJudgeStartRound,
}
}
29 changes: 29 additions & 0 deletions internal/pipeline/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ func (a *ArtifactSet) IssuePostResultPath(purpose string) string {
return filepath.Join(a.BaseDir, fmt.Sprintf("issue_post_result_%s.json", safePurpose))
}

// EnsureRoundDir 为指定轮次创建工件子目录。
func (a *ArtifactSet) EnsureRoundDir(round int) error {
dir := filepath.Join(a.BaseDir, fmt.Sprintf("round-%02d", round))
if err := os.MkdirAll(dir, artifactDirPermission); err != nil {
return fmt.Errorf("create round directory %s: %w", dir, err)
}
return nil
}

// CodingResultPathForRound 返回指定轮次的 coding 结果文件路径。
func (a *ArtifactSet) CodingResultPathForRound(round int) string {
return filepath.Join(a.BaseDir, fmt.Sprintf("round-%02d", round), "coding_result.json")
}

// ReviewResultPathForRound 返回指定轮次的 review 结果文件路径。
func (a *ArtifactSet) ReviewResultPathForRound(round int) string {
return filepath.Join(a.BaseDir, fmt.Sprintf("round-%02d", round), "review_result.json")
}

// LoopJudgeResultPath 返回指定轮次的 loop judge 结果文件路径。
func (a *ArtifactSet) LoopJudgeResultPath(round int) string {
return filepath.Join(a.BaseDir, fmt.Sprintf("round-%02d", round), "loop_judge_result.json")
}

// LoopHistoryPath 返回聚合历史文件路径。
func (a *ArtifactSet) LoopHistoryPath() string {
return filepath.Join(a.BaseDir, "loop_history.json")
}

// sanitizeRepoName 将 owner/repo 形式的仓库名转换为安全目录名。
func sanitizeRepoName(repo string) string {
replacer := strings.NewReplacer("/", "__", "\\", "__", " ", "_", ":", "_")
Expand Down
Loading
Loading