# 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.