38 KiB
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:
# 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:
# 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:
# 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:
# 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
.gitignorefor generated artifacts
Ensure .gitignore includes:
.DS_Store
.superpowers/
dist/
node_modules/
*.log
tmp/
*.bak.*
- Step 3: Commit planning baseline
Run:
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:
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:
go test ./internal/codexhome
Expected: FAIL because package files are not implemented yet.
- Step 3: Implement config and path boundary
Create go.mod:
module codex-agent-manager
go 1.22
Create internal/app/config.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:
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:
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:
go test ./...
Expected: PASS.
- Step 5: Update docs and commit
Update docs/project.md runbook with:
## Runbook
启动后端:
```bash
go run ./cmd/codex-agent-manager
健康检查:
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:
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:
go test ./internal/agents
Expected: FAIL because Store is undefined.
- Step 3: Implement agent model and store
Create internal/agents/model.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:
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:
go test ./internal/agents
go test ./...
Expected: PASS.
- Step 5: Add
/api/agentshandler
Create internal/server/server.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):
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:
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:
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:
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:
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:
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:
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:
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:
{
"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:
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:
<!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:
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:
<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:
: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:
<template>
<section class="panel">
<h2>项目视图</h2>
<p>按项目查看智能体执行情况、状态来源和置信度。</p>
</section>
</template>
Create web/src/views/WorkflowView.vue:
<template>
<section class="panel">
<h2>工作流视图</h2>
<p>查看阶段进度、智能体交接、审查循环和主智能体监管。</p>
</section>
</template>
Create web/src/views/AgentView.vue:
<template>
<section class="panel">
<h2>智能体视图</h2>
<p>查看和编辑智能体名称、描述、角色设定。</p>
</section>
</template>
Create web/src/views/DraftsView.vue:
<template>
<section class="panel">
<h2>草稿</h2>
<p>查看 TOML 校验、差异、备份和待写回变更。</p>
</section>
</template>
Create web/src/views/SettingsView.vue:
<template>
<section class="panel">
<h2>设置</h2>
<p>配置 Codex 路径、数据源和备份策略。</p>
</section>
</template>
- Step 4: Build frontend
Run:
cd web && pnpm install && pnpm build
Expected: build succeeds. If network blocks dependency install, request escalation or document the blocker.
- Step 5: Commit
Run:
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:
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:
<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:
.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:
<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:
.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:
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:
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:
type WriteResult struct {
TargetPath string `json:"targetPath"`
BackupPath string `json:"backupPath"`
Status string `json:"status"`
}
Add to internal/agents/store.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:
go test ./internal/agents
go test ./...
Expected: PASS.
- Step 4: Add server endpoints and UI flow
Implement minimal endpoints:
POST /api/agents/{id}/validatePOST /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:
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:
# Codex 智能体管理台
本项目是本机 localhost 工具,用中文管理 Codex 智能体配置、项目运行状态和动态工作流交接。
## 启动后端
```bash
go run ./cmd/codex-agent-manager
启动前端
cd web
pnpm install
pnpm dev
验证
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:
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:
| Time | Command | Result | Notes |
| --- | --- | --- | --- |
| 2026-05-25 | go test ./... | pass | 后端测试 |
| 2026-05-25 | cd web && pnpm build | pass | 前端构建 |
- Step 5: Final commit
Run:
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:
## 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, andStatusEvidencenames match the design spec and task snippets.