The embed sync goroutine only walked brain/wiki/. brain/knowledge/ (112
curated entries, per CLAUDE.md the most-important brain content) had zero
coverage in brain_embeddings — vector retrieval was blind to it. Hybrid
BM25 + pgvector retrieval would never surface a curated knowledge entry
via the vector arm.
Extract the per-root walk into a loop over a small subdir list and add
"knowledge" alongside "wiki". scanDirs is package-level so it stays a
single source of truth for what gets embedded.
Also log each failing item's path + error string from StartSync.
Previously only the aggregate count was logged, so a persistent
`errors=1` per cycle was opaque. With per-item warnings, the actual
ollama "input length exceeds the context length" surface immediately.
Refs gitea/mathias/infra#37 (this commit covers the knowledge/ scan
bug; the long-file chunking bug is a separate change.)
The previous "crude redaction" — pgDSN[:strings.IndexByte(pgDSN+"@", '@')] —
sliced up to the `@` character, which sits *after* the password in a
postgres URL, so the log line included the password in plaintext (caught
on first activation, 2026-05-18 startup log).
Use url.Parse + URL.Redacted() instead. Falls back to "postgres://***"
if parsing fails — we never log a raw DSN.
CREATE DATABASE doesn't work inside a DO $$ ... $$ block (transactional
restriction). And psql `:'var'` substitutions resolve client-side, so
they can't reach inside a DO block either.
Replace both DO blocks with psql-native idioms:
- `\gexec` for the conditional CREATE DATABASE
- `\if` + `\gset` for the create-or-rotate-password branch on the
brain_app role
Verified end-to-end on koala postgres18: brain DB created, vector
0.8.1 extension installed, brain_app role login works.
Wires nomic-embed-text (iguana ollama) + pgvector on the shared
postgres18 into brain_query / brain_answer via Reciprocal Rank Fusion.
Pure BM25 stays the default; setting BRAIN_PG_DSN and BRAIN_EMBED_URL
together opts in. Setting one without the other is misconfiguration →
exit 1.
New packages:
- internal/embed
Client.Embed(ctx, text) → []float32 via POST {URL}/api/embed.
Defaults to nomic-embed-text:latest (768 dim). nil-on-empty-URL so
callers gate on a single nil check.
- internal/vectorstore
PGStore wraps a pgxpool against postgres18. Init creates
brain_embeddings(path PK, vector(768), updated_at) + HNSW cosine
index idempotently. Upsert / Delete / Search / KnownPaths.
Sync(brainDir, store, embedder) diffs brain/wiki/ against the store
and upserts new files / deletes removed ones; StartSync runs it on
a ticker (default 300s). Integration tests gated by BRAIN_PG_TEST_DSN.
- scripts/brain-embeddings-init.sql
One-time DBA setup: brain DB, brain_app role, vector extension,
GRANTs. Idempotent.
Search layer:
- search.QueryOptions gains Vector + Embedder fields.
- QueryContext is the cancellable variant; Query stays for callers.
- When both are set, BM25 (top-N) and pgvector (top-4N) candidates
merge via Reciprocal Rank Fusion (k=60, Cormack et al. 2009 — no
tuning knob, robust to scale differences between rankers).
- Vector-only hits are hydrated from disk so callers see uniform
Result records (path, title, excerpt, wing, hall, score).
- Wing/hall filters still apply to vector candidates via path-prefix.
- On embedder/vector errors the search falls back to BM25 — embedding
outage degrades quality but doesn't take the brain offline.
MCP wiring:
- mcp.Server.WithHybridRetrieval(v, e) opt-in setter, same shape as
WithReranker.
- brainQuery and brainAnswer pass the wired vector/embedder through
to search.QueryContext.
REST:
- POST /backfill-embeddings drives Sync synchronously. Returns
{added, deleted, errors[]}. 503 when feature is unconfigured.
cmd/server/main.go:
- BRAIN_PG_DSN + BRAIN_EMBED_URL together enable hybrid; one alone
→ exit 1.
- vectorAdapter bridges *PGStore (returns []Hit) to
search.VectorSearcher (which takes []VectorHit) without either
package importing the other.
- BRAIN_EMBED_SYNC_INTERVAL (default 300s) controls the background
Sync ticker.
Backend pivot from Qdrant to pgvector recorded in DECISIONS.md
2026-05-18 (supersedes 2026-04-08): postgres18 already runs in
databases/ ns, Qdrant was never deployed, one engine beats two.
Dependency: github.com/jackc/pgx/v5 — modern, native pgvector via
parametric vector literals.
Tests:
- embed.Client: empty-URL nil, request shape, dimension, upstream
error propagation, empty-text rejection.
- vectorstore.PGStore: dimension validation (unit); upsert/search/
KnownPaths (integration, BRAIN_PG_TEST_DSN-gated).
- vectorstore.Sync: adds new files, skips known, deletes
disappeared, skips _index.md, no-op when nil, collects embedder
errors.
- search.Query: hybrid promotes vector-only hits via RRF; falls
back to BM25 on embedder error.
Closes hyperguild#8.
Adds an opt-in cross-encoder rerank step between BM25 retrieval and LLM
synthesis. With BRAIN_RERANKER_URL set, brain_answer retrieves BM25
top-20, scores each excerpt against the query via Qwen3-Reranker on
Ollama, drops the "no" answers, and forwards up to 5 surviving sources
to the LLM. Unset, behaviour is unchanged (BM25 top-10 → LLM).
The reranker is a *filter*, not a re-ranker: Qwen3-Reranker emits a
binary yes/no token under its native chat template, and ties within the
"yes" set are broken by BM25 rank — what got retrieved first stays
ahead.
New package ingestion/internal/reranker:
- Client with URL, Model, HTTP fields.
- New(url, model) returns nil on empty url so callers can treat
"feature disabled" as a single nil check.
- Score(ctx, query, docs) issues one /api/generate call per doc using
the Qwen3-Reranker yes/no chat template (verbatim, because the model
was trained on this exact wording). Parses the first non-think token.
Wiring:
- mcp.Server gains a WithReranker fluent setter to keep NewServer
signature stable.
- brain_answer's BM25 limit jumps to 20 only when a reranker is wired,
to give the filter something to do.
- cmd/server/main.go reads BRAIN_RERANKER_URL (+ optional
BRAIN_RERANKER_MODEL, default dengcao/Qwen3-Reranker-0.6B:F16).
Tests cover: nil-on-empty-url, ordered yes/no scoring, request shape
(model, prompt contents, yes/no template), ambiguous response → 0,
empty doc slice, upstream-error propagation, plus an end-to-end
brain_answer integration that proves only the relevant note reaches the
LLM when noise.md is rejected.
Closes hyperguild#7.
Adds a minimal RFC 8414 + RFC 6749 client_credentials flow so claude.ai's
custom-MCP integration (no static-Bearer field in the UI) can exchange a
client_id + client_secret pair for the existing BRAIN_MCP_TOKEN and use
it as a Bearer on /mcp. No JWTs, no refresh, no expiry — the rest of
the auth middleware is unchanged.
New package ingestion/internal/oauth:
- MetadataHandler(issuer): serves /.well-known/oauth-authorization-server
with grant_types=[client_credentials] and both
token_endpoint_auth_methods (post + basic).
- TokenHandler(cfg): serves /oauth/token. Validates client_id and
client_secret via constant-time compare; returns BRAIN_MCP_TOKEN as
access_token. RFC 6749 §5.2 error JSON on bad grant / bad creds.
Wiring in cmd/server/main.go: opt-in by setting both OAUTH_CLIENT_ID and
OAUTH_CLIENT_SECRET. Setting only one is misconfiguration → exit 1.
Mounts both endpoints with no auth; MCP_RESOURCE_URL supplies the
issuer.
Also pivots issue #8's vector backend from Qdrant to pgvector (see
DECISIONS.md 2026-05-18) — Qdrant was never deployed and postgres18 with
pgvector already runs as the project default; supersedes 2026-04-08 for
this use case.
Tests cover post-auth, basic-auth, wrong secret, bad grant, GET
rejection, malformed Basic header, and Basic without colon.
Closes hyperguild#5.
Adds the `brain_tunnel` MCP tool and auto-tunnel behaviour for
`brain_write`, so concepts that appear in multiple wings become
navigable from any of them.
New surface in package brain:
- WriteTunnel(brainDir, src, tgt) — appends a `## See also` bidirectional
wikilink between two notes in different wings. Idempotent (link not
duplicated on re-call) and reuses an existing See also section.
- DetectTunnels(brainDir, content) — walks brain/wiki/, returns
TunnelCandidates for notes whose title appears in content. Tags
whole-word case-insensitive hits as Exact=true and substring-only hits
as Exact=false.
- AutoTunnel(brainDir, src, content) — wraps DetectTunnels: writes
cross-wing exact matches, stages fuzzy matches into
brain/raw/tunnel-candidates-<YYYY-MM-DD>.md for human review.
MCP wiring:
- `brain_tunnel` tool: explicit manual link (source, target).
- `brain_write` with wing+hall now triggers AutoTunnel on the new
content. Failures are logged and never abort the primary write.
readTitleAndCreated also humanises the slug fallback (hyphens → spaces)
so titleless notes participate in content matching.
Closes hyperguild#16.
Tests: idempotency, same-wing rejection, missing-note rejection,
See-also reuse, exact/fuzzy detection, slug fallback, MCP tool happy
path, auto-tunnel hook (cross-wing exact → linked; same-wing → skipped;
fuzzy → candidates file).
Reorders BearerAuth so a valid BRAIN_MCP_TOKEN match wins instantly and
never emits WWW-Authenticate. Adds RFC 9728 resource_metadata challenge
header on 401 (only when MCP_RESOURCE_URL is configured) so claude.ai's
OAuth-discovery path still works.
Why: claude CLI on koala/flamingo with `.mcp.json` `Authorization: Bearer
$BRAIN_MCP_TOKEN` was being kicked into RFC 7591 dynamic client
registration against Dex (static-only) and dying. Cause was the auth
middleware running JWT validation first and emitting an OAuth challenge
on the fall-through 401 even when the caller had a valid static token.
Inverting the precedence and gating the challenge on resourceMetadataURL
keeps the LAN/Tailscale CLI path silent and only invites OAuth discovery
on actually-unauthenticated requests.
Regression guards in the test file:
- valid static Bearer 200 has no WWW-Authenticate
- 401 with resourceMetadataURL set carries the challenge
- 401 with empty resourceMetadataURL emits no challenge
Closes hyperguild#9 in code. Live verification (claude CLI on koala
listing brain tools) blocked on ingestion image rebuild + redeploy.
Adds a two-dimensional address (wing, hall) to brain notes. A wing is a
topic domain (e.g. jepa-fx, hyperguild); a hall is one of a closed
vocabulary of memory types (facts, decisions, failures, hypotheses,
sources). Notes route to brain/wiki/<wing>/<hall>/<slug>.md with
wing/hall/created_at YAML frontmatter, making the directory a valid
Obsidian vault.
Changes:
- new package ingestion/internal/brain (NotePath, ValidHalls, Sanitise,
BuildWingIndex, BuildAllWingIndexes)
- api.WriteNote refactored to WriteNoteOptions; wing+hall routes to
brain/wiki/, otherwise falls back to brain/knowledge/ (legacy)
- search.Query → QueryOptions with optional Wing/Hall filtering; Result
carries wing/hall extracted from frontmatter or path segments
- MCP tools brain_write and brain_query gain optional wing/hall params
(hall enum-validated); new brain_index tool regenerates _index.md MOC
- POST /index REST endpoint mirrors brain_index
- brain_write auto-rebuilds the wing's _index.md after a wing+hall write
- scripts/migrate-brain-halls.sh migrates flat brain/wiki/{concepts,entities}/
into the new layout (dry-run by default, --commit applies)
All existing tests pass; new tests cover wing/hall write routing, scope
filtering, invalid hall rejection, _index.md generation, and migration
script paths.
Closes hyperguild#1.
- Random port via net.Listen(":0") replaces hardcoded 33310 (was the
primary failure mode under parallel test load).
- Bump waitForPort deadline 5s → 30s — `go build` under -race can exceed
5s on a loaded machine.
- Replace osPath() (always returned empty PATH because exec.Command("env").Env
is the *child's* env, not the parent's) with explicit PATH+HOME via
os.Getenv. Don't inherit full env: would leak ROUTING_MCP_TOKEN from the
parent shell and flip the routing pod into auth-required mode, breaking
the test.
Closes#15. Verified: 10 cold-cache test runs pass, 3 consecutive task check
runs pass.
Drops the intermediate `staging/<name>` branch so Flux begins reconciling the
namespace within ~60s of `project_create` instead of waiting on a human PR
merge. Consistent with project-wide trunk-based development.
Rationale: ADR 2026-05-18 in DECISIONS.md.
Closes hyperguild#14 (item 1). Item 2 (GITEA_MCP_TOKEN in SOPS) verified
already-present in infra@408a527 secrets.enc.yaml.
Note: TestRoutingPodEndToEnd is failing on main pre-existing this commit
(context deadline waiting for port 33310 in <5s). Not caused by this change;
project skill tests pass. To track in a separate issue.
mcpclient.New previously accepted an empty token and silently omitted
the Authorization header at request time. When the env var sourcing
the token was missing from a Kubernetes Secret (envFrom doesn't warn
on missing keys), this surfaced as an opaque 401 from the upstream
MCP server with no log trail — see hyperguild #13 and brain entry
"mcpclient-empty-token-silent-401-envfrom-missing-key".
mcpclient.New now returns ErrTokenRequired when token is empty.
The routing pod's project_create init checks the error and exits
with a clear message pointing at routing-secrets, turning a runtime
401 storm into a startup crashloop the operator can fix immediately.
Tests pass a dummy "test" token (httptest servers don't enforce
bearer auth, so any non-empty value works). Added a regression
test asserting empty-token construction returns ErrTokenRequired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gitea's push-mirror cannot push to a non-existent remote — it just
runs 'git push' against whatever URL it's given. So a project_create
flow that only configures the mirror leaves the GitHub side as an
unfulfillable URL.
New internal/githubclient package: single-purpose client that POSTs
/user/repos to create an empty private repo (auto_init=false so the
first mirror push doesn't conflict with a generated README). Treats
422 'name already exists' as idempotent success via ErrAlreadyExists.
401/403 are surfaced as 'PAT missing repo scope or invalid' so the
operator sees the real cause instead of a vague upstream error.
Skill wiring:
- New stepCreateGitHub between stepCreateRepo and stepMirror in the
orchestrator.
- Skipped entirely when Config.GitHub is nil (degraded mode — the
routing pod runs without GITHUB_PAT, mirror config still lands,
but the actual sync to github fails until the repo exists).
- cmd/routing/main.go constructs githubclient.New(GitHubPAT) only
when the PAT is set; the skill receives nil otherwise.
Tests:
- happy path: fake github 201 + assertions that the 'reached' array
is [create_repo, create_github_repo, mirror, infra_commit, issue].
- github 422 already-exists: idempotent, all gitea steps still run.
- github 401: returns failed_step=create_github_repo, no mirror or
later steps.
- degraded mode (Config.GitHub nil): reached omits create_github_repo,
rest of the flow runs unchanged.
Updated existing tests to read [skill, gh] from newSkill instead of
just skill, and adjusted reached-array expectations to include the
new step.
Tracks #10.
Plan 7 (2026-05-12) retired the supervisor pod, deleted cmd/supervisor/
and the root Dockerfile, but cd.yml still tried to:
- buildctl a supervisor image using the (non-existent) root Dockerfile
- sed gitea.d-ma.be/mathias/supervisor: in k3s/apps/supervisor/deployment.yaml
(also non-existent — k3s/apps/supervisor/ only ships ingestion-* files now)
- wait for and rollout-verify a supervisor Deployment that no longer exists
Result: every CD run since the retirement has been failing at 'Build and push
supervisor image', leaving ingestion + routing un-deployed despite the binaries
being built. The routing pod was last deployed at sha 189ff89c (weeks stale).
This commit:
- Removes the supervisor build step and supervisor sed/git add lines.
- Adds 'Wait for Flux to apply new routing image' + 'Verify routing rollout'
steps that mirror the ingestion equivalents, so failures land loudly rather
than 5 min later when something tries to call the new tool.
- Updates the chore(deploy) commit message to 'ingestion+routing' to match
reality.
Unblocks deployment of feat: project_create (#10).
Adds the project_create tool to the routing pod that automates the
"new project" bootstrap end-to-end from claude.ai. Gitea-first
architecture: GitHub receives the repo only via push-mirror, never
via a direct GitHub API call from this server.
Four sequential calls to the gitea-mcp server (configured via
GITEA_MCP_URL):
1. create_project_from_template — Gitea repo from
template-go-{agent,web} per the 'stack' arg
2. repo_mirror_push (action=add) — push-mirror to
github.com/<GITHUB_OWNER>/<name>.git, interval 8h, sync_on_commit
3. file_write_branch — k3s/staging/<name>/namespace.yaml committed
on a staging/<name> branch in the infra repo
4. issue_create — experiment brief (hypothesis + description + stack
+ provisioning log) on the new repo, returns the issue_url
Returns gitea_url, github_url, issue_url, next_steps. The next_steps
string is the exact shell sequence the operator runs locally to
clone, scaffold via local-dev 'task new-project', and push.
Idempotency: create_project_from_template + repo_mirror_push +
file_write_branch all return JSON-RPC code -32003 (Conflict) when
their target already exists; the orchestrator swallows the conflict
and continues. Re-running on an existing repo restates the brief in
a fresh issue.
Error handling: on any non-conflict downstream failure the response
returns {reached: ["<step>",...], failed_step: "<step>"} alongside
a JSON-RPC error. No rollback — partial state stays so the operator
can resume manually.
New env vars (all optional except GITEA_MCP_URL):
GITEA_MCP_URL enables the tool
GITEA_MCP_TOKEN bearer auth for gitea-mcp
GITEA_OWNER default mathias
GITHUB_OWNER default mathiasb
INFRA_REPO default infra
GITHUB_PAT repo scope, used as mirror remote_password; never logged
Without GITEA_MCP_URL set, the tool is not registered and the
routing pod starts normally (degrades open).
internal/mcpclient/: new minimal JSON-RPC tools/call client with
bearer auth, used by project_create. Unwraps MCP's
content[0].text envelope and surfaces typed errors via mcpclient.Error.
Tests: table-driven against an httptest fake gitea-mcp covering happy
path (4-step success + correct PATCH-style arg shapes), idempotent
repo-exists, mirror failure (partial-success response with reached=
[create_repo] + failed_step=mirror), infra-commit failure (reached up
to mirror + failed_step=infra_commit), and validation errors.
Closes#10
Removes the supervisor binary and its two exclusive skill packages (tdd,
spec) now that all functionality is covered by SKILL.md files, the routing
pod, and the brain MCP. Routing pod reuses review/debug/retrospective/trainer
skill packages which are intentionally preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two new LLM-backed MCP tools to the ingestion service:
- brain_answer(query): BM25 retrieval + LLM synthesis → answer + sources
- brain_classify(text): classifies doc into type/title/tags via LLM
Adds llm.Router for primary→fallback routing (berget.ai → iguana).
Wired via BRAIN_LLM_PRIMARY_URL/BRAIN_LLM_FALLBACK_URL env vars;
no-op when unset so existing deployments are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The routing decision is about reasoning capacity, not cost or provider.
Fast model (koala/qwen35-9b-fast) handles high-pass-rate calls; thinking
model (iguana/gemma4-26b) handles low-pass-rate calls. Removes the
implicit Anthropic dependency from the routing pod — both models go
through LiteLLM.
Renames: HYPERGUILD_LOCAL_MODEL → HYPERGUILD_FAST_MODEL,
HYPERGUILD_CLAUDE_MODEL → HYPERGUILD_THINKING_MODEL,
Router.LocalModel → FastModel, Router.ClaudeModel → ThinkingModel,
log decision "claude_fallback" → "thinking_fallback".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brain and supervisor now behind NPM with Let's Encrypt. Use canonical
hostnames (brain-mcp.d-ma.be, supervisor-mcp.d-ma.be) over NodePorts so
connections work across networks without Tailscale for DNS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
claude.ai probes with GET before initialize; without this the supervisor
returned application/json parse error instead of text/event-stream, causing
"Couldn't reach the MCP server" in the claude.ai connector setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns hyperguild's cd.yml with the cobalt-dingo reference pattern:
- Add environment: staging to the deploy job
- Add Flux reconcile trigger after infra repo push
- Add polling wait for supervisor and ingestion image tags to propagate
- Add rollout status verification for both deployments with failure
diagnostics (pod status, events, describe)
Add routing pod to README architecture diagram and env vars table.
Add routing MCP endpoint to .context/PROJECT.md. Regenerate derived
context adapters via task context:sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plan 6 is now deployed; replace the _routing_pending placeholder in the
routing MCP entry with a real headers block carrying X-Hyperguild-Mode:
client-local. The pod treats absent or unknown values as client-local,
so this is forward-compat for future modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires Config → LiteLLMExecutor → Router → four skills (review, debug,
retrospective, trainer) → Registry → MCP server with bearer auth and
/healthz. Each skill's CompleteFunc is wrapped so the Router decides
local-vs-Claude per call and logs every decision to the brain /mcp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures the four routed skills' (review, debug, retrospective, trainer)
tool definitions as a JSON snapshot and asserts the routing pod's registry
advertises byte-equal schemas. A deliberate schema change fails this test,
requiring an intentional snapshot update in lockstep with consumers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace silent `_ = r.Logger.LogDecision(...)` discards with an
if-err check that emits slog.Warn on failure. A brain outage now
produces a visible warn line instead of swallowing the telemetry
error entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Composes Fetcher + Policy + Logger + CompleteFunc into a single Run method.
Falls open to Claude on local-model errors; defaults to local when brain is
unreachable. Skill packages will receive Router.Run as their CompleteFunc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTTP client that calls GET /pass-rate?skill=X&window=Y on the brain pod.
Caches *float64 results (including nil) per-skill for the configured TTL
(default 60s). On non-200 or network error returns (nil, err) so the
upstream router can fall through to default-to-local.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SHA-256 of (system, user) joined with 0x00 separator, truncated to
uint64. Drives deterministic sample-band routing: identical prompt pair
→ same hash → same local-vs-Claude decision on every call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typed config struct and env parser for the routing pod. Kept separate
from the supervisor Config to avoid forcing routing fields onto the
supervisor and vice versa. Uses the existing envOr helper from config.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 TDD-shaped tasks across two worktrees: hyperguild for code
(internal/routing package, cmd/routing binary, Dockerfile, CD
workflow, mode template, smoke test, docs) and infra for the
k3s manifests (deployment, service, nodeport, SOPS-encrypted
secret). Plan 7 amendment baked in: internal/skills/{review,
debug,retrospective,trainer} survive Plan 6 — Plan 7 only
deletes tdd, spec, and the supervisor binary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drafted via superpowers:feature-spec. Plan 6 of 7 in the skill
migration. Surface frozen at 4 cost-routable skills (code_review,
debug, retrospective, trainer); LiteLLM proxies model choice; pass-
rate drives the route decision with default-to-local plus an env
kill switch for the empty-data window. Plan 7 amendment baked in:
internal/skills/{review,debug,retrospective,trainer} survive Plan 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pod now enforces SUPERVISOR_MCP_TOKEN; this matches the .mcp.json
header so a Claude Code session in this repo authenticates correctly.
Token comes from the operator's shell env, not the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enables exposing the supervisor MCP via Tailscale Funnel for claude.ai
custom-connector tests. Auth is opt-in: empty SUPERVISOR_MCP_TOKEN
preserves the existing unauthenticated behavior for tailnet-internal
callers and local dev.
When the token is set, every request must carry
"Authorization: Bearer <token>" or it is rejected with HTTP 401 and a
JSON-RPC -32001 error. Comparison uses crypto/subtle.ConstantTimeCompare;
the token value and the supplied header are never logged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two corrections applied during Plan 5 execution:
- Task 2 test helper writeSession now joins a sessions/ subdir so it
matches the handler's <brainDir>/sessions/*.jsonl scan path.
(The original heredoc would have produced 0 records in tests.)
- Task 6 grew a Step 1.5 to update the session_log MCP tool's
final_status description, picking up a spec requirement that
didn't translate into a task in the original plan.
Adds the read side of pass-rate logging that consumes the SKILL.md
instrumentation landed in the local-dev companion merge:
- GET /pass-rate?skill=X&window=Y on the brain pod (ingestion module)
walks brain/sessions/*.jsonl, normalizes legacy {ok,error,skipped}
to canonical {pass,fail,skip}, returns aggregate counts plus
pass_rate (null when pass+fail == 0).
- hyperguild brain pass-rate <skill> [--window 7d] [--json] CLI
subcommand (third nested verb under brain).
- session_log MCP tool docstring updated to lead with the new
pass|fail|skip vocabulary; legacy values still accepted by both
the tool's loose validator and the aggregator's normalization.
This is the brain HTTP REST API's first GET endpoint — pure reads
follow REST semantics; legacy POST routes (query, write, ingest, etc.)
all take JSON bodies. Future read endpoints SHOULD use GET.
Plan 6 routing pod is the consumer; one week of usage data between
this merge and Plan 6 deploy will mean Plan 6 lands on real numbers.
Plan: docs/superpowers/plans/2026-05-03-pass-rate-logging.md
Spec: docs/superpowers/specs/2026-05-03-pass-rate-logging-design.md
Adds pass-rate to the CLI README's subcommand block. Updates CLAUDE.md
to note the new /pass-rate endpoint alongside the existing brain
HTTP REST API surface. Updates the session_log MCP tool's
final_status description to reflect the new pass|fail|skip vocabulary
introduced by Plan 5's SKILL.md instrumentation; the aggregator
still accepts legacy ok|error|skipped values for backwards compat.
Adds 'hyperguild brain pass-rate <skill> [--window 7d] [--json]'
calling the new /pass-rate endpoint. Human output:
tdd: 47 / 50 = 94% (window: 7d)
or 'no data (window: 7d)' when pass_rate is null.
PassRateResult mirrors the response envelope; PassRate is *float64
so null is preserved across decode.
Adds the route entry alongside the existing POST routes. Note: this
is the brain HTTP REST API's first GET endpoint — it follows REST
semantics for pure reads, while the legacy POST routes (query, write,
ingest, etc.) all take JSON bodies. Future read endpoints SHOULD use
GET; future write endpoints continue with POST.
Adds a new HTTP GET handler at the ingestion pod that walks
brain/sessions/*.jsonl, filters by skill name and timestamp window
(default 7d, accepts Nh and Nd), normalizes legacy status vocabulary
(ok->pass, error->fail, skipped->skip), and returns aggregated counts
plus pass_rate.
Pass rate is null when pass+fail == 0, distinguishing 'no data' from
'always passes'. Plan 6 routing pod will check for null before
making decisions.
Route registration in cmd/server/main.go lands in a follow-up commit.
Seven-task plan spanning two worktrees: Phase 1 (local-dev) for
SKILL.md instrumentation (pilot tdd, then rollout to 6 binary-outcome
skills), Phase 2 (hyperguild) for the /pass-rate endpoint + CLI
subcommand. Uses GET for the pure-read endpoint (REST semantics)
despite the brain API's POST-everywhere precedent for legacy
endpoints.
Pilot-then-rollout structure: tdd SKILL.md lands first; the
endpoint TDD task naturally exercises the new logging contract
as the dogfood validation step before the other 6 skills get
instrumented.
Skill instrumentation pattern, brain /pass-rate HTTP endpoint, and
optional hyperguild CLI subcommand for shell access. Pilot with tdd
SKILL.md, then roll out to 6 binary-outcome skills (code-review,
debug, feature-spec, session-retrospective, trainer, spec-driven-dev).
Decisions: SKILL.md as source of truth for the logging contract;
on-demand aggregation from JSONL (no materialized counters until
proven necessary); pass|fail|skip vocabulary forward, with
ok|error|skipped accepted by the read-side aggregator for backwards
compat.
Seven success criteria, ten out-of-scope items, six risks.
A small Go binary at cmd/hyperguild/ providing four subcommands:
- tier probe Anthropic + LiteLLM, print operating tier
- brain query BM25 search the brain via HTTP REST
- brain write write a knowledge entry from stdin
- mode <name> bootstrap .mcp.json (cloud|client-local|sovereign)
Reuses internal/tier verbatim. Brain access uses the HTTP REST API
at ${BRAIN_URL:-http://koala:30330} with POST /query and POST /write.
Mode templates include a Plan 6 routing-pod placeholder (client-local)
and a Crush-fallback note (sovereign).
Stdlib only — flag, net/http, encoding/json. 32 unit tests via
httptest.Server fakes; smoke-tested end-to-end against the live brain.
Replaces the supervisor's tier MCP. Plans 5 (pass-rate logging) and 6
(routing pod) build on the brain client and tier subcommand.
Plan: docs/superpowers/plans/2026-05-03-hyperguild-cli.md
Spec: docs/superpowers/specs/2026-05-03-hyperguild-cli-design.md
dispatch() already prefixes errors with 'hyperguild <subcmd>: ', so
handlers re-prefixing with their own name produced stuttered output
like 'hyperguild brain: brain query: topic required'. Strip the
redundant prefixes from the seven affected errors.New / fmt.Errorf
calls in brain.go and mode.go.
Also fix the LITELLM_BASE_URL usage text — it's optional (empty
falls through to airplane tier), not required, matching the
README.
The brain HTTP REST /query endpoint accepts POST with JSON body
{query, limit}, not GET with URL query string. Surfaced empirically
during Plan 4 Task 7 smoke testing. Spec text now matches the
implementation.
The brain HTTP REST /query endpoint accepts POST with JSON
{query, limit}, not GET with URL query string. Surfaced by
Task 7 smoke testing — GET returned 405 Method Not Allowed.
The response shape ({results:[...]}) is unchanged; only the
request side flips to POST + JSON body. brainClient.Write was
already using POST + JSON body and is unaffected.
Tests updated to assert POST + JSON body on the Query path.
Adds cmd/hyperguild/README.md (subcommands, env vars, install path)
and three Taskfile targets:
task hyperguild:dev — go run from source
task hyperguild:build — build into ./bin/hyperguild
task hyperguild:install — go install into $GOBIN
Concludes Plan 4 of the hyperguild migration. The binary replaces
the supervisor's tier MCP and surfaces brain HTTP REST access plus
mode bootstrap to shell pipelines and ad-hoc agent prompts.
'hyperguild mode <cloud|client-local|sovereign>' writes a per-mode
.mcp.json template:
- cloud: brain MCP only
- client-local: brain + routing placeholder with _routing_pending
pointer to Plan 6
- sovereign: brain only + top-level _mode_note explaining Crush
is primary; .mcp.json is Claude Code fallback
Default output is ./.mcp.json; --out overrides; --force overwrites.
Brain URL sourced from BRAIN_URL (default http://koala:30330) so the
template stays in lockstep with the user's brain host.
All three subcommands now wired; notYet/errNotImplemented removed
from main.go.
Reads markdown from stdin, POSTs to the brain's /write endpoint with
type + slug, prints the resulting path. Pairs with 'brain query' for
shell-friendly read/write access to the brain HTTP REST API.
Tests cover success, missing args, backend error propagation, and
empty stdin (which produces an empty content payload — the brain
server's responsibility to validate).
Adds 'hyperguild brain query <topic>' against the brain HTTP REST
/query endpoint. Default human output prints path + score + title;
--json passes through the response envelope. --limit overrides the
default 5-result cap.
runBrainWrite remains a stub for Task 5.
Adds brainClient with Query and Write methods against the brain's
HTTP REST endpoints (/query, /write). Constructor reads BRAIN_URL env
var, defaulting to http://koala:30330 — the Tailscale-exposed
NodePort that serves both MCP and REST.
Tests cover success, transport error, and non-200 cases via
httptest fakes; URL override is verified via t.Setenv.
Adds the tier subcommand to the hyperguild CLI. Reuses
internal/tier.Detect verbatim, sources probe URLs from
ANTHROPIC_PROBE_URL (default https://api.anthropic.com) and
LITELLM_BASE_URL (no default — empty triggers airplane).
Human-readable output by default; --json emits the same Info struct
as the supervisor's tier MCP returns. Tests cover all three tier
states via httptest fakes.
Lays down the cmd/hyperguild/ entry point. Defines the subcommand
contract (ctx, args, stdin, stdout, stderr) error, the dispatch()
function that's testable without os.Exit, and stubs for tier / brain /
mode that return errNotImplemented. Subsequent commits replace each
stub.
Part of Plan 4 (hyperguild CLI) of the hyperguild migration.
Implementation-level spec for the hyperguild CLI: a stdlib Go binary
at cmd/hyperguild/ with subcommands tier, brain query/write, and mode
(cloud|client-local|sovereign). Replaces the supervisor's tier MCP and
provides shell-friendly access to the brain HTTP REST API.
Six measurable success criteria, seven out-of-scope items, six risks.
Decisions logged: stdlib flag + inline router (no cobra), reuse
internal/tier verbatim, BRAIN_URL env override, mode subcommand writes
.mcp.json with per-mode template plus placeholder for the Plan 6
routing pod.
- README architecture diagram now shows two MCP servers (supervisor +
brain) with the brain hosted by ingestion directly.
- Connect-a-project example includes both servers.
- .context/PROJECT.md replaces the boilerplate "Knowledge base access"
block with the actual hyperguild MCP endpoints.
- Adapters regenerated via task context:sync.
Captures the transitional state where two MCPs coexist; the supervisor
MCP will shrink as skill workers move to SKILL.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The brain MCP at koala:30330 hosts the brain_* and session_log tools
formerly on supervisor. Supervisor stays connected during the
transition; its skill workers and the brain duplication will be
removed in a later plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In CI's clean checkout the tree-walk for ~/dev/.context/AGENT.md
finds nothing, leaving ROOT_CONTEXT empty. The script previously
proceeded to regenerate AGENTS.md, .cursorrules,
.aider.conventions.md, and .context/system-prompt.txt as
project-only — but the committed versions are root+project, so
the drift gate added in cc401d9 fails CI on every push.
When no root context is reachable, only regenerate CLAUDE.md
(which is project-only by design — Claude Code walks up the tree
itself to find the root). The root-bearing adapters are left
untouched, eliminating the false-positive drift.
Local runs (with root context reachable) are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 1 of the larger SKILL.md + routing-MCP architecture migration.
Adds an MCP HTTP handler to the ingestion service at POST /mcp,
exposing 5 tools (brain_query, brain_write, brain_ingest_raw,
brain_ingest, session_log). Plan and 9 implementation commits
preserved on the feat/brain-mcp-migration branch.
NodePort 30330 wired in infra repo separately (commit 008548e).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ingestion server now exposes both REST and MCP on the same port
(3300). MCP shares brainDir, pipeline config, and LLM client with the
REST handlers — single source of process state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Appends a JSON line to brainDir/sessions/<session_id>.jsonl using the
session package copied in Task 2. Required for upcoming pass-rate
logging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps pipeline.Run with the existing LLM client. Mirrors the HTTP
/ingest and /ingest-path semantics — accepts either path or
content+source, validates mutual exclusion, surfaces an explicit error
when the LLM client is not configured (test-mode).
ctx is threaded through to pipeline.Run for cancellation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps pipeline.RunRaw directly. Same dry-run semantics as the HTTP
/ingest-raw endpoint. Test exercises a single concept page; asserts
returned path and that no file is written under dry_run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
api.WriteNote captures the file-write logic that was previously inline
in Handler.Write. The existing HTTP endpoint now delegates to it; the
new MCP brain_write tool reuses the same function. Path-traversal
guard is strengthened to explicitly reject filenames containing path
separators or "..", so the rejection is surfaced before filepath.Base
strips the suspicious component (the previous defense-in-depth prefix
check became unreachable for these inputs after Base normalisation).
HTTP error code for caller-input errors shifts from 500 to 400, which
is semantically correct and not exercised by any existing test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the existing search.Query function. Same BM25 over
brain/knowledge/ and brain/wiki/ that the HTTP /query endpoint serves.
Plan note: handleCall switch replaces the single-line stub from Task 1
— no unknownToolError type to remove since Task 1 inlined the error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy of internal/session from the supervisor module — the ingestion
service needs it for the upcoming session_log MCP tool. The supervisor
copy will be removed in the supervisor-retirement plan; until then
the two packages are intentionally identical and pinned (no edits).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an MCP HTTP handler under ingestion/internal/mcp. Implements
initialize, tools/list, and the JSON-RPC notification skip from prior
work. Tool dispatch is stubbed (returns unknown-tool error) and will be
filled in by subsequent tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 TDD-disciplined tasks moving brain_* and session_log out of the
supervisor pod and into the ingestion pod's MCP handler. Slice 1 of
the larger SKILL.md + routing-MCP architecture migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task's sources: declaration cached the regeneration on the assumption
that adapter outputs depend only on .context/AGENT.md (or PROJECT.md +
.skills/). The cache occasionally skipped legitimate runs after manual
edits to root or template content not detectable from those source paths.
context-sync.sh is idempotent and cheap; running it on every invocation
is the right default. The freshness gate (git status --porcelain) is
unaffected — it always checked the actual git tree state.
Root AGENT.md dropped a stale paragraph; adapters that embed root
(AGENTS.md, .cursorrules, .aider.conventions.md, system-prompt.txt)
need to be regenerated to match. CLAUDE.md is project-only by design
and unchanged.
Nothing in the workspace uses the HANDROLLED escape hatch anymore — the
infra repo (its only consumer) was migrated to the standard pattern.
Removing the dead code: HANDROLLED_MARKER constant, skip_if_handrolled
function, and call sites in each generator. Sync output for any non-
HANDROLLED file (i.e., every file we have) is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 sweep updated .gitignore and Taskfile.yml but missed this
script. Copy the canonical from ~/dev/project-template/ so the
HANDROLLED escape hatch works in this project (a CLAUDE.md or
adapter file containing '<!-- HANDROLLED: do not regenerate -->'
near the top is now skipped on regeneration).
Adapters are now tracked so non-Mac hosts get full agent context after
a plain git pull. task check runs context:sync first and fails on drift
via git status --porcelain over the 5 adapter paths.
Claude Code now supports MCP servers over HTTP natively (type: "http"
in .mcp.json). The stdio↔HTTP bridge binary was a workaround for the
older stdio-only constraint and is no longer needed — the supervisor
NodePort on koala (30320) is reachable over Tailscale from any client
machine.
Removed:
- cmd/bridge/ (Go source, ~60 lines)
- bin/supervisor-bridge artifact
- Taskfile bridge:build target and the build aggregate's reference
- README "Build the bridge binary" instruction
Updated:
- .mcp.json switched to {type:"http", url:"http://koala:30320/mcp"}
- README architecture diagram and "Connect a project" section
Behavioural prerequisite for this change shipped in 7f7524c
(notifications fix). Verified end-to-end: tier tool call returns
{tier:2, label:"lan-only"} via direct HTTP, no shim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The supervisor's MCP HTTP handler was answering every parsed request,
including notifications (messages with no id field). Per JSON-RPC 2.0,
notifications must not receive a response. The Apr-29 incident saw
Claude Code's MCP client receive a -32601 error for the standard
notifications/initialized handshake step and disconnect immediately
after a successful initialize.
Skip writing the response when req.ID == nil. Cover both the
known-method (notifications/initialized) and unknown-method paths
with tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows callers to provide pre-structured RawPage data directly, bypassing the
LLM extraction step. The pipeline still handles slug computation, frontmatter,
link canonicalization, source back-references, and dedup — only the extraction
is skipped. Useful when a more capable model or manual curation produces the
structured data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change prompt to reflect new output format: title, type, subtype, domain, content
- Remove slug/path generation responsibility from LLM — pipeline now handles it
- Wikilinks change from [[slug|Display Name]] to [[Display Name]] only
- LLM no longer includes frontmatter or paths in output
docs(schema): update LLM output format and wikilink convention for Level 3
- Specify JSON schema: title, type, subtype, domain, content fields
- Remove frontmatter requirements from schema output (handled by pipeline)
- Simplify wikilink format to [[Display Name]] — no slug or pipe
- Pipeline now responsible for slug generation and frontmatter construction
These changes shift slug/frontmatter generation from LLM to pipeline,
reducing cognitive load on the model and improving control over output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>