Add brain_reject: brain/raw/ pending notes can only be promoted, never discarded #81

Closed
opened 2026-07-15 09:45:18 +00:00 by mathias · 3 comments
Owner

Context

brain_pending lists notes in brain/raw/ awaiting human review; brain_promote
moves one into the structured wiki. There is no third option. Confirmed by
reading ingestion/internal/api/promote.go + ingestion/internal/mcp/handlers.go
directly (not guessing): only ListPending/PromoteNote exist, no
RejectNote, no reject.go, no reject tool descriptor.

Live consequence: brain_pending currently returns 4 items, oldest dated
2026-04-23 — three months old. They're not low-value by content (financial
sentiment analysis / hash-encoding scrape notes, an invoice-extraction design
note) — they're stuck because there's no way to say "reviewed, not worth
promoting" and have that recorded. The only way to remove one today is an
un-audited rm on the raw file directly, bypassing the tool surface entirely.

This is the same shape of gap as brain-gardener#104 (a queue that only grows
because nothing forces or records a negative decision) — found while auditing
that exact failure mode in a sibling system.

Proposal

Add RejectNote(brainDir, filename, reason string) (string, error) in
ingestion/internal/api/promote.go (or a new reject.go alongside it),
matching PromoteNote's existing conventions:

  • Same filename validation (basename-only, no traversal — reuse the exact
    check PromoteNote already does).
  • Move (not delete) brain/raw/<filename> → brain/raw/rejected/<filename>,
    write-then-remove like PromoteNote's write-then-delete pattern, so a
    filesystem failure never loses the note.
  • Stamp rejected_at (+ reject_reason if given) into frontmatter before the
    move, same style as promoted_at.
  • No index rebuild / auto-tunnel needed (rejected notes never entered the wiki).

Expose as brain_reject in handlers.go's tool list, same schema shape as
brain_promote (filename required, reason optional) — human-invoked only,
matching the existing promote/pending human-in-the-loop model. Never called
automatically; this stays a human decision, same principle as
issue-status-derived-surfaced-not-closed in the brain (issue closure is a
human judgment call, never a mechanical fact) — a reject is exactly that same
shape of judgment call, just one level earlier in the pipeline.

Acceptance

  • RejectNote + test (moves file, stamps frontmatter, write-then-remove
    safety, filename validation matches PromoteNote's).
  • brain_reject MCP tool wired in, descriptor + handler + test (mirror
    brain_promote's existing test in handlers_test.go).
  • Live-run against the 4 real stale pending items once shipped — the actual
    backlog that surfaced this gap should get cleared as part of verifying it.

Related

  • brain-gardener#104/#121/#122 (same failure shape: unreviewed queue, no
    negative-decision path, discovered during a broader "how do we keep brain
    weeds from taking root" review, 2026-07-15).
  • Also noticed while reading ListPending: it explicitly skips
    tunnel-candidates-*.md files in raw/ — a second, distinct candidate queue
    (auto-detected cross-wing link suggestions?) that wasn't investigated this
    session. Worth checking whether it has the same rot risk — filed as a
    discovered-but-deferred note, not scoped here.
## Context `brain_pending` lists notes in `brain/raw/` awaiting human review; `brain_promote` moves one into the structured wiki. There is no third option. Confirmed by reading `ingestion/internal/api/promote.go` + `ingestion/internal/mcp/handlers.go` directly (not guessing): only `ListPending`/`PromoteNote` exist, no `RejectNote`, no `reject.go`, no reject tool descriptor. **Live consequence:** `brain_pending` currently returns 4 items, oldest dated 2026-04-23 — three months old. They're not low-value by content (financial sentiment analysis / hash-encoding scrape notes, an invoice-extraction design note) — they're stuck because there's no way to say "reviewed, not worth promoting" and have that recorded. The only way to remove one today is an un-audited `rm` on the raw file directly, bypassing the tool surface entirely. This is the same shape of gap as brain-gardener#104 (a queue that only grows because nothing forces or records a negative decision) — found while auditing that exact failure mode in a sibling system. ## Proposal Add `RejectNote(brainDir, filename, reason string) (string, error)` in `ingestion/internal/api/promote.go` (or a new `reject.go` alongside it), matching `PromoteNote`'s existing conventions: - Same filename validation (basename-only, no traversal — reuse the exact check `PromoteNote` already does). - Move (not delete) `brain/raw/<filename>` → `brain/raw/rejected/<filename>`, write-then-remove like `PromoteNote`'s write-then-delete pattern, so a filesystem failure never loses the note. - Stamp `rejected_at` (+ `reject_reason` if given) into frontmatter before the move, same style as `promoted_at`. - No index rebuild / auto-tunnel needed (rejected notes never entered the wiki). Expose as `brain_reject` in `handlers.go`'s tool list, same schema shape as `brain_promote` (`filename` required, `reason` optional) — human-invoked only, matching the existing promote/pending human-in-the-loop model. Never called automatically; this stays a human decision, same principle as `issue-status-derived-surfaced-not-closed` in the brain (issue closure is a human judgment call, never a mechanical fact) — a reject is exactly that same shape of judgment call, just one level earlier in the pipeline. ## Acceptance - [ ] `RejectNote` + test (moves file, stamps frontmatter, write-then-remove safety, filename validation matches `PromoteNote`'s). - [ ] `brain_reject` MCP tool wired in, descriptor + handler + test (mirror `brain_promote`'s existing test in `handlers_test.go`). - [ ] Live-run against the 4 real stale pending items once shipped — the actual backlog that surfaced this gap should get cleared as part of verifying it. ## Related - brain-gardener#104/#121/#122 (same failure shape: unreviewed queue, no negative-decision path, discovered during a broader "how do we keep brain weeds from taking root" review, 2026-07-15). - Also noticed while reading `ListPending`: it explicitly skips `tunnel-candidates-*.md` files in `raw/` — a second, distinct candidate queue (auto-detected cross-wing link suggestions?) that wasn't investigated this session. Worth checking whether it has the same rot risk — filed as a discovered-but-deferred note, not scoped here.
mathias added the status:dispatch label 2026-08-24 21:49:53 +00:00
mathias added the status:dispatched label 2026-08-24 21:51:03 +00:00
Author
Owner

🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run

🤖 dispatch: routing: executor=claude (default — no executor:<name> label) — starting headless run
Author
Owner

🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)

🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
mathias added the risk/mediumsize/stype/feat labels 2026-08-27 05:25:42 +00:00
mathias removed the status:dispatch label 2026-08-27 05:31:45 +00:00
Author
Owner

Re-reviewed and merged. Merge commit 674858fe5eec9d220260a6eeaa764fba02a272ff (squash of PR #133, head 69731d9).

Watcher bug fix verified independently (not trusted from the commit message):

  • Confirmed "rejected" is in the skip-list at the exact d.IsDir() branch in processDir (ingestion/internal/watcher/watcher.go).
  • Flip-tested myself: removed "rejected" from the skip-list -> TestProcessDir_SkipsSubdirs went red with file processed file=declined-file.md in the log, exactly the bug described. Restored -> green again.
  • Independently read graphsync.BackfillFromBrainDir (roots := []string{"wiki", "knowledge"}) and search.resolveRoots (all returned paths under wiki//knowledge/) - neither touches raw/, confirming the PR's claim.
  • Searched the whole ingestion/ module for every raw/raw/ reference to find a fourth consumer: found internal/brain/tunnel.go's logFuzzyCandidates (writes into raw/ root, never reads/walks it - harmless) and internal/notes/path.go's deniedPrefixes (a denylist that explicitly refuses raw/ in every scope - protective, not a consumer). No fourth walker of raw/rejected/ exists.
  • Full suite green: go build ./... && go vet ./... && go test ./... -race -count=1, all packages pass.
  • CI (gitea actions run 7878) green on head sha 69731d9, matching PR head exactly.

Gap found, not blocking: the issue's third acceptance item ("live-run against the real stale pending items once shipped") was never done - the PR author explicitly deferred it to post-deploy. Filed as #134 to track so it isn't lost to merge auto-close.

Re-reviewed and merged. Merge commit `674858fe5eec9d220260a6eeaa764fba02a272ff` (squash of PR #133, head 69731d9). **Watcher bug fix verified independently** (not trusted from the commit message): - Confirmed `"rejected"` is in the skip-list at the exact `d.IsDir()` branch in `processDir` (`ingestion/internal/watcher/watcher.go`). - Flip-tested myself: removed `"rejected"` from the skip-list -> `TestProcessDir_SkipsSubdirs` went red with `file processed file=declined-file.md` in the log, exactly the bug described. Restored -> green again. - Independently read `graphsync.BackfillFromBrainDir` (`roots := []string{"wiki", "knowledge"}`) and `search.resolveRoots` (all returned paths under `wiki/`/`knowledge/`) - neither touches `raw/`, confirming the PR's claim. - Searched the whole `ingestion/` module for every `raw`/`raw/` reference to find a fourth consumer: found `internal/brain/tunnel.go`'s `logFuzzyCandidates` (writes into `raw/` root, never reads/walks it - harmless) and `internal/notes/path.go`'s `deniedPrefixes` (a denylist that explicitly refuses `raw/` in every scope - protective, not a consumer). No fourth walker of `raw/rejected/` exists. - Full suite green: `go build ./... && go vet ./... && go test ./... -race -count=1`, all packages pass. - CI (gitea actions run 7878) green on head sha `69731d9`, matching PR head exactly. **Gap found, not blocking:** the issue's third acceptance item ("live-run against the real stale pending items once shipped") was never done - the PR author explicitly deferred it to post-deploy. Filed as #134 to track so it isn't lost to merge auto-close.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#81