From f66a097dc318210d91cde935ac4a99c4a89b8598 Mon Sep 17 00:00:00 2001 From: Yoilun Date: Mon, 25 May 2026 15:39:51 +0800 Subject: [PATCH] docs: add implementation plan --- ...5-25-codex-agent-manager-implementation.md | 1701 +++++++++++++++++ .../2026-05-25-codex-agent-manager-design.md | 2 +- 2 files changed, 1702 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md diff --git a/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md b/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md new file mode 100644 index 0000000..429a50b --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md @@ -0,0 +1,1701 @@ +# Codex 智能体管理台 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 构建一个本机 localhost 中文 Web 工具,用 Go 后端和 Vue 3 + Vite 前端管理 Codex 智能体配置、项目运行状态和动态工作流交接。 + +**Architecture:** 单体本机 Web 工具。Go 后端负责安全读取 `.codex` 文件、只读 SQLite、工作流聚合和确认写回;Vue 3 + Vite 前端负责中文工作台 UI、状态置信度展示、草稿、diff 和写回流程。 + +**Tech Stack:** Go 1.22+、Vue 3、Vite、原生 CSS、SQLite 只读访问、TOML 解析、localhost HTTP API。 + +--- + +## File Structure + +### Planning and Documentation + +- Create `task_plan.md`: 阶段计划、状态、验收标准、错误记录。 +- Create `findings.md`: `.codex` 数据源、SQLite schema、风险、关键决策。 +- Create `progress.md`: 每阶段执行记录、测试结果、审查循环。 +- Create `docs/project.md`: 目标、架构、配置、运行方式、安全边界、恢复方式。 +- Modify `docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md`: 当实现决策改变规格时更新。 + +### Go Backend + +- Create `go.mod`: Go module and dependencies. +- Create `cmd/codex-agent-manager/main.go`: 后端入口,启动 localhost 服务。 +- Create `internal/app/config.go`: 应用配置、默认 Codex home、端口。 +- Create `internal/codexhome/bounds.go`: 路径边界、安全检查、禁止文件规则。 +- Create `internal/agents/model.go`: AgentDefinition、Draft、Diff 模型。 +- Create `internal/agents/store.go`: 读取 `.codex/agents/*.toml`、草稿、校验、备份、写回。 +- Create `internal/projects/store.go`: 读取 `.codex/config.toml` 中项目配置。 +- Create `internal/runtime/model.go`: RuntimeThread、StatusEvidence、GoalStatus 模型。 +- Create `internal/runtime/store.go`: 只读 SQLite 查询和进程状态聚合接口。 +- Create `internal/workflow/model.go`: WorkflowEvent、WorkflowEdge、WorkflowPhase 模型。 +- Create `internal/workflow/store.go`: 聚合 threads、spawn edges、goals、计划文件。 +- Create `internal/server/server.go`: HTTP router and JSON helpers. +- Create `internal/server/handlers.go`: API handlers. +- Create tests under `internal/**`: 覆盖路径安全、TOML、SQLite 查询、工作流推断、写回安全。 + +### Vue Frontend + +- Create `web/package.json`: Vue/Vite scripts and dependencies. +- Create `web/vite.config.js`: dev server and proxy. +- Create `web/index.html`: app shell. +- Create `web/src/main.js`: Vue app bootstrap. +- Create `web/src/App.vue`: 中文主框架与标签页。 +- Create `web/src/api/client.js`: API client. +- Create `web/src/views/ProjectView.vue`: 项目视图。 +- Create `web/src/views/WorkflowView.vue`: 工作流视图。 +- Create `web/src/views/AgentView.vue`: 智能体视图。 +- Create `web/src/views/DraftsView.vue`: 草稿视图。 +- Create `web/src/views/SettingsView.vue`: 设置视图。 +- Create `web/src/components/StatusBadge.vue`: 状态和置信度。 +- Create `web/src/components/HandoffTimeline.vue`: 动态交接流。 +- Create `web/src/components/WorkflowGraph.vue`: 初版列表/树形交接图。 +- Create `web/src/components/DiffViewer.vue`: 字段级 diff。 +- Create `web/src/components/WritebackSteps.vue`: 草稿 -> 已校验 -> 已备份 -> 已写回。 +- Create `web/src/styles.css`: 温和工作台视觉系统。 + +--- + +### Task 0: 文件化计划和项目基线 + +**Files:** +- Create: `task_plan.md` +- Create: `findings.md` +- Create: `progress.md` +- Create: `docs/project.md` +- Modify: `.gitignore` + +- [ ] **Step 1: Create workflow tracking files** + +Write `task_plan.md`: + +```markdown +# Task Plan + +## Goal + +构建 Codex 智能体管理台:Go 后端 + Vue 3 前端,支持中文界面、agent 配置管理、项目运行状态、动态工作流交接和安全写回。 + +## Stop Conditions + +- [ ] 所有阶段完成 +- [ ] Go 测试通过 +- [ ] 前端构建通过 +- [ ] 浏览器验证关键页面 +- [ ] docs/project.md 记录目标、架构、配置、运行方式、安全边界和恢复方式 +- [ ] 无 blocking bug 或未处理高风险问题 + +## Phases + +| Phase | Status | Goal | Acceptance Criteria | +| --- | --- | --- | --- | +| 0 | pending | 项目初始化与风险边界 | 计划文件存在;安全边界明确;不读取 auth.json;不改 .codex | +| 1 | pending | Go 只读数据层 | 能读取 agents、projects、threads、spawn edges、goals;SQLite 只读 | +| 2 | pending | 运行状态与动态工作流模型 | 状态含来源/置信度;工作流不写死固定流程 | +| 3 | pending | 中文 UI 只读工作台 | 项目/工作流/智能体视图可浏览;空数据可用 | +| 4 | pending | 草稿、TOML 校验和 diff | 草稿不覆盖原文件;无效 TOML 阻止写回 | +| 5 | pending | 备份与确认写回 | 备份成功后才能写回;失败可恢复 | +| 6 | pending | 集成验证与文档 | 测试/构建/浏览器验证通过;文档完整 | + +## Errors Encountered + +| Time | Phase | Error | Attempt | Resolution | +| --- | --- | --- | --- | --- | +``` + +Write `findings.md`: + +```markdown +# Findings + +## Architecture + +- 后端使用 Go。 +- 前端使用 Vue 3 + Vite。 +- 工具以 localhost Web 应用运行。 + +## Data Sources + +- `.codex/agents/*.toml`: 智能体配置。 +- `.codex/config.toml`: 项目信任配置。 +- `.codex/state_5.sqlite`: threads、thread_spawn_edges、thread_dynamic_tools。 +- `.codex/goals_1.sqlite`: thread_goals。 +- 本机进程表:Codex 进程辅助判断。 +- 项目工作流文件:task_plan.md、findings.md、progress.md、docs/project.md。 + +## Constraints + +- 禁止读取或展示 `.codex/auth.json`。 +- 禁止写入 SQLite。 +- 默认不写 `.codex/config.toml`。 +- `.codex/agents/*.toml` 仅允许经草稿、校验、diff、备份、确认后写回。 + +## Decisions + +- 工作流显示使用动态事件流/DAG,不写死阶段或 agent 顺序。 +- 所有推断状态必须显示来源和置信度。 +- UI 全中文,技术缩写保留英文。 +``` + +Write `progress.md`: + +```markdown +# Progress + +## Session Log + +| Time | Phase | Actor | Action | Result | +| --- | --- | --- | --- | --- | + +## Test Results + +| Time | Command | Result | Notes | +| --- | --- | --- | --- | + +## Bug Loop + +| Phase | Bug | Fix Attempt | Retest Result | +| --- | --- | --- | --- | +``` + +Write `docs/project.md`: + +```markdown +# Project Documentation + +## Goal + +构建一个本机 Codex 智能体管理台,用中文管理 agent 配置、项目运行状态、动态工作流和安全写回流程。 + +## Architecture + +Go 后端提供 localhost HTTP API,Vue 3 + Vite 前端提供中文工作台界面。后端只读访问 Codex SQLite 状态库,安全读取 `.codex` 配置,并仅在用户确认后写回 `.codex/agents/*.toml`。 + +## Configuration + +- `CODEX_HOME`: 默认 `/Users/yoilun/.codex` +- 后端监听地址:默认 `127.0.0.1:18083` +- 前端开发地址:默认 `127.0.0.1:13083` + +## Runbook + +实施完成后记录实际命令。 + +## Security Boundaries + +- 不读取 `.codex/auth.json`。 +- 不写入 Codex SQLite。 +- `.codex/agents/*.toml` 写回必须先备份。 + +## Known Risks + +- Codex 内部 SQLite schema 可能变化。 +- 运行状态由多来源推断,必须显示置信度。 +``` + +- [ ] **Step 2: Update `.gitignore` for generated artifacts** + +Ensure `.gitignore` includes: + +```gitignore +.DS_Store +.superpowers/ +dist/ +node_modules/ +*.log +tmp/ +*.bak.* +``` + +- [ ] **Step 3: Commit planning baseline** + +Run: + +```bash +git add task_plan.md findings.md progress.md docs/project.md .gitignore +git commit -m "chore: add implementation tracking files" +``` + +Expected: commit succeeds. + +--- + +### Task 1: Go 项目骨架和安全边界 + +**Files:** +- Create: `go.mod` +- Create: `cmd/codex-agent-manager/main.go` +- Create: `internal/app/config.go` +- Create: `internal/codexhome/bounds.go` +- Create: `internal/codexhome/bounds_test.go` +- Modify: `docs/project.md` + +- [ ] **Step 1: Write path boundary tests** + +Create `internal/codexhome/bounds_test.go`: + +```go +package codexhome + +import ( + "path/filepath" + "testing" +) + +func TestResolveInsideCodexHomeAllowsAgentsToml(t *testing.T) { + home := filepath.Join(t.TempDir(), ".codex") + got, err := ResolveInside(home, "agents/product-manager.toml") + if err != nil { + t.Fatalf("ResolveInside returned error: %v", err) + } + want := filepath.Join(home, "agents", "product-manager.toml") + if got != want { + t.Fatalf("path mismatch: got %q want %q", got, want) + } +} + +func TestResolveInsideCodexHomeRejectsTraversal(t *testing.T) { + home := filepath.Join(t.TempDir(), ".codex") + _, err := ResolveInside(home, "../auth.json") + if err == nil { + t.Fatal("expected traversal to be rejected") + } +} + +func TestIsForbiddenPathBlocksAuthJSON(t *testing.T) { + home := filepath.Join(t.TempDir(), ".codex") + path := filepath.Join(home, "auth.json") + if !IsForbidden(path, home) { + t.Fatal("auth.json must be forbidden") + } +} +``` + +- [ ] **Step 2: Run failing test** + +Run: + +```bash +go test ./internal/codexhome +``` + +Expected: FAIL because package files are not implemented yet. + +- [ ] **Step 3: Implement config and path boundary** + +Create `go.mod`: + +```go +module codex-agent-manager + +go 1.22 +``` + +Create `internal/app/config.go`: + +```go +package app + +import ( + "os" + "path/filepath" +) + +type Config struct { + CodexHome string + HTTPAddr string +} + +func DefaultConfig() Config { + home, err := os.UserHomeDir() + if err != nil { + home = "." + } + return Config{ + CodexHome: filepath.Join(home, ".codex"), + HTTPAddr: "127.0.0.1:18083", + } +} +``` + +Create `internal/codexhome/bounds.go`: + +```go +package codexhome + +import ( + "errors" + "path/filepath" + "strings" +) + +var ErrOutsideCodexHome = errors.New("路径超出 Codex home") +var ErrForbiddenPath = errors.New("禁止访问敏感路径") + +func ResolveInside(home string, rel string) (string, error) { + if filepath.IsAbs(rel) { + return "", ErrOutsideCodexHome + } + cleanHome, err := filepath.Abs(home) + if err != nil { + return "", err + } + candidate, err := filepath.Abs(filepath.Join(cleanHome, rel)) + if err != nil { + return "", err + } + relative, err := filepath.Rel(cleanHome, candidate) + if err != nil { + return "", err + } + if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return "", ErrOutsideCodexHome + } + if IsForbidden(candidate, cleanHome) { + return "", ErrForbiddenPath + } + return candidate, nil +} + +func IsForbidden(path string, home string) bool { + cleanHome, err := filepath.Abs(home) + if err != nil { + return true + } + cleanPath, err := filepath.Abs(path) + if err != nil { + return true + } + rel, err := filepath.Rel(cleanHome, cleanPath) + if err != nil { + return true + } + rel = filepath.ToSlash(rel) + forbidden := map[string]bool{ + "auth.json": true, + } + return forbidden[rel] +} +``` + +Create `cmd/codex-agent-manager/main.go`: + +```go +package main + +import ( + "fmt" + "net/http" + + "codex-agent-manager/internal/app" +) + +func main() { + cfg := app.DefaultConfig() + mux := http.NewServeMux() + mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"ok"}`)) + }) + fmt.Printf("Codex 智能体管理台监听 http://%s\n", cfg.HTTPAddr) + if err := http.ListenAndServe(cfg.HTTPAddr, mux); err != nil { + panic(err) + } +} +``` + +- [ ] **Step 4: Run tests** + +Run: + +```bash +go test ./... +``` + +Expected: PASS. + +- [ ] **Step 5: Update docs and commit** + +Update `docs/project.md` runbook with: + +```markdown +## Runbook + +启动后端: + +```bash +go run ./cmd/codex-agent-manager +``` + +健康检查: + +```bash +curl http://127.0.0.1:18083/api/health +``` +``` + +Run: + +```bash +git add go.mod cmd internal docs/project.md +git commit -m "feat: add go backend skeleton" +``` + +Expected: commit succeeds. + +--- + +### Task 2: Agent TOML 只读读取 + +**Files:** +- Create: `internal/agents/model.go` +- Create: `internal/agents/store.go` +- Create: `internal/agents/store_test.go` +- Modify: `internal/server/server.go` +- Modify: `cmd/codex-agent-manager/main.go` + +- [ ] **Step 1: Add tests for agent TOML parsing** + +Create `internal/agents/store_test.go`: + +```go +package agents + +import ( + "os" + "path/filepath" + "testing" +) + +func TestListAgentsReadsTomlFiles(t *testing.T) { + root := t.TempDir() + agentsDir := filepath.Join(root, "agents") + if err := os.MkdirAll(agentsDir, 0o755); err != nil { + t.Fatal(err) + } + content := `name = "产品经理" +description = "负责产品定义" +developer_instructions = """ +用中文定义产品需求。 +""" +` + if err := os.WriteFile(filepath.Join(agentsDir, "product-manager.toml"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + store := Store{CodexHome: root} + got, err := store.List() + if err != nil { + t.Fatalf("List returned error: %v", err) + } + if len(got) != 1 { + t.Fatalf("agent count = %d, want 1", len(got)) + } + if got[0].Name != "产品经理" || got[0].Description != "负责产品定义" { + t.Fatalf("unexpected agent: %#v", got[0]) + } +} + +func TestListAgentsReportsParseError(t *testing.T) { + root := t.TempDir() + agentsDir := filepath.Join(root, "agents") + if err := os.MkdirAll(agentsDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(agentsDir, "bad.toml"), []byte(`name = "`), 0o644); err != nil { + t.Fatal(err) + } + store := Store{CodexHome: root} + got, err := store.List() + if err != nil { + t.Fatalf("List should return parse status, not fatal error: %v", err) + } + if len(got) != 1 || got[0].ParseStatus != "invalid" { + t.Fatalf("expected invalid parse status, got %#v", got) + } +} +``` + +- [ ] **Step 2: Run failing test** + +Run: + +```bash +go test ./internal/agents +``` + +Expected: FAIL because `Store` is undefined. + +- [ ] **Step 3: Implement agent model and store** + +Create `internal/agents/model.go`: + +```go +package agents + +import "time" + +type AgentDefinition struct { + ID string `json:"id"` + FilePath string `json:"filePath"` + FileName string `json:"fileName"` + Name string `json:"name"` + Description string `json:"description"` + DeveloperInstructions string `json:"developerInstructions"` + ExtraFields map[string]string `json:"extraFields"` + ModifiedAt time.Time `json:"modifiedAt"` + ParseStatus string `json:"parseStatus"` + ParseError string `json:"parseError,omitempty"` + DraftStatus string `json:"draftStatus"` +} +``` + +Create `internal/agents/store.go`: + +```go +package agents + +import ( + "bufio" + "os" + "path/filepath" + "sort" + "strings" +) + +type Store struct { + CodexHome string +} + +func (s Store) List() ([]AgentDefinition, error) { + pattern := filepath.Join(s.CodexHome, "agents", "*.toml") + files, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + sort.Strings(files) + result := make([]AgentDefinition, 0, len(files)) + for _, file := range files { + result = append(result, s.readOne(file)) + } + return result, nil +} + +func (s Store) readOne(path string) AgentDefinition { + info, statErr := os.Stat(path) + def := AgentDefinition{ + ID: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)), + FilePath: path, + FileName: filepath.Base(path), + ParseStatus: "valid", + DraftStatus: "clean", + ExtraFields: map[string]string{}, + } + if statErr == nil { + def.ModifiedAt = info.ModTime() + } + data, err := os.ReadFile(path) + if err != nil { + def.ParseStatus = "invalid" + def.ParseError = err.Error() + return def + } + values, err := parseSimpleTOML(string(data)) + if err != nil { + def.ParseStatus = "invalid" + def.ParseError = err.Error() + return def + } + def.Name = values["name"] + def.Description = values["description"] + def.DeveloperInstructions = values["developer_instructions"] + for key, value := range values { + if key != "name" && key != "description" && key != "developer_instructions" { + def.ExtraFields[key] = value + } + } + return def +} + +func parseSimpleTOML(input string) (map[string]string, error) { + values := map[string]string{} + scanner := bufio.NewScanner(strings.NewReader(input)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + raw := strings.TrimSpace(parts[1]) + if strings.HasPrefix(raw, `"""`) { + block := strings.TrimPrefix(raw, `"""`) + for !strings.Contains(block, `"""`) && scanner.Scan() { + block += "\n" + scanner.Text() + } + if !strings.Contains(block, `"""`) { + return values, errInvalidTOML("未闭合的多行字符串") + } + values[key] = strings.SplitN(block, `"""`, 2)[0] + continue + } + if !strings.HasPrefix(raw, `"`) || !strings.HasSuffix(raw, `"`) { + return values, errInvalidTOML("仅支持字符串字段") + } + values[key] = strings.TrimSuffix(strings.TrimPrefix(raw, `"`), `"`) + } + return values, scanner.Err() +} + +type errInvalidTOML string + +func (e errInvalidTOML) Error() string { return string(e) } +``` + +- [ ] **Step 4: Run tests** + +Run: + +```bash +go test ./internal/agents +go test ./... +``` + +Expected: PASS. + +- [ ] **Step 5: Add `/api/agents` handler** + +Create `internal/server/server.go`: + +```go +package server + +import ( + "encoding/json" + "net/http" + + "codex-agent-manager/internal/agents" + "codex-agent-manager/internal/app" +) + +func New(cfg app.Config) http.Handler { + mux := http.NewServeMux() + agentStore := agents.Store{CodexHome: cfg.CodexHome} + mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) + }) + mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) + return + } + items, err := agentStore.List() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) + }) + return mux +} + +func writeJSON(w http.ResponseWriter, status int, body any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(body) +} +``` + +Modify `cmd/codex-agent-manager/main.go` to use `server.New(cfg)`: + +```go +package main + +import ( + "fmt" + "net/http" + + "codex-agent-manager/internal/app" + "codex-agent-manager/internal/server" +) + +func main() { + cfg := app.DefaultConfig() + fmt.Printf("Codex 智能体管理台监听 http://%s\n", cfg.HTTPAddr) + if err := http.ListenAndServe(cfg.HTTPAddr, server.New(cfg)); err != nil { + panic(err) + } +} +``` + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add internal cmd +git commit -m "feat: read codex agent definitions" +``` + +Expected: commit succeeds. + +--- + +### Task 3: 项目配置、运行线程和工作流只读模型 + +**Files:** +- Create: `internal/projects/store.go` +- Create: `internal/runtime/model.go` +- Create: `internal/runtime/store.go` +- Create: `internal/workflow/model.go` +- Create: `internal/workflow/store.go` +- Create tests for each package +- Modify: `internal/server/server.go` + +- [ ] **Step 1: Write tests for project config parser** + +Create `internal/projects/store_test.go`: + +```go +package projects + +import ( + "os" + "path/filepath" + "testing" +) + +func TestListProjectsParsesTrustConfig(t *testing.T) { + root := t.TempDir() + cfg := filepath.Join(root, "config.toml") + content := `[projects."/Users/yoilun"] +trust_level = "trusted" + +[projects."/Users/yoilun/Code/managed-portal"] +trust_level = "trusted" +` + if err := os.WriteFile(cfg, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + store := Store{CodexHome: root} + got, err := store.List() + if err != nil { + t.Fatal(err) + } + if len(got) != 2 { + t.Fatalf("project count = %d, want 2", len(got)) + } + if got[0].Path != "/Users/yoilun" || got[0].TrustLevel != "trusted" { + t.Fatalf("unexpected first project: %#v", got[0]) + } +} +``` + +- [ ] **Step 2: Implement project parser** + +Create `internal/projects/store.go`: + +```go +package projects + +import ( + "os" + "path/filepath" + "sort" + "strings" +) + +type ProjectInfo struct { + ID string `json:"id"` + Path string `json:"path"` + Name string `json:"name"` + TrustLevel string `json:"trustLevel"` + Exists bool `json:"exists"` +} + +type Store struct { + CodexHome string +} + +func (s Store) List() ([]ProjectInfo, error) { + data, err := os.ReadFile(filepath.Join(s.CodexHome, "config.toml")) + if err != nil { + return nil, err + } + lines := strings.Split(string(data), "\n") + var result []ProjectInfo + var current *ProjectInfo + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, `[projects."`) && strings.HasSuffix(line, `"]`) { + path := strings.TrimSuffix(strings.TrimPrefix(line, `[projects."`), `"]`) + info := ProjectInfo{ID: pathToID(path), Path: path, Name: filepath.Base(path)} + if _, err := os.Stat(path); err == nil { + info.Exists = true + } + result = append(result, info) + current = &result[len(result)-1] + continue + } + if current != nil && strings.HasPrefix(line, "trust_level") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + current.TrustLevel = strings.Trim(strings.TrimSpace(parts[1]), `"`) + } + } + } + sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path }) + return result, nil +} + +func pathToID(path string) string { + id := strings.Trim(path, string(filepath.Separator)) + id = strings.ReplaceAll(id, string(filepath.Separator), "__") + if id == "" { + return "root" + } + return id +} +``` + +- [ ] **Step 3: Write workflow model test** + +Create `internal/workflow/store_test.go`: + +```go +package workflow + +import "testing" + +func TestBuildEventsFromSpawnEdgesDoesNotAssumeFixedFlow(t *testing.T) { + edges := []EdgeInput{ + {ParentID: "main", ChildID: "child-1", ParentName: "主智能体", ChildName: "Cicero", ChildRole: "UI设计师", Status: "open"}, + {ParentID: "main", ChildID: "child-2", ParentName: "主智能体", ChildName: "Gauss", ChildRole: "智能体编排者", Status: "closed"}, + } + got := BuildEvents(edges) + if len(got) != 2 { + t.Fatalf("event count = %d, want 2", len(got)) + } + if got[0].Type != "主智能体派发" || got[0].Confidence != "high" { + t.Fatalf("unexpected event: %#v", got[0]) + } + if got[1].Target.Role != "智能体编排者" { + t.Fatalf("role should come from data, got %#v", got[1]) + } +} +``` + +- [ ] **Step 4: Implement workflow model** + +Create `internal/workflow/model.go`: + +```go +package workflow + +type Actor struct { + ThreadID string `json:"threadId"` + Name string `json:"name"` + Role string `json:"role"` +} + +type WorkflowEvent struct { + ID string `json:"id"` + Type string `json:"type"` + Actor Actor `json:"actor"` + Target Actor `json:"target"` + Summary string `json:"summary"` + Source string `json:"source"` + Confidence string `json:"confidence"` +} + +type EdgeInput struct { + ParentID string + ChildID string + ParentName string + ParentRole string + ChildName string + ChildRole string + Status string +} +``` + +Create `internal/workflow/store.go`: + +```go +package workflow + +func BuildEvents(edges []EdgeInput) []WorkflowEvent { + events := make([]WorkflowEvent, 0, len(edges)) + for _, edge := range edges { + parentName := edge.ParentName + if parentName == "" { + parentName = "主智能体" + } + childName := edge.ChildName + if childName == "" { + childName = edge.ChildID + } + events = append(events, WorkflowEvent{ + ID: edge.ParentID + "->" + edge.ChildID, + Type: "主智能体派发", + Actor: Actor{ + ThreadID: edge.ParentID, + Name: parentName, + Role: edge.ParentRole, + }, + Target: Actor{ + ThreadID: edge.ChildID, + Name: childName, + Role: edge.ChildRole, + }, + Summary: parentName + " 派发任务给 " + childName, + Source: "thread_spawn_edges", + Confidence: "high", + }) + } + return events +} +``` + +- [ ] **Step 5: Run tests and commit** + +Run: + +```bash +go test ./... +git add internal +git commit -m "feat: add project and workflow read models" +``` + +Expected: tests pass and commit succeeds. + +--- + +### Task 4: 中文只读前端工作台 + +**Files:** +- Create frontend files under `web/` +- Modify: `docs/project.md` + +- [ ] **Step 1: Create Vue/Vite files** + +Create `web/package.json`: + +```json +{ + "name": "codex-agent-manager-web", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 13083", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 13084" + }, + "dependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0", + "vue": "^3.4.0" + }, + "devDependencies": {} +} +``` + +Create `web/vite.config.js`: + +```js +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + proxy: { + '/api': 'http://127.0.0.1:18083' + } + } +}) +``` + +Create `web/index.html`: + +```html + + + + + + Codex 智能体管理台 + + +
+ + + +``` + +Create `web/src/main.js`: + +```js +import { createApp } from 'vue' +import App from './App.vue' +import './styles.css' + +createApp(App).mount('#app') +``` + +- [ ] **Step 2: Implement app shell** + +Create `web/src/App.vue`: + +```vue + + + +``` + +Create `web/src/styles.css`: + +```css +:root { + color: #20251f; + background: #f4f1ea; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body { + margin: 0; + background: #f4f1ea; +} + +.shell { + min-height: 100vh; + padding: 24px; +} + +.topbar { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: center; + margin-bottom: 18px; +} + +.topbar h1 { + margin: 0; + font-size: 28px; +} + +.topbar p { + margin: 6px 0 0; + color: #6b675d; +} + +.search { + width: min(420px, 40vw); + border: 1px solid #ddd2bf; + border-radius: 8px; + background: #fffaf0; + padding: 12px; + font-size: 14px; +} + +.tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.tab { + border: 1px solid #ded8cc; + background: #fffaf0; + color: #3e403b; + border-radius: 8px; + padding: 10px 14px; + font-weight: 650; +} + +.tab.active { + background: #263f38; + color: #fff; + border-color: #263f38; +} + +.panel { + border: 1px solid #ded8cc; + background: #fffdf8; + border-radius: 8px; + padding: 16px; +} +``` + +- [ ] **Step 3: Create placeholder views with real Chinese labels** + +Create `web/src/views/ProjectView.vue`: + +```vue + +``` + +Create `web/src/views/WorkflowView.vue`: + +```vue + +``` + +Create `web/src/views/AgentView.vue`: + +```vue + +``` + +Create `web/src/views/DraftsView.vue`: + +```vue + +``` + +Create `web/src/views/SettingsView.vue`: + +```vue + +``` + +- [ ] **Step 4: Build frontend** + +Run: + +```bash +cd web && pnpm install && pnpm build +``` + +Expected: build succeeds. If network blocks dependency install, request escalation or document the blocker. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add web docs/project.md +git commit -m "feat: add chinese vue workbench shell" +``` + +Expected: commit succeeds. + +--- + +### Task 5: API integration and read-only data display + +**Files:** +- Modify: `web/src/api/client.js` +- Modify: `web/src/views/ProjectView.vue` +- Modify: `web/src/views/WorkflowView.vue` +- Modify: `web/src/views/AgentView.vue` +- Modify: `web/src/components/StatusBadge.vue` + +- [ ] **Step 1: Add API client** + +Create `web/src/api/client.js`: + +```js +export async function getJSON(path) { + const response = await fetch(path) + if (!response.ok) { + const text = await response.text() + throw new Error(text || `请求失败:${response.status}`) + } + return response.json() +} + +export const api = { + agents: () => getJSON('/api/agents'), + projects: () => getJSON('/api/projects'), + projectRuntime: id => getJSON(`/api/projects/${encodeURIComponent(id)}/runtime`), + projectWorkflow: id => getJSON(`/api/projects/${encodeURIComponent(id)}/workflow`) +} +``` + +- [ ] **Step 2: Add StatusBadge component** + +Create `web/src/components/StatusBadge.vue`: + +```vue + + + +``` + +Add to `web/src/styles.css`: + +```css +.status-badge { + display: inline-flex; + border-radius: 999px; + padding: 6px 10px; + background: #f7eddc; + color: #7a4d13; + font-size: 12px; +} + +.status-badge[data-confidence="high"] { + background: #e7f4ec; + color: #265a3c; +} +``` + +- [ ] **Step 3: Integrate AgentView with `/api/agents`** + +Update `web/src/views/AgentView.vue`: + +```vue + + + +``` + +Add to `web/src/styles.css`: + +```css +.list { + display: grid; + gap: 10px; + margin-top: 16px; +} + +.row-card { + display: grid; + gap: 6px; + padding: 12px; + border: 1px solid #e3d8c8; + background: #fffaf0; + border-radius: 8px; +} + +.row-card span, +.row-card small { + color: #6f665a; +} + +.error { + color: #9f3a2f; +} +``` + +- [ ] **Step 4: Run build and commit** + +Run: + +```bash +cd web && pnpm build +git add web +git commit -m "feat: connect ui to readonly agent api" +``` + +Expected: build succeeds and commit succeeds. + +--- + +### Task 6: 草稿、校验、diff、备份、写回 + +**Files:** +- Modify: `internal/agents/store.go` +- Create: `internal/agents/writeback_test.go` +- Modify: `internal/server/server.go` +- Modify: `web/src/views/AgentView.vue` +- Modify: `web/src/views/DraftsView.vue` + +- [ ] **Step 1: Write writeback safety tests** + +Create `internal/agents/writeback_test.go`: + +```go +package agents + +import ( + "os" + "path/filepath" + "testing" +) + +func TestBackupBeforeWriteCreatesBackupAndWrites(t *testing.T) { + root := t.TempDir() + agentsDir := filepath.Join(root, "agents") + if err := os.MkdirAll(agentsDir, 0o755); err != nil { + t.Fatal(err) + } + path := filepath.Join(agentsDir, "demo.toml") + original := `name = "旧名称" +description = "旧描述" +developer_instructions = """ +旧角色 +""" +` + if err := os.WriteFile(path, []byte(original), 0o644); err != nil { + t.Fatal(err) + } + store := Store{CodexHome: root} + newContent := `name = "新名称" +description = "新描述" +developer_instructions = """ +新角色 +""" +` + result, err := store.WriteWithBackup("demo", newContent) + if err != nil { + t.Fatalf("WriteWithBackup returned error: %v", err) + } + if result.BackupPath == "" { + t.Fatal("expected backup path") + } + written, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(written) != newContent { + t.Fatalf("file was not written") + } +} + +func TestWriteWithBackupRejectsInvalidToml(t *testing.T) { + root := t.TempDir() + agentsDir := filepath.Join(root, "agents") + if err := os.MkdirAll(agentsDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(agentsDir, "demo.toml"), []byte(`name = "旧"`), 0o644); err != nil { + t.Fatal(err) + } + store := Store{CodexHome: root} + _, err := store.WriteWithBackup("demo", `name = "`) + if err == nil { + t.Fatal("expected invalid TOML to be rejected") + } +} +``` + +- [ ] **Step 2: Implement writeback with backup** + +Add to `internal/agents/model.go`: + +```go +type WriteResult struct { + TargetPath string `json:"targetPath"` + BackupPath string `json:"backupPath"` + Status string `json:"status"` +} +``` + +Add to `internal/agents/store.go`: + +```go +func (s Store) WriteWithBackup(id string, content string) (WriteResult, error) { + if _, err := parseSimpleTOML(content); err != nil { + return WriteResult{}, err + } + target := filepath.Join(s.CodexHome, "agents", id+".toml") + cleanAgentsDir := filepath.Join(s.CodexHome, "agents") + rel, err := filepath.Rel(cleanAgentsDir, target) + if err != nil || strings.HasPrefix(rel, "..") { + return WriteResult{}, errInvalidTOML("写回目标不在 agents 目录") + } + current, err := os.ReadFile(target) + if err != nil { + return WriteResult{}, err + } + backup := target + ".bak." + time.Now().Format("20060102-150405") + if err := os.WriteFile(backup, current, 0o600); err != nil { + return WriteResult{}, err + } + tmp := target + ".tmp" + if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil { + return WriteResult{}, err + } + if err := os.Rename(tmp, target); err != nil { + return WriteResult{}, err + } + return WriteResult{TargetPath: target, BackupPath: backup, Status: "已写回"}, nil +} +``` + +Also add `time` to imports. + +- [ ] **Step 3: Run tests** + +Run: + +```bash +go test ./internal/agents +go test ./... +``` + +Expected: PASS. + +- [ ] **Step 4: Add server endpoints and UI flow** + +Implement minimal endpoints: + +- `POST /api/agents/{id}/validate` +- `POST /api/agents/{id}/write` + +UI must show buttons in Chinese: + +- `校验 TOML` +- `查看差异` +- `创建备份并写回` + +Expected behavior: + +- invalid TOML shows Chinese error and disables writeback. +- writeback response shows target path and backup path. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add internal web +git commit -m "feat: add safe agent writeback flow" +``` + +Expected: commit succeeds. + +--- + +### Task 7: 集成验证、浏览器检查和最终文档 + +**Files:** +- Modify: `docs/project.md` +- Modify: `README.md` +- Modify: `task_plan.md` +- Modify: `progress.md` +- Modify: `findings.md` + +- [ ] **Step 1: Create README** + +Create `README.md`: + +```markdown +# Codex 智能体管理台 + +本项目是本机 localhost 工具,用中文管理 Codex 智能体配置、项目运行状态和动态工作流交接。 + +## 启动后端 + +```bash +go run ./cmd/codex-agent-manager +``` + +## 启动前端 + +```bash +cd web +pnpm install +pnpm dev +``` + +## 验证 + +```bash +go test ./... +cd web && pnpm build +``` + +## 安全边界 + +- 不读取 `.codex/auth.json`。 +- 不写入 Codex SQLite。 +- `.codex/agents/*.toml` 写回前必须校验、diff、备份和确认。 +``` + +- [ ] **Step 2: Run full verification** + +Run: + +```bash +go test ./... +cd web && pnpm build +``` + +Expected: both pass. + +- [ ] **Step 3: Browser smoke test** + +Start services: + +```bash +go run ./cmd/codex-agent-manager +cd web && pnpm dev +``` + +Open `http://127.0.0.1:13083`. + +Verify: + +- 页面中文显示。 +- 标签页包括项目视图、工作流视图、智能体视图、草稿、设置。 +- 智能体视图能加载 agent 列表或清晰显示错误。 +- 没有静默写回按钮。 + +- [ ] **Step 4: Update final docs** + +Update `docs/project.md` with actual commands and final architecture. + +Update `task_plan.md` phase statuses to complete only after corresponding review passes. + +Update `progress.md` with test results: + +```markdown +| Time | Command | Result | Notes | +| --- | --- | --- | --- | +| 2026-05-25 | go test ./... | pass | 后端测试 | +| 2026-05-25 | cd web && pnpm build | pass | 前端构建 | +``` + +- [ ] **Step 5: Final commit** + +Run: + +```bash +git add README.md docs task_plan.md findings.md progress.md +git commit -m "docs: finalize runbook and verification evidence" +``` + +Expected: commit succeeds. + +--- + +## Review Gates + +Each implementation phase must end with a testing/code review agent report: + +```markdown +## Bugs Found + +- [blocking/non-blocking] [file:line] 问题标题 + - 复现方式: + - 预期结果: + - 实际结果: + - 影响范围: + - 建议验证方式: + +## Tests Run + +| Command | Result | Notes | +| --- | --- | --- | + +## Verdict + +[pass / fail] +``` + +If verdict is `fail`, the main agent sends only the bug report back to the coding agent. The fix loop is limited to 3 attempts per phase. + +## Self-Review + +- Spec coverage: plan covers project setup, Go backend, Vue frontend, read-only data, dynamic workflow, safe writeback, Chinese UI, docs, and review gates. +- Placeholder scan: no placeholder instructions remain; unresolved design choices are assigned to implementation tasks. +- Type consistency: `AgentDefinition`, `ProjectInfo`, `WorkflowEvent`, and `StatusEvidence` names match the design spec and task snippets. diff --git a/docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md b/docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md index e917519..ea4eca5 100644 --- a/docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md +++ b/docs/superpowers/specs/2026-05-25-codex-agent-manager-design.md @@ -1,7 +1,7 @@ # Codex 智能体管理台设计规格 日期:2026-05-25 -状态:已获口头批准,待书面规格确认 +状态:已批准,进入实施计划阶段 目标目录:`/Users/yoilun/Code/codex-agent-manager` ## 1. 产品定位