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

38 KiB
Raw Blame History

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、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:

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

.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/agents handler

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}/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:

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, and StatusEvidence names match the design spec and task snippets.