Files
Agent-System/internal/controlcenter/server/export_doc_handler.go
T
weichen f79956c498 feat(control-center): P4 .doc importer, agent CLI, task doc export
- internal/controlcenter/importer: scan .doc/ops/{short-term,long-term}
  and .doc/backlog for task docs, upsert cc_tasks keyed by slug; maps doc
  status/stage, auto-creates the three plans, follows doc status only
  while a task is planned/in_progress (terminal statuses never regress),
  skips unchanged docs, and resolves cross-directory slug collisions by
  fixed directory precedence with a warning
- cmd/control-center: import-docs subcommand (also runs at serve startup
  and on the doc-sync ticker) plus the agent CLI: next / instruction /
  claim / heartbeat / release / event / artifact / advance-request /
  decision / report, thin net/http client, compact JSON output
- console API POST /api/v1/tasks/{id}/export-doc renders a task per
  .doc/ops/templates/short-term-task.md into .doc/ops/short-term/, refuses
  overwrite (409) and unsafe slugs, then backfills doc_path
- fix pre-existing TestAgentHasNoAdvanceRoute: SPA catch-all GET / makes
  the mux answer 405 for unmatched POST paths; accept 404 or 405
- archtest allowlist: controlcenter/importer -> pipeline, store
2026-09-17 11:57:47 +00:00

65 lines
1.8 KiB
Go

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 <RepoRoot>/.doc/ops/short-term/YYYY-MM-DD-<slug>.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})
}