- 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
65 lines
1.8 KiB
Go
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})
|
|
}
|