feat(brain): ListPending + PromoteNote — raw→wiki curation primitives (#38)

Closes the human curation loop: list brain/raw/ notes awaiting review
(oldest-first, with excerpt) and promote one into brain/wiki/<wing>/<hall>/.

PromoteNote rewrites frontmatter (sets wing/hall/promoted_at, preserves
created_at + custom fields via the existing frontmatter editor), rebuilds
the wing _index, and runs auto-tunnel. Atomic from the caller's view:
hall/wing/slug/collision validation happens before any fs change, and the
source is deleted only after the destination write succeeds (write-then-
delete, never move) — a collision or write failure leaves raw/ intact.
Default slug = filename minus the YYYY-MM-DD- prefix.

Also exposes GET /pending + POST /promote for shell scripts (bad
hall/collision/missing-source → 400, not 500).

Scope note: operates on raw/ (the retrospective-skill review queue) per
this issue's spec. The knowledge/ legacy pile is #22's bulk-migration
concern, not this ongoing queue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:34:15 +02:00
co-authored by Claude Opus 4.8
parent 66a9b8e725
commit 14b04a25cb
3 changed files with 311 additions and 0 deletions
+34
View File
@@ -483,6 +483,40 @@ func (h *Handler) BackfillRefs(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]int{"updated": n})
}
// Pending handles GET /pending — list raw/ notes awaiting promotion.
func (h *Handler) Pending(w http.ResponseWriter, _ *http.Request) {
pending, err := ListPending(h.brainDir)
if err != nil {
h.logger.Error("pending failed", "err", err)
writeError(w, http.StatusInternalServerError, "pending error")
return
}
writeJSON(w, map[string]any{"pending": pending})
}
type promoteRequest struct {
Filename string `json:"filename"`
Wing string `json:"wing"`
Hall string `json:"hall"`
Slug string `json:"slug,omitempty"`
}
// Promote handles POST /promote — move a raw/ note into the wiki. A bad
// hall / collision / missing source is a 400 (caller error), not a 500.
func (h *Handler) Promote(w http.ResponseWriter, r *http.Request) {
var req promoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
rel, err := PromoteNote(h.brainDir, PromoteOptions(req))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, map[string]string{"path": rel})
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v) //nolint:errcheck