Files
codex-agent-manager/docs/superpowers/plans/2026-05-25-codex-agent-manager-implementation.md
2026-05-25 15:39:51 +08:00

1702 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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、goalsSQLite 只读 |
| 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 APIVue 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
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codex 智能体管理台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
```
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
<script setup>
import { ref } from 'vue'
import ProjectView from './views/ProjectView.vue'
import WorkflowView from './views/WorkflowView.vue'
import AgentView from './views/AgentView.vue'
import DraftsView from './views/DraftsView.vue'
import SettingsView from './views/SettingsView.vue'
const tabs = [
{ key: 'projects', label: '项目视图', component: ProjectView },
{ key: 'workflow', label: '工作流视图', component: WorkflowView },
{ key: 'agents', label: '智能体视图', component: AgentView },
{ key: 'drafts', label: '草稿', component: DraftsView },
{ key: 'settings', label: '设置', component: SettingsView }
]
const active = ref('projects')
</script>
<template>
<main class="shell">
<header class="topbar">
<div>
<h1>智能体工作台</h1>
<p>项目活动角色编辑工作流交接审查循环</p>
</div>
<input class="search" placeholder="搜索项目、智能体、阶段、PID..." />
</header>
<nav class="tabs" aria-label="主导航">
<button
v-for="tab in tabs"
:key="tab.key"
:class="['tab', { active: active === tab.key }]"
@click="active = tab.key"
>
{{ tab.label }}
</button>
</nav>
<component :is="tabs.find(tab => tab.key === active).component" />
</main>
</template>
```
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
<template>
<section class="panel">
<h2>项目视图</h2>
<p>按项目查看智能体执行情况状态来源和置信度</p>
</section>
</template>
```
Create `web/src/views/WorkflowView.vue`:
```vue
<template>
<section class="panel">
<h2>工作流视图</h2>
<p>查看阶段进度智能体交接审查循环和主智能体监管</p>
</section>
</template>
```
Create `web/src/views/AgentView.vue`:
```vue
<template>
<section class="panel">
<h2>智能体视图</h2>
<p>查看和编辑智能体名称描述角色设定</p>
</section>
</template>
```
Create `web/src/views/DraftsView.vue`:
```vue
<template>
<section class="panel">
<h2>草稿</h2>
<p>查看 TOML 校验差异备份和待写回变更</p>
</section>
</template>
```
Create `web/src/views/SettingsView.vue`:
```vue
<template>
<section class="panel">
<h2>设置</h2>
<p>配置 Codex 路径数据源和备份策略</p>
</section>
</template>
```
- [ ] **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
<script setup>
defineProps({
label: { type: String, required: true },
confidence: { type: String, default: 'low' },
source: { type: String, default: 'unknown' }
})
</script>
<template>
<span class="status-badge" :data-confidence="confidence">
{{ label }} · 置信度{{ confidence }} · 来源{{ source }}
</span>
</template>
```
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
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api/client'
const agents = ref([])
const error = ref('')
onMounted(async () => {
try {
const data = await api.agents()
agents.value = data.items || []
} catch (err) {
error.value = err.message
}
})
</script>
<template>
<section class="panel">
<h2>智能体视图</h2>
<p>查看和编辑智能体名称描述角色设定</p>
<p v-if="error" class="error">{{ error }}</p>
<div class="list">
<article v-for="agent in agents" :key="agent.id" class="row-card">
<strong>{{ agent.name || agent.fileName }}</strong>
<span>{{ agent.description || '无描述' }}</span>
<small>{{ agent.parseStatus === 'valid' ? 'TOML 有效' : 'TOML 无效' }}</small>
</article>
</div>
</section>
</template>
```
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.