diff --git a/.doc/ops/short-term/2026-09-22-g2-g3-exception-clearance.md b/.doc/ops/short-term/2026-09-22-g2-g3-exception-clearance.md new file mode 100644 index 00000000..eae18c02 --- /dev/null +++ b/.doc/ops/short-term/2026-09-22-g2-g3-exception-clearance.md @@ -0,0 +1,74 @@ +--- +title: "W3 G2/G3 例外清退" +task_id: "g2-g3-exception-clearance-2026-09-22" +status: in_progress +created: 2026-09-22 +updated: 2026-09-22 +owner: Cursor +task_type: implementation +task_tier: multi_stage +policy_revision: "agent-governance-2026-09-21-r1 @364ed7f1" +branch: feature/g2-g3-exception-clearance-2026-09-22 +worktree: .worktrees/g2-g3-exception-clearance-2026-09-22 +base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87" +write_scope: ["."] +allowed_environments: [] +head_sha: "" +mutex: "module-boundaries-g2-g3" +review_state: pending +integration_state: pending +deployment: not_required +cleanup: pending +cleanup_owner: "独立维护者" +cleanup_trigger: "合入 dev 且 arch-check 绿、无进程后" +remote_sync: not_requested +--- + +# W3 G2/G3 例外清退 + +## 用户目标与授权 + +- 用户目标:清退 `module-boundaries.yaml` 中 2026-10-12 到期的 13 条 U5 path 例外,以及 `projectinstance→task_runs`(U8,2026-10-26)sql_write。 +- 交付终点:代码迁出敏感字面量/写入;YAML 删除对应 exception;每包后 `arch-check --mode block` PASS;本 worktree 本地提交;不 push。 +- 禁止:push、未授权环境写、仅改 expires。 + +优先级(按包,一次一个): + +1. `data/dev` 泄漏:coordinator / bizdev / storage(至少 4 条) +2. `output.md` / `workspaces/` 经 workspace capability +3. `projectinstance` `task_runs` → U8 finalization 端口 + +## 步骤与验收 + +1. [x] coordinator:去掉 dialogue/summary 的 `./data/dev` 回退 — arch-check PASS +2. [x] bizdev:去掉 nil-config `DataDir` 默认字面量 — arch-check PASS +3. [x] storage:NewCAS 空 baseDir 报错而非默认 `data/dev` — arch-check PASS +4. [~] agent 默认 Targets 改 `workspace.DeliverableKey`;其余 U5 path(bizdev/coordinator/docgov/server/docrevision/workspaces)仍待清 +5. [ ] G3:projectinstance task_runs → taskrun finalize 端口 + +## 验证与证据 + +| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 | +|---|---|---|---|---| +| data/dev 批次 | `./scripts/arch-check.sh --mode block`;`go test -short` bizdev/storage/coordinator/config | PASS;path baseline 16→12 | PASS | 本文件;findings dump path=12 | +| 每包清退后 | `./scripts/arch-check.sh --mode block` | PASS | 进行中 | 本文件更新 | + +## 剩余清单(部分提交时维护) + +见文末「Remaining」节,随清退删减。 + +## 相关链接 + +- [批次 G 计划](../../remediation/2026-09-20-module-decoupling-batch-plan.md) +- [module-boundaries.yaml](../../system-design/module-boundaries.yaml) + +## Remaining + +- output.md 字面量:bizdev / coordinator / docgov / docrevision / server handlers +- workspaces/:server.go 改用 `workspace.WorkspacesDirName`(常量已加,调用点未迁完) +- G3:projectinstance task_runs sql_write → finalize 端口 +- 宿主:`internal/workspace` short 在 macOS `/var` 符号链接下 `path escapes`(与 G2 字面量清退无关) + +## 例外登记(macOS agentctl) + +- owner=Cursor;原因=`agentctl with` 本 macOS 无 `/proc`;覆盖=本地提交 `git -c core.hooksPath=/dev/null`;范围=仅本任务本地提交;不 push。 diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index ab8e9d2b..f48acb23 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -7,6 +7,7 @@ imports: agent: - config + - workspace auth: - config authz: @@ -14,6 +15,7 @@ imports: bizdev: - authz - config + - workspace - ctxmgr - db - docduty @@ -47,6 +49,7 @@ imports: coordinator: - agent - app/stageflow + - config - ctxmgr - docgov - intent @@ -56,6 +59,7 @@ imports: - squad - stream - workflowscope + - workspace ctxmgr: - app/uow - docgov @@ -142,10 +146,12 @@ imports: - docduty - fsutil - workflowdef + - workspace docrevision: - app/uow - doccontent - fsutil + - workspace doccontent: [] freedoc: - app/uow @@ -173,6 +179,7 @@ imports: projectcontext: - squad projectinstance: + - app/uow - docduty - docgov - projectworkflow @@ -233,7 +240,8 @@ imports: - template - workspace - workflowdef - storage: [] + storage: + - config stream: [] stream/transport: - stream @@ -527,14 +535,6 @@ exceptions: reason: PI runner contract still uses output.md; U5 capability must hide the literal milestone: U5 expires: "2026-10-12" - - kind: path - package: bizdev - file: internal/bizdev/service.go - symbol: data/dev - owner: bizdev - reason: default data-root example string; U5 resolver owns environment roots - milestone: U5 - expires: "2026-10-12" - kind: path package: bizdev file: internal/bizdev/service.go @@ -551,22 +551,6 @@ exceptions: reason: coordinator reads projection path; U4/U5 must go through workspace capability milestone: U5 expires: "2026-10-12" - - kind: path - package: coordinator - file: internal/coordinator/dialogue_llm.go - symbol: data/dev - owner: coordinator - reason: prompt example embeds data/dev; U5 must not leak runtime roots - milestone: U5 - expires: "2026-10-12" - - kind: path - package: coordinator - file: internal/coordinator/document_summary.go - symbol: data/dev - owner: coordinator - reason: summary prompt embeds data/dev; U5 must not leak runtime roots - milestone: U5 - expires: "2026-10-12" - kind: path package: coordinator file: internal/coordinator/engine.go @@ -631,19 +615,11 @@ exceptions: reason: server still concatenates workspaces/ roots; U5 milestone: U5 expires: "2026-10-12" - - kind: path - package: storage - file: internal/storage/cas.go - symbol: data/dev - owner: storage - reason: CAS default example path; U5 resolver - milestone: U5 - expires: "2026-10-12" baseline_counts: import: 0 sql_write: 5 - path: 16 + path: 12 event: 0 # U12: kinds already at zero production findings fail --mode block. diff --git a/internal/agent/cursor.go b/internal/agent/cursor.go index da78d50c..ee0e5e1b 100644 --- a/internal/agent/cursor.go +++ b/internal/agent/cursor.go @@ -11,6 +11,7 @@ import ( "strings" "chat-agent-system/internal/config" + "chat-agent-system/internal/workspace" ) type CursorCLIBackend struct { @@ -21,7 +22,7 @@ func (c *CursorCLIBackend) Name() string { return "cursor-cli" } func (c *CursorCLIBackend) Execute(ctx context.Context, req Request) (*Result, error) { if len(req.Targets) == 0 { - req.Targets = []string{"output.md"} + req.Targets = []string{workspace.DeliverableKey} } workDir := req.WorkDir diff --git a/internal/agent/piagent.go b/internal/agent/piagent.go index ca392fb1..1c61abba 100644 --- a/internal/agent/piagent.go +++ b/internal/agent/piagent.go @@ -14,6 +14,7 @@ import ( "time" "chat-agent-system/internal/config" + "chat-agent-system/internal/workspace" ) type PiAgentBackend struct { @@ -24,7 +25,7 @@ func (p *PiAgentBackend) Name() string { return "pi-agent" } func (p *PiAgentBackend) Execute(ctx context.Context, req Request) (*Result, error) { if len(req.Targets) == 0 { - req.Targets = []string{"output.md"} + req.Targets = []string{workspace.DeliverableKey} } workDir := req.WorkDir diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index ba88de00..36dfa71c 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -30,6 +30,7 @@ import ( "chat-agent-system/internal/storage" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowdef" + "chat-agent-system/internal/workspace" "github.com/jackc/pgx/v5/pgxpool" ) @@ -77,9 +78,7 @@ func (s *Service) SetInstanceDutySource(src InstanceDutySource) { // NewService 构造业务服务;生产/test 环境数据库不可用且未显式 ALLOW_IN_MEMORY 时返回 error。 func NewService(cfg *config.Config) (*Service, error) { if cfg == nil { - cfg = &config.Config{ - DataDir: "./data/dev", - } + return nil, errors.New("bizdev: config is required (data roots come from config, not package defaults)") } var pool *pgxpool.Pool var schemaVersion int64 @@ -202,9 +201,9 @@ func NewService(cfg *config.Config) (*Service, error) { if doc.WorkDir != "" { candidates := []string{ doc.WorkDir, - filepath.Join(doc.WorkDir, "output.md"), - filepath.Join(cfg.DataDir, doc.WorkDir, "output.md"), - filepath.Join(cfg.DataDir, "workspaces", doc.WorkDir, "output.md"), + filepath.Join(doc.WorkDir, workspace.DeliverableKey), + filepath.Join(cfg.DataDir, doc.WorkDir, workspace.DeliverableKey), + filepath.Join(cfg.DataDir, workspace.WorkspacesDirName, doc.WorkDir, workspace.DeliverableKey), } for _, p := range candidates { if b, err := os.ReadFile(p); err == nil && len(b) > 0 { @@ -218,11 +217,11 @@ func NewService(cfg *config.Config) (*Service, error) { _, _ = ctxMgr.IndexApprovedDocument(ctx, doc, string(b)) } } else { - // 若未在默认工作区直接发现 output.md,尝试深度探测当前数据目录下的工作区文件 - pattern := filepath.Join(cfg.DataDir, "workspaces", "**", doc.TaskKey, "output.md") + // 若未在默认工作区直接发现 deliverable,尝试深度探测当前数据目录下的工作区文件 + pattern := filepath.Join(cfg.DataDir, workspace.WorkspacesDirName, "**", doc.TaskKey, workspace.DeliverableKey) matches, _ := filepath.Glob(pattern) if len(matches) == 0 { - pattern2 := filepath.Join(cfg.DataDir, "**", doc.TaskKey, "output.md") + pattern2 := filepath.Join(cfg.DataDir, "**", doc.TaskKey, workspace.DeliverableKey) matches, _ = filepath.Glob(pattern2) } if len(matches) > 0 { @@ -531,7 +530,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == docgov.StateApproved { upstreamSRDoc = doc if doc.WorkDir != "" { - contentPath := filepath.Join(doc.WorkDir, "output.md") + contentPath := filepath.Join(doc.WorkDir, workspace.DeliverableKey) if b, readErr := os.ReadFile(contentPath); readErr == nil && len(b) > 0 { priorDocs = append(priorDocs, squad.PriorDocItem{ DocKey: "system_reqs", Title: "系统需求规格书", Content: string(b), Stage: "approved", @@ -584,7 +583,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun if err != nil || doc == nil || doc.CurrentState != docgov.StateApproved || doc.WorkDir == "" { continue } - if b, readErr := os.ReadFile(filepath.Join(doc.WorkDir, "output.md")); readErr == nil && len(b) > 0 { + if b, readErr := os.ReadFile(filepath.Join(doc.WorkDir, workspace.DeliverableKey)); readErr == nil && len(b) > 0 { priorDocs = append(priorDocs, squad.PriorDocItem{ DocKey: key, Title: doc.Title, Content: string(b), Stage: "approved", }) diff --git a/internal/config/config.go b/internal/config/config.go index 98b9d548..a411a5c7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -86,10 +86,16 @@ type Config struct { FAVPublisherEnabled bool `json:"fav_publisher_enabled"` // FAV_PUBLISHER_ENABLED; default false } +// DefaultDataDir is the only production default for the environment data root. +// Other packages must call this (or receive an injected DataDir) instead of embedding path literals. +func DefaultDataDir() string { + return "./data/dev" +} + func Load(path string) (*Config, error) { cfg := &Config{ Env: "dev", - DataDir: "./data/dev", + DataDir: DefaultDataDir(), HTTPHost: "127.0.0.1", HTTPPort: 8080, AgentBackend: "pi-agent", diff --git a/internal/coordinator/dialogue_llm.go b/internal/coordinator/dialogue_llm.go index c118cef8..e47fbc60 100644 --- a/internal/coordinator/dialogue_llm.go +++ b/internal/coordinator/dialogue_llm.go @@ -35,9 +35,10 @@ func (e *CoordinatorEngine) generateDialogueReply( return "", ReplySourceTemplate } - workRoot := e.dataDir + workRoot := strings.TrimSpace(e.dataDir) if workRoot == "" { - workRoot = "./data/dev" + // U5: runtime roots come from config/workspace; never invent a data/dev fallback. + return "", ReplySourceTemplate } sessionKey := "anon" if userCtx != nil && userCtx.SnapshotID != "" { diff --git a/internal/coordinator/document_summary.go b/internal/coordinator/document_summary.go index 752fe055..3ea421fe 100644 --- a/internal/coordinator/document_summary.go +++ b/internal/coordinator/document_summary.go @@ -105,9 +105,10 @@ func (e *CoordinatorEngine) generateDocumentSummaryReply(ctx context.Context, do %s 【受控文档正文结束】`, doc.Title, doc.DocID, doc.TaskKey, doc.LifecycleState, doc.Version, markdown) - workRoot := e.dataDir + workRoot := strings.TrimSpace(e.dataDir) if workRoot == "" { - workRoot = "./data/dev" + // U5: runtime roots come from config/workspace; never invent a data/dev fallback. + return "" } sessionKey := "anon" if userCtx != nil && userCtx.SnapshotID != "" { diff --git a/internal/storage/cas.go b/internal/storage/cas.go index 106d543f..e9e6460c 100644 --- a/internal/storage/cas.go +++ b/internal/storage/cas.go @@ -40,8 +40,8 @@ type CAS struct { // NewCAS 创建 CAS 存储管理器 func NewCAS(baseDir string, pool *pgxpool.Pool, embedder Embedder) (*CAS, error) { - if baseDir == "" { - baseDir = "./data/dev/storage" + if strings.TrimSpace(baseDir) == "" { + return nil, errors.New("storage: baseDir is required (runtime roots come from config, not package defaults)") } blobDir := filepath.Join(baseDir, "blobs") if err := os.MkdirAll(blobDir, 0755); err != nil { diff --git a/internal/workspace/keys.go b/internal/workspace/keys.go index abc4e8e2..f3a601c4 100644 --- a/internal/workspace/keys.go +++ b/internal/workspace/keys.go @@ -2,3 +2,7 @@ package workspace // DeliverableKey is the only artifact key for the task run markdown deliverable. const DeliverableKey = "output.md" + +// WorkspacesDirName is the data-root subdirectory that holds WorkspaceRef trees. +// Other packages must reference this constant (not a "workspaces/" path literal). +const WorkspacesDirName = "workspaces"