feat(api): brain_pending lists notes parked in raw/needs-review/ with the wing proposal (#205 S4)

Approval is moving the note into raw/<wing>/ (the watcher ingests it there);
a dedicated approve tool is tracked separately. Proposal reason is written
Go-quoted so LLM text cannot start a frontmatter key; pinned by a
mutation-checked test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 06:10:39 +02:00
co-authored by Claude Sonnet 5.5
parent 20d874a23d
commit b9657973ff
3 changed files with 118 additions and 4 deletions
+51 -4
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -20,6 +21,15 @@ type PendingNote struct {
CreatedAt string `json:"created_at"`
SizeBytes int64 `json:"size_bytes"`
Excerpt string `json:"excerpt"`
// Set only for a note parked in raw/needs-review/ by the wing resolver
// (hyperguild#205): it was not placed automatically, and the proposal says
// what the resolver would have done and why it did not. Approve by moving
// the note into raw/<wing>/ and the watcher ingests it there.
Queue string `json:"queue,omitempty"`
ProposedWing string `json:"proposed_wing,omitempty"`
ProposalReason string `json:"proposal_reason,omitempty"`
ProposalConfidence float64 `json:"proposal_confidence,omitempty"`
}
// datePrefix matches a leading YYYY-MM-DD- on a raw filename, stripped when
@@ -38,11 +48,32 @@ func ListPending(brainDir string) ([]PendingNote, error) {
return nil, fmt.Errorf("read raw dir: %w", err)
}
defer func() { _ = root.Close() }()
out, err := listNotes(root, "")
if err != nil {
return nil, err
}
// raw/needs-review/ is read through its own root, so a symlinked directory
// or note there is refused exactly like at the top level (#153).
if sub, serr := root.OpenRoot("needs-review"); serr == nil {
defer func() { _ = sub.Close() }()
parked, perr := listNotes(sub, "needs-review")
if perr != nil {
return nil, perr
}
out = append(out, parked...)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt < out[j].CreatedAt })
return out, nil
}
// listNotes lists the regular .md notes directly under root. queue labels the
// notes ("" for the top of raw/) and enables reading a wing proposal.
func listNotes(root *os.Root, queue string) ([]PendingNote, error) {
entries, err := fs.ReadDir(root.FS(), ".")
if err != nil {
return nil, fmt.Errorf("read raw dir: %w", err)
}
out := make([]PendingNote, 0, len(entries))
for _, e := range entries {
if !e.Type().IsRegular() || !strings.HasSuffix(e.Name(), ".md") || strings.HasPrefix(e.Name(), "tunnel-candidates-") {
@@ -57,14 +88,20 @@ func ListPending(brainDir string) ([]PendingNote, error) {
if created == "" {
created = info.ModTime().UTC().Format(time.RFC3339)
}
out = append(out, PendingNote{
n := PendingNote{
Filename: e.Name(),
CreatedAt: created,
SizeBytes: info.Size(),
Excerpt: excerpt(body, 200),
})
Queue: queue,
}
if queue != "" {
n.ProposedWing = fm.get("proposed_wing")
n.ProposalReason = unquoteScalar(fm.get("proposal_reason"))
n.ProposalConfidence, _ = strconv.ParseFloat(fm.get("proposal_confidence"), 64)
}
out = append(out, n)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt < out[j].CreatedAt })
return out, nil
}
@@ -239,3 +276,13 @@ func readRawSource(brainDir, base string) ([]byte, error) {
}
return raw, nil
}
// unquoteScalar undoes the Go-style double quoting the watcher writes around a
// free-text value (strconv.Quote keeps it on one line); a bare value is
// returned as is.
func unquoteScalar(v string) string {
if u, err := strconv.Unquote(v); err == nil {
return u
}
return v
}