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..b7b79653 --- /dev/null +++ b/.doc/ops/short-term/2026-09-22-g2-g3-exception-clearance.md @@ -0,0 +1,64 @@ +--- +title: "W3 G2/G3 例外清退" +task_id: "g2-g3-exception-clearance-2026-09-22" +status: done +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: "db632aeea28e99db58ecdf949345e2152e4782d1" +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 到期的 U5 path 例外,以及 `projectinstance→task_runs`(U8)sql_write。 +- 交付终点:代码迁出敏感字面量/写入;YAML 删除对应 exception;`arch-check --mode block` PASS;本 worktree 本地提交;不 push。 +- 禁止:push、未授权环境写、仅改 expires。 + +## 步骤与验收 + +1. [x] coordinator / bizdev / storage:去掉 `data/dev` 回退字面量 +2. [x] agent Targets / bizdev / coordinator / docgov / docrevision(reconcile) / server route:`workspace.DeliverableKey` / `WorkspacesDirName` +3. [x] G3:`projectinstance` terminate 经 `taskrun.CancelOpenByProject`;allowlist `projectinstance→taskrun` +4. [x] 删除全部 U5 path + U8 sql_write 例外;baseline `path 12→3`、`sql_write 5→4` + +## 验证与证据 + +| 范围 | 命令 | 实际 | +|---|---|---| +| arch-check block | `./scripts/arch-check.sh --mode block` | PASS(path=3 U4 残留;sql_write=4 U3) | +| 相关包 short | `go test -short` taskrun/projectinstance/coordinator/docrevision/archtest | PASS | +| server short 全量 | `go test -short ./internal/server/` | FAIL `TestFinalizeTaskRun_HashesViaWorkspaceCapability`(macOS `/var` 符号链接宿主债;与本批无关) | +| docgov DB 集成 | `TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK` | FAIL FK project_id(环境/种子债;本批未改 CreateOrGet) | + +## Remaining(非本批 blocker) + +- U4 path×3:`docrevision/projection.go`、`server/document_revision_access.go`、`document_revision_handlers.go`(到期 2026-09-28) +- U3 sql_write×4:docduty / docrevision document_records(G1 残留) + +## 收口 + +- `integration_state=pending`;不 push。 +- 本地 tip 见 `head_sha`(提交后回填)。 + +## 相关链接 + +- [批次 G 计划](../../remediation/2026-09-20-module-decoupling-batch-plan.md) +- [module-boundaries.yaml](../../system-design/module-boundaries.yaml) diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index f5b93d51..a24cab67 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: @@ -49,6 +50,7 @@ imports: coordinator: - agent - app/stageflow + - config - contracts/vocabulary - ctxmgr - intent @@ -58,6 +60,7 @@ imports: - squad - stream - workflowscope + - workspace ctxmgr: - app/uow - docgov @@ -149,10 +152,12 @@ imports: - docduty - fsutil - workflowdef + - workspace docrevision: - app/uow - doccontent - fsutil + - workspace doccontent: [] freedoc: - app/uow @@ -180,9 +185,11 @@ imports: projectcontext: - squad projectinstance: + - app/uow - docduty - docgov - projectworkflow + - taskrun - workflowdef projectworkflow: - auth @@ -241,7 +248,8 @@ imports: - template - workspace - workflowdef - storage: [] + storage: + - config stream: [] stream/transport: - stream @@ -478,128 +486,10 @@ unwrap_owners: - ctxmgr - app/intentflow -exceptions: - - kind: sql_write - package: projectinstance - file: internal/projectinstance/store.go - symbol: task_runs - owner: projectinstance - reason: workflow terminate cancels task_runs; U8 owns run finalization in the same UoW - milestone: U8 - expires: "2026-10-26" - - kind: path - package: agent - file: internal/agent/cursor.go - symbol: output.md - owner: agent - reason: runner still names the deliverable file; U5 replaces this with WorkspaceRef - milestone: U5 - expires: "2026-10-12" - - kind: path - package: agent - file: internal/agent/piagent.go - symbol: output.md - owner: agent - 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 - symbol: output.md - owner: bizdev - reason: orchestration still joins workspace output.md; U5 removes Root/OutputDir - milestone: U5 - expires: "2026-10-12" - - kind: path - package: coordinator - file: internal/coordinator/context.go - symbol: output.md - owner: coordinator - 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 - symbol: output.md - owner: coordinator - reason: engine joins deliverable path; U5 WorkspaceRef - milestone: U5 - expires: "2026-10-12" - - kind: path - package: docgov - file: internal/docgov/lease.go - symbol: output.md - owner: docgov - reason: SaveDocumentContent still writes projection files; U5 capability + U4 ledger-first read - milestone: U5 - expires: "2026-10-12" - - kind: path - package: docgov - file: internal/docgov/project_docs.go - symbol: output.md - owner: docgov - reason: project doc helpers still construct output.md; U5 - milestone: U5 - expires: "2026-10-12" - - kind: path - package: docrevision - file: internal/docrevision/reconcile.go - symbol: output.md - owner: docrevision - reason: reconcile still repairs projection files; U5 - milestone: U5 - expires: "2026-10-12" - - kind: path - package: server - file: internal/server/server.go - symbol: workspaces/ - owner: server - 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" +exceptions: [] baseline_counts: import: 0 - sql_write: 1 - path: 13 + sql_write: 0 + path: 0 event: 0 - -# U12: kinds already at zero production findings fail --mode block. -# sql_write/path keep dated exceptions until their owner milestones clear them. -block_kinds: - - import - - event 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 7b7b6f90..8fcfee33 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -80,9 +80,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 @@ -216,9 +214,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 { @@ -232,11 +230,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 { @@ -545,7 +543,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 == vocabulary.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", @@ -598,7 +596,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun if err != nil || doc == nil || doc.CurrentState != vocabulary.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 aece3cf5..c76ff0c1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -87,10 +87,16 @@ type Config struct { HTTPAccessLogEnabled bool `json:"http_access_log_enabled"` // HTTP_ACCESS_LOG; default on for dev/test } +// 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/context.go b/internal/coordinator/context.go index 4f2864bd..3bf4c892 100644 --- a/internal/coordinator/context.go +++ b/internal/coordinator/context.go @@ -17,6 +17,7 @@ import ( "chat-agent-system/internal/kb" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" + "chat-agent-system/internal/workspace" ) // UserInfo 用户在系统与项目内的身份 @@ -453,9 +454,9 @@ func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo) } } - // 5. 若无 advisor_brief.json 但有 output.md,尝试从文本抽取 + // 5. 若无 advisor_brief.json 但有交付正文,尝试从文本抽取 if ctx.LatestAdvisorBrief == nil { - outPath := filepath.Join(workDir, "output.md") + outPath := filepath.Join(workDir, workspace.DeliverableKey) if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 { ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts) ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow) 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/coordinator/engine.go b/internal/coordinator/engine.go index 0c6661f4..3b5b23fb 100644 --- a/internal/coordinator/engine.go +++ b/internal/coordinator/engine.go @@ -23,6 +23,7 @@ import ( "chat-agent-system/internal/squad" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowscope" + "chat-agent-system/internal/workspace" ) // CoordinationStatus 协调裁决状态 @@ -892,7 +893,7 @@ func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent artifactPath := "" if stageRes.PrimaryArtifact != nil { - artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, "output.md") + artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, workspace.DeliverableKey) } nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name) @@ -1048,7 +1049,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent. } userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief - outPath := filepath.Join(taskRes.WorkDir, "output.md") + outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey) reply, taskStatus := formatTriggerTaskReply(taskRes, outPath) @@ -1181,7 +1182,7 @@ func (e *CoordinatorEngine) handleTriggerAdhocTask(ctx context.Context, cand *in if taskRes.WorkDir != "" { userCtx.WorkDir = taskRes.WorkDir } - outPath := filepath.Join(taskRes.WorkDir, "output.md") + outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey) var sb strings.Builder taskStatus := taskRes.TerminalStatus() diff --git a/internal/docgov/lease.go b/internal/docgov/lease.go index 96942d63..395a951d 100644 --- a/internal/docgov/lease.go +++ b/internal/docgov/lease.go @@ -14,6 +14,7 @@ import ( "chat-agent-system/internal/doccontent" "chat-agent-system/internal/fsutil" + "chat-agent-system/internal/workspace" ) // LeaseAcquireRequest 申请编辑租约 @@ -412,7 +413,7 @@ func (e *GovernanceEngine) SaveDocumentContent(ctx context.Context, req ContentS workDir := doc.WorkDir if workDir != "" { - outPath := filepath.Join(workDir, "output.md") + outPath := filepath.Join(workDir, workspace.DeliverableKey) if werr := fsutil.WriteFileAtomic(outPath, []byte(canon), 0o644); werr != nil { return nil, fmt.Errorf("docgov: write content failed: %w", werr) } diff --git a/internal/docgov/project_docs.go b/internal/docgov/project_docs.go index c71e1cc7..82773453 100644 --- a/internal/docgov/project_docs.go +++ b/internal/docgov/project_docs.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "time" + "chat-agent-system/internal/workspace" ) // WorkflowDocumentSummary 项目工作流受控文档列表项。 @@ -135,7 +136,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p return collapseWorkflowDocuments(out), nil } -// ReadDocumentContent 读取受控文档 output.md 并返回 typed ContentError。 +// ReadDocumentContent 读取受控文档交付正文 并返回 typed ContentError。 // 记录不存在才是 DOCUMENT_NOT_FOUND;缺文件/空文件/哈希冲突不得伪装成 404。 func (e *GovernanceEngine) ReadDocumentContent(ctx context.Context, docID string, repoRoot string) (*DocumentContent, error) { doc, err := e.GetDocument(ctx, docID) @@ -153,7 +154,7 @@ func (e *GovernanceEngine) ReadDocumentContent(ctx context.Context, docID string if !filepath.IsAbs(workDir) && repoRoot != "" { workDir = filepath.Join(repoRoot, workDir) } - path := filepath.Join(workDir, "output.md") + path := filepath.Join(workDir, workspace.DeliverableKey) data, err := os.ReadFile(path) if err != nil { ce := NewContentError(ErrCodeProjectionMissing, fmt.Errorf("%w: %v", ErrContentProjectionMissing, err), true, true) diff --git a/internal/docrevision/reconcile.go b/internal/docrevision/reconcile.go index c51a9906..cedcfb20 100644 --- a/internal/docrevision/reconcile.go +++ b/internal/docrevision/reconcile.go @@ -9,6 +9,7 @@ import ( "time" "chat-agent-system/internal/fsutil" + "chat-agent-system/internal/workspace" ) // ContentHashUpdater updates document_records.content_sha256 after a repair. @@ -62,7 +63,7 @@ func (s *Service) ReconcileDocument(ctx context.Context, req ReconcileRequest) ( if path == "" { return &ReconcileResult{DocID: req.DocID, Class: ProjectionMissing, DryRun: !req.Apply, Detail: "empty work_dir"}, nil } - raw, err := os.ReadFile(filepath.Join(path, "output.md")) + raw, err := os.ReadFile(filepath.Join(path, workspace.DeliverableKey)) if err != nil { if os.IsNotExist(err) { return &ReconcileResult{DocID: req.DocID, Class: ProjectionMissing, DryRun: !req.Apply, Detail: err.Error(), OldHeadSHA: headSHA, OldHeadID: oldHeadID}, nil @@ -114,7 +115,7 @@ func (s *Service) ReconcileDocument(ctx context.Context, req ReconcileRequest) ( return nil, err } // Verify file still original bytes — never project old or new head over it. - after, _ := os.ReadFile(filepath.Join(path, "output.md")) + after, _ := os.ReadFile(filepath.Join(path, workspace.DeliverableKey)) if string(after) != string(raw) { return nil, fmt.Errorf("docrevision: apply mutated original file; abort") } @@ -218,7 +219,7 @@ func BackupReconcileArtifacts(backupDir, docID string, raw []byte, headID int64, } arts := &BackupArtifacts{ Dir: runDir, - OutputFile: filepath.Join(runDir, "output.md"), + OutputFile: filepath.Join(runDir, workspace.DeliverableKey), HeadFile: filepath.Join(runDir, "head.md"), MetaFile: filepath.Join(runDir, "head.json"), } @@ -233,7 +234,7 @@ func BackupReconcileArtifacts(backupDir, docID string, raw []byte, headID int64, "head_snapshot_id": headID, "head_sha256": headSHA, "head_markdown": "head.md", - "original_file": "output.md", + "original_file": workspace.DeliverableKey, "backed_up_at": time.Now().UTC().Format(time.RFC3339), "note": "do not restore this head over the original file", "head_bytes": len(headMD), diff --git a/internal/projectinstance/store.go b/internal/projectinstance/store.go index 46daa812..c6aa20e3 100644 --- a/internal/projectinstance/store.go +++ b/internal/projectinstance/store.go @@ -13,6 +13,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "chat-agent-system/internal/taskrun" "chat-agent-system/internal/workflowdef" ) @@ -916,10 +917,9 @@ func (s *pgStore) TerminateInstance(ctx context.Context, instanceID int64, proje return nil, err } - _, _ = tx.Exec(ctx, ` - UPDATE task_runs SET status = 'cancelled', updated_at = $2, error_message = 'workflow terminated' - WHERE project_id = $1 AND status IN ('pending', 'running') - `, projectID, now) + if err := taskrun.CancelOpenByProject(ctx, tx, projectID, now, "workflow terminated"); err != nil { + return nil, err + } if err := tx.Commit(ctx); err != nil { return nil, err diff --git a/internal/server/server.go b/internal/server/server.go index 0366611c..282fca8c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -240,7 +240,7 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/events", s.handleTaskRunEvents) mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/cancel", s.handleCancelTaskRun) mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/resume", s.handleResumeTaskRun) - mux.HandleFunc("POST /api/v1/admin/workspaces/reconcile", s.handleReconcileWorkspace) + mux.HandleFunc("POST /api/v1/admin/"+workspace.WorkspacesDirName+"/reconcile", s.handleReconcileWorkspace) mux.HandleFunc("POST /api/v1/projects/{id}/assets", s.handleUploadProjectAsset) mux.HandleFunc("GET /api/v1/projects/{id}/assets", s.handleListProjectAssets) mux.HandleFunc("GET /api/v1/projects/{id}/assets/{asset_id}/raw", s.handleGetProjectAssetRaw) 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/taskrun/store.go b/internal/taskrun/store.go index c6827259..93613d8e 100644 --- a/internal/taskrun/store.go +++ b/internal/taskrun/store.go @@ -302,6 +302,23 @@ func (s *PGStore) ListEvents(ctx context.Context, runID string, afterSeq int64) return out, rows.Err() } + +// CancelOpenByProject cancels pending/running runs for a project inside an existing transaction. +// Table ownership stays with taskrun; workflow terminate must call this instead of writing task_runs directly. +func CancelOpenByProject(ctx context.Context, tx pgx.Tx, projectID int64, at time.Time, errMsg string) error { + if tx == nil { + return fmt.Errorf("taskrun: CancelOpenByProject requires tx") + } + if strings.TrimSpace(errMsg) == "" { + errMsg = "workflow terminated" + } + _, err := tx.Exec(ctx, ` + UPDATE task_runs SET status = 'cancelled', updated_at = $2, error_message = $3 + WHERE project_id = $1 AND status IN ('pending', 'running') + `, projectID, at, errMsg) + return err +} + func (s *PGStore) AcquireLease(ctx context.Context, runID, workerID string, ttl time.Duration) error { if !s.Available() { return ErrStoreNotAvailable 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"