package server import ( "errors" "net/http" "os" "path/filepath" "time" "chat-agent-system/internal/controlcenter/pipeline" "chat-agent-system/internal/controlcenter/tasks" ) // handleTaskExportDoc renders a task as a .doc short-term task document and // writes it to /.doc/ops/short-term/YYYY-MM-DD-.md. The // periodic docs sync then indexes it like any other .doc file. An existing // file is never overwritten (409). func (s *Server) handleTaskExportDoc(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") t, err := tasks.GetTask(r.Context(), s.store, id) if err != nil { writeDomainError(w, err) return } if !tasks.ValidDocSlug(t.Slug) { writeError(w, http.StatusBadRequest, "task slug is not exportable: must match [a-z0-9-]+") return } today := time.Now().Format("2006-01-02") rel := filepath.Join(".doc", "ops", "short-term", today+"-"+t.Slug+".md") full := filepath.Join(s.cfg.RepoRoot, rel) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { writeDomainError(w, err) return } f, err := os.OpenFile(full, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if errors.Is(err, os.ErrExist) { writeError(w, http.StatusConflict, "doc file already exists: "+filepath.ToSlash(rel)) return } if err != nil { writeDomainError(w, err) return } if _, err := f.WriteString(tasks.RenderTaskDoc(t, today)); err != nil { f.Close() writeDomainError(w, err) return } if err := f.Close(); err != nil { writeDomainError(w, err) return } relSlash := filepath.ToSlash(rel) t, err = tasks.PatchTask(r.Context(), s.store, id, tasks.Patch{DocPath: &relSlash}) if err != nil { writeDomainError(w, err) return } s.appendEvent(r.Context(), id, "", pipeline.PhaseSystem, "exported doc "+relSlash, nil) writeJSON(w, http.StatusCreated, map[string]any{"doc_path": relSlash, "task": t}) }