Compare commits
9 Commits
v0.4.0
...
3d6f33881b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d6f33881b | ||
|
|
07e3f341ef | ||
|
|
5c532e708c | ||
|
|
a34c66d7cd | ||
|
|
cc401d92d6 | ||
|
|
9bdf00f51f | ||
|
|
7f7524c859 | ||
|
|
0a70d9e972 | ||
|
|
3e9a648115 |
236
.aider.conventions.md
Normal file
236
.aider.conventions.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Agent context — Mathias workspace
|
||||||
|
|
||||||
|
<!-- Canonical root context for all AI coding agents.
|
||||||
|
Lives at: ~/dev/.context/AGENT.md
|
||||||
|
Applies to every project under ~/dev/ unless overridden.
|
||||||
|
|
||||||
|
Run `task context:sync` from ~/dev/ to regenerate harness-specific files.
|
||||||
|
Project-level context in .context/PROJECT.md layers on top of this. -->
|
||||||
|
|
||||||
|
## Who I am
|
||||||
|
|
||||||
|
I'm Mathias, a digital product manager and technology consultant based in Sweden.
|
||||||
|
I build software, research emerging tech, and deliver consulting engagements
|
||||||
|
for clients under NDA. I work across AI/ML, financial automation, web applications,
|
||||||
|
and climate/sustainability tech.
|
||||||
|
|
||||||
|
## How I work with agents
|
||||||
|
|
||||||
|
- I think like a product manager — I care about *why* before *how*
|
||||||
|
- I want agents to be opinionated and push back, not just execute blindly
|
||||||
|
- I prefer concise responses; skip ceremony and get to the point
|
||||||
|
- When I say "build this", I mean production-quality with tests, not a demo
|
||||||
|
- Ask me before making irreversible changes or adding heavy dependencies
|
||||||
|
- I work with confidential client data — never send it to cloud APIs unless I explicitly say it's OK
|
||||||
|
|
||||||
|
## Behavior rules
|
||||||
|
|
||||||
|
These rules apply to every task across every project, regardless of harness.
|
||||||
|
|
||||||
|
1. **No assumptions.** Don't hide confusion — surface it. Surface tradeoffs explicitly.
|
||||||
|
Think before coding; if the problem is unclear, ask or state assumptions before acting.
|
||||||
|
2. **Minimum viable code.** Solve with the smallest change that works. Nothing
|
||||||
|
speculative, no "while we're here" cleanups, no premature abstractions. Simplicity first.
|
||||||
|
3. **Surgical changes.** Touch only what the task requires. Leave unrelated code,
|
||||||
|
files, and formatting alone. Diffs should be small and reviewable.
|
||||||
|
4. **Goal-driven execution.** Define clear success criteria up front for every task.
|
||||||
|
Loop — implement, verify, refine — until those criteria are met. Don't claim
|
||||||
|
completion without evidence (tests pass, command output, observed behavior).
|
||||||
|
|
||||||
|
## Default stack
|
||||||
|
|
||||||
|
| Layer | Default | Fallback | Last resort |
|
||||||
|
|-------|---------|----------|-------------|
|
||||||
|
| Language | Go | Python | TypeScript, Java, C |
|
||||||
|
| UI | HTMX + Templ | Server-rendered HTML | React (only if SPA is justified) |
|
||||||
|
| Build | Task (taskfile.dev) | Make | — |
|
||||||
|
| Containers | Docker Compose (dev), k3s (prod) | — | — |
|
||||||
|
| DB | PostgreSQL + sqlc | SQLite | — |
|
||||||
|
| Search | Qdrant (vector), BM25 | — | — |
|
||||||
|
| Logging | slog (structured) | — | — |
|
||||||
|
| Testing | Table-driven, testify | — | — |
|
||||||
|
|
||||||
|
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- **Go style**: golines, gofumpt, golangci-lint
|
||||||
|
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return
|
||||||
|
- **Naming**: stdlib conventions, no stuttering
|
||||||
|
- **Architecture**: prefer stdlib over frameworks, constructor injection, env-var config parsed into typed structs
|
||||||
|
- **Git**: conventional commits (`feat:`, `fix:`, `chore:`), one concern per PR, PR describes *why* not *what*
|
||||||
|
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||||
|
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc are pre-approved; anything else needs justification in the commit message
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
Three machines on Tailscale:
|
||||||
|
|
||||||
|
| Machine | Role | Key specs |
|
||||||
|
|---------|------|-----------|
|
||||||
|
| koala | GPU inference, heavy compute | RTX 5070, runs llama-swap, Qdrant |
|
||||||
|
| iguana | Services, builds | M2 Ultra Mac |
|
||||||
|
| flamingo | Daily driver, edge | Mac mini, ~/dev is here |
|
||||||
|
|
||||||
|
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||||
|
- **Orchestration**: k3s cluster across all three machines
|
||||||
|
- **Networking**: Tailscale mesh
|
||||||
|
|
||||||
|
## Project landscape
|
||||||
|
|
||||||
|
All development repos live at `~/dev/` (softlink from `~/Documents/local-dev/`).
|
||||||
|
|
||||||
|
Organized in thematic folders:
|
||||||
|
|
||||||
|
| Folder | Focus | Count |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| `GO/` | Go web frameworks, API integrations, learning projects | ~10 |
|
||||||
|
| `AI/` | ML research, AI frameworks (FinRL, DSPy, crawl4ai) | ~6 |
|
||||||
|
| `AGENTS/` | Autonomous agents, coding agents, MCP servers, infra | ~15 |
|
||||||
|
| `QKX/` | Invoice processing, financial automation, payment systems | ~13 |
|
||||||
|
| `XT/` | Climate data, sustainability (Klimatkollen, Garbo) | ~2 |
|
||||||
|
|
||||||
|
See `~/dev/PROJECT_SUMMARY.md` for detailed descriptions of each project.
|
||||||
|
|
||||||
|
### Key active projects
|
||||||
|
|
||||||
|
- **super-koala** (`AGENTS/`) — multi-component agent stack with LangGraph, DSPy, MCP
|
||||||
|
- **azure-tiger** (`QKX/`) — invoice extraction → ISO 20022 payment instructions
|
||||||
|
- **gocrwl** (`AGENTS/`) — Go web crawler with containerized deployment
|
||||||
|
- **koala-ai-stack** (`AGENTS/`) — local AI server infrastructure management
|
||||||
|
- **klimatkollen** (`XT/`) — Swedish municipal climate data platform
|
||||||
|
|
||||||
|
## Knowledge base
|
||||||
|
|
||||||
|
When available, agents can query the shared knowledge base:
|
||||||
|
|
||||||
|
- **MCP**: `mcp://hyperguild.<TAILNET>.ts.net:3100/knowledge`
|
||||||
|
- **HTTP**: `http://hyperguild.<TAILNET>.ts.net:3100/api/v1/search`
|
||||||
|
|
||||||
|
<!-- TODO: replace <TAILNET> placeholder with the real Tailscale tailnet
|
||||||
|
name once hyperguild is deployed. Until then, agents that try to
|
||||||
|
reach the knowledge service on a host where it isn't running will
|
||||||
|
get DNS NXDOMAIN, which is the desired fail-loudly behavior. -->
|
||||||
|
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`
|
||||||
|
|
||||||
|
## Client work rules
|
||||||
|
|
||||||
|
When working on a project tagged with a client name:
|
||||||
|
1. Never send code, data, or context to cloud APIs — use local models only
|
||||||
|
2. Never reference other client projects or their data
|
||||||
|
3. Keep all artifacts within the client's git org / directory
|
||||||
|
4. Treat everything as confidential unless told otherwise
|
||||||
|
|
||||||
|
## Harness-agnostic principles
|
||||||
|
|
||||||
|
This context is designed to work with any AI coding tool:
|
||||||
|
- Claude Code, Cursor, Aider, Open WebUI, Charmbracelet Mods/Crush
|
||||||
|
- Pi Coding Agent, Mistral Vibe, Antigravity
|
||||||
|
- Any tool that accepts a system prompt or reads a markdown context file
|
||||||
|
|
||||||
|
The canonical source is always `.context/AGENT.md` (root) and `.context/PROJECT.md` (per-project).
|
||||||
|
Derived files are committed (see *How context propagates* below) so a `git pull` on any host yields full agent context with no setup.
|
||||||
|
|
||||||
|
## How context propagates
|
||||||
|
|
||||||
|
Canonical sources of truth:
|
||||||
|
- Universal: `~/dev/.context/AGENT.md` (this file)
|
||||||
|
- Project: `<repo>/.context/PROJECT.md` (per-repo)
|
||||||
|
|
||||||
|
Derived files (committed, regenerated by `task context:sync`):
|
||||||
|
- `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.aider.conventions.md`,
|
||||||
|
`.context/system-prompt.txt`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Edit a canonical file. Run `task context:sync`. Commit canonical and
|
||||||
|
derived together. Push.
|
||||||
|
2. On any other host, `git pull` brings both. Claude Code (tree-walking)
|
||||||
|
uses `CLAUDE.md`; Crush / Pi / Antigravity (cwd-only) use `AGENTS.md`;
|
||||||
|
Cursor uses `.cursorrules`; Aider uses `.aider.conventions.md`.
|
||||||
|
3. `task check` runs `context:sync` then asserts `git status --porcelain`
|
||||||
|
is empty over the derived files (catches both modified-tracked drift
|
||||||
|
and missing-untracked adapters). A drift fails the check with a
|
||||||
|
message telling you to stage the regenerated files.
|
||||||
|
|
||||||
|
Behavior rules in this file and per-project rules in `PROJECT.md` apply
|
||||||
|
unconditionally on every host, every harness.
|
||||||
|
|
||||||
|
## Engineering Skills
|
||||||
|
|
||||||
|
Shared engineering skills are available in `~/dev/.skills/`. Load on demand via the index.
|
||||||
|
|
||||||
|
See `~/dev/.skills/SKILLS_INDEX.md` for the full list with descriptions and "use when" triggers.
|
||||||
|
|
||||||
|
Key skills:
|
||||||
|
- **TDD**: always write tests first — load `tdd` skill
|
||||||
|
- **Code Review**: load `code-review` skill before any review
|
||||||
|
- **SOLID/Clean Code**: load `solid` or `clean-code` skill for design work
|
||||||
|
- **Problem first**: load `problem-analysis` skill before coding non-trivial features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project context
|
||||||
|
|
||||||
|
<!-- Canonical project context. Edit this, run `task context:sync`.
|
||||||
|
Root agent context from ~/dev/.context/AGENT.md is automatically
|
||||||
|
prepended for harnesses that don't walk the directory tree. -->
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: supervisor
|
||||||
|
- **Owner**: Mathias
|
||||||
|
- **Client**: personal
|
||||||
|
- **Repo**:
|
||||||
|
- **Status**: active
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Primary language**: Go
|
||||||
|
- **UI layer**: HTMX + Templ (when applicable)
|
||||||
|
- **Fallback languages**: Python, TypeScript (justify in PR if used)
|
||||||
|
- **Build**: Task (taskfile.dev), not Make
|
||||||
|
- **Containers**: Docker (compose for dev, k3s for deploy)
|
||||||
|
- **Target infra**: koala (GPU workloads), iguana (services), flamingo (edge)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
- Go: follow `golines`, `gofumpt`, `golangci-lint` with project config
|
||||||
|
- Tests: table-driven, in `_test.go` next to source, `testify` for assertions
|
||||||
|
- Errors: wrap with `fmt.Errorf("operation: %w", err)`, no naked returns
|
||||||
|
- Naming: stdlib conventions, no stuttering (`http.Client` not `http.HTTPClient`)
|
||||||
|
|
||||||
|
### Architecture preferences
|
||||||
|
- Prefer standard library over frameworks (net/http over gin/echo)
|
||||||
|
- Dependency injection via constructor functions, not containers
|
||||||
|
- Configuration via environment variables, parsed at startup into a typed struct
|
||||||
|
- Structured logging via `slog`
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`
|
||||||
|
- Branch naming: `feat/short-description`, `fix/short-description`
|
||||||
|
- PRs: one concern per PR, description explains *why* not *what*
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- No secrets in code, ever — use env vars or SOPS-encrypted files
|
||||||
|
- Client data never leaves local network unless explicitly cleared
|
||||||
|
- Dependencies: audit with `govulncheck` before adding
|
||||||
|
|
||||||
|
## Knowledge base access
|
||||||
|
|
||||||
|
This project can query the shared knowledge base via MCP or HTTP:
|
||||||
|
|
||||||
|
- **MCP endpoint**: `mcp://localhost:3100/knowledge`
|
||||||
|
- **HTTP fallback**: `http://localhost:3100/api/v1/search`
|
||||||
|
- **Scoping**: queries are filtered to collection `personal` + `public`
|
||||||
|
|
||||||
|
## Agent instructions
|
||||||
|
|
||||||
|
When acting as a coding agent on this project:
|
||||||
|
|
||||||
|
1. Read this file and all `SKILL.md` files in `.skills/` before starting work
|
||||||
|
2. Run `task check` before committing (lint + test + vet)
|
||||||
|
3. If unsure about a convention, check `DECISIONS.md` or ask
|
||||||
|
4. Never modify files outside the project root without explicit permission
|
||||||
|
5. When adding a dependency, explain why in the commit message
|
||||||
|
6. For client projects: never send code or context to cloud APIs — use local models via LiteLLM
|
||||||
243
.context/system-prompt.txt
Normal file
243
.context/system-prompt.txt
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
You are a coding assistant working on a specific project.
|
||||||
|
Follow all conventions from both the root agent context and project context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent context — Mathias workspace
|
||||||
|
|
||||||
|
<!-- Canonical root context for all AI coding agents.
|
||||||
|
Lives at: ~/dev/.context/AGENT.md
|
||||||
|
Applies to every project under ~/dev/ unless overridden.
|
||||||
|
|
||||||
|
Run `task context:sync` from ~/dev/ to regenerate harness-specific files.
|
||||||
|
Project-level context in .context/PROJECT.md layers on top of this. -->
|
||||||
|
|
||||||
|
## Who I am
|
||||||
|
|
||||||
|
I'm Mathias, a digital product manager and technology consultant based in Sweden.
|
||||||
|
I build software, research emerging tech, and deliver consulting engagements
|
||||||
|
for clients under NDA. I work across AI/ML, financial automation, web applications,
|
||||||
|
and climate/sustainability tech.
|
||||||
|
|
||||||
|
## How I work with agents
|
||||||
|
|
||||||
|
- I think like a product manager — I care about *why* before *how*
|
||||||
|
- I want agents to be opinionated and push back, not just execute blindly
|
||||||
|
- I prefer concise responses; skip ceremony and get to the point
|
||||||
|
- When I say "build this", I mean production-quality with tests, not a demo
|
||||||
|
- Ask me before making irreversible changes or adding heavy dependencies
|
||||||
|
- I work with confidential client data — never send it to cloud APIs unless I explicitly say it's OK
|
||||||
|
|
||||||
|
## Behavior rules
|
||||||
|
|
||||||
|
These rules apply to every task across every project, regardless of harness.
|
||||||
|
|
||||||
|
1. **No assumptions.** Don't hide confusion — surface it. Surface tradeoffs explicitly.
|
||||||
|
Think before coding; if the problem is unclear, ask or state assumptions before acting.
|
||||||
|
2. **Minimum viable code.** Solve with the smallest change that works. Nothing
|
||||||
|
speculative, no "while we're here" cleanups, no premature abstractions. Simplicity first.
|
||||||
|
3. **Surgical changes.** Touch only what the task requires. Leave unrelated code,
|
||||||
|
files, and formatting alone. Diffs should be small and reviewable.
|
||||||
|
4. **Goal-driven execution.** Define clear success criteria up front for every task.
|
||||||
|
Loop — implement, verify, refine — until those criteria are met. Don't claim
|
||||||
|
completion without evidence (tests pass, command output, observed behavior).
|
||||||
|
|
||||||
|
## Default stack
|
||||||
|
|
||||||
|
| Layer | Default | Fallback | Last resort |
|
||||||
|
|-------|---------|----------|-------------|
|
||||||
|
| Language | Go | Python | TypeScript, Java, C |
|
||||||
|
| UI | HTMX + Templ | Server-rendered HTML | React (only if SPA is justified) |
|
||||||
|
| Build | Task (taskfile.dev) | Make | — |
|
||||||
|
| Containers | Docker Compose (dev), k3s (prod) | — | — |
|
||||||
|
| DB | PostgreSQL + sqlc | SQLite | — |
|
||||||
|
| Search | Qdrant (vector), BM25 | — | — |
|
||||||
|
| Logging | slog (structured) | — | — |
|
||||||
|
| Testing | Table-driven, testify | — | — |
|
||||||
|
|
||||||
|
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- **Go style**: golines, gofumpt, golangci-lint
|
||||||
|
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return
|
||||||
|
- **Naming**: stdlib conventions, no stuttering
|
||||||
|
- **Architecture**: prefer stdlib over frameworks, constructor injection, env-var config parsed into typed structs
|
||||||
|
- **Git**: conventional commits (`feat:`, `fix:`, `chore:`), one concern per PR, PR describes *why* not *what*
|
||||||
|
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||||
|
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc are pre-approved; anything else needs justification in the commit message
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
Three machines on Tailscale:
|
||||||
|
|
||||||
|
| Machine | Role | Key specs |
|
||||||
|
|---------|------|-----------|
|
||||||
|
| koala | GPU inference, heavy compute | RTX 5070, runs llama-swap, Qdrant |
|
||||||
|
| iguana | Services, builds | M2 Ultra Mac |
|
||||||
|
| flamingo | Daily driver, edge | Mac mini, ~/dev is here |
|
||||||
|
|
||||||
|
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||||
|
- **Orchestration**: k3s cluster across all three machines
|
||||||
|
- **Networking**: Tailscale mesh
|
||||||
|
|
||||||
|
## Project landscape
|
||||||
|
|
||||||
|
All development repos live at `~/dev/` (softlink from `~/Documents/local-dev/`).
|
||||||
|
|
||||||
|
Organized in thematic folders:
|
||||||
|
|
||||||
|
| Folder | Focus | Count |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| `GO/` | Go web frameworks, API integrations, learning projects | ~10 |
|
||||||
|
| `AI/` | ML research, AI frameworks (FinRL, DSPy, crawl4ai) | ~6 |
|
||||||
|
| `AGENTS/` | Autonomous agents, coding agents, MCP servers, infra | ~15 |
|
||||||
|
| `QKX/` | Invoice processing, financial automation, payment systems | ~13 |
|
||||||
|
| `XT/` | Climate data, sustainability (Klimatkollen, Garbo) | ~2 |
|
||||||
|
|
||||||
|
See `~/dev/PROJECT_SUMMARY.md` for detailed descriptions of each project.
|
||||||
|
|
||||||
|
### Key active projects
|
||||||
|
|
||||||
|
- **super-koala** (`AGENTS/`) — multi-component agent stack with LangGraph, DSPy, MCP
|
||||||
|
- **azure-tiger** (`QKX/`) — invoice extraction → ISO 20022 payment instructions
|
||||||
|
- **gocrwl** (`AGENTS/`) — Go web crawler with containerized deployment
|
||||||
|
- **koala-ai-stack** (`AGENTS/`) — local AI server infrastructure management
|
||||||
|
- **klimatkollen** (`XT/`) — Swedish municipal climate data platform
|
||||||
|
|
||||||
|
## Knowledge base
|
||||||
|
|
||||||
|
When available, agents can query the shared knowledge base:
|
||||||
|
|
||||||
|
- **MCP**: `mcp://hyperguild.<TAILNET>.ts.net:3100/knowledge`
|
||||||
|
- **HTTP**: `http://hyperguild.<TAILNET>.ts.net:3100/api/v1/search`
|
||||||
|
|
||||||
|
<!-- TODO: replace <TAILNET> placeholder with the real Tailscale tailnet
|
||||||
|
name once hyperguild is deployed. Until then, agents that try to
|
||||||
|
reach the knowledge service on a host where it isn't running will
|
||||||
|
get DNS NXDOMAIN, which is the desired fail-loudly behavior. -->
|
||||||
|
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`
|
||||||
|
|
||||||
|
## Client work rules
|
||||||
|
|
||||||
|
When working on a project tagged with a client name:
|
||||||
|
1. Never send code, data, or context to cloud APIs — use local models only
|
||||||
|
2. Never reference other client projects or their data
|
||||||
|
3. Keep all artifacts within the client's git org / directory
|
||||||
|
4. Treat everything as confidential unless told otherwise
|
||||||
|
|
||||||
|
## Harness-agnostic principles
|
||||||
|
|
||||||
|
This context is designed to work with any AI coding tool:
|
||||||
|
- Claude Code, Cursor, Aider, Open WebUI, Charmbracelet Mods/Crush
|
||||||
|
- Pi Coding Agent, Mistral Vibe, Antigravity
|
||||||
|
- Any tool that accepts a system prompt or reads a markdown context file
|
||||||
|
|
||||||
|
The canonical source is always `.context/AGENT.md` (root) and `.context/PROJECT.md` (per-project).
|
||||||
|
Derived files are committed (see *How context propagates* below) so a `git pull` on any host yields full agent context with no setup.
|
||||||
|
|
||||||
|
## How context propagates
|
||||||
|
|
||||||
|
Canonical sources of truth:
|
||||||
|
- Universal: `~/dev/.context/AGENT.md` (this file)
|
||||||
|
- Project: `<repo>/.context/PROJECT.md` (per-repo)
|
||||||
|
|
||||||
|
Derived files (committed, regenerated by `task context:sync`):
|
||||||
|
- `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.aider.conventions.md`,
|
||||||
|
`.context/system-prompt.txt`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Edit a canonical file. Run `task context:sync`. Commit canonical and
|
||||||
|
derived together. Push.
|
||||||
|
2. On any other host, `git pull` brings both. Claude Code (tree-walking)
|
||||||
|
uses `CLAUDE.md`; Crush / Pi / Antigravity (cwd-only) use `AGENTS.md`;
|
||||||
|
Cursor uses `.cursorrules`; Aider uses `.aider.conventions.md`.
|
||||||
|
3. `task check` runs `context:sync` then asserts `git status --porcelain`
|
||||||
|
is empty over the derived files (catches both modified-tracked drift
|
||||||
|
and missing-untracked adapters). A drift fails the check with a
|
||||||
|
message telling you to stage the regenerated files.
|
||||||
|
|
||||||
|
Behavior rules in this file and per-project rules in `PROJECT.md` apply
|
||||||
|
unconditionally on every host, every harness.
|
||||||
|
|
||||||
|
## Engineering Skills
|
||||||
|
|
||||||
|
Shared engineering skills are available in `~/dev/.skills/`. Load on demand via the index.
|
||||||
|
|
||||||
|
See `~/dev/.skills/SKILLS_INDEX.md` for the full list with descriptions and "use when" triggers.
|
||||||
|
|
||||||
|
Key skills:
|
||||||
|
- **TDD**: always write tests first — load `tdd` skill
|
||||||
|
- **Code Review**: load `code-review` skill before any review
|
||||||
|
- **SOLID/Clean Code**: load `solid` or `clean-code` skill for design work
|
||||||
|
- **Problem first**: load `problem-analysis` skill before coding non-trivial features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project context
|
||||||
|
|
||||||
|
<!-- Canonical project context. Edit this, run `task context:sync`.
|
||||||
|
Root agent context from ~/dev/.context/AGENT.md is automatically
|
||||||
|
prepended for harnesses that don't walk the directory tree. -->
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: supervisor
|
||||||
|
- **Owner**: Mathias
|
||||||
|
- **Client**: personal
|
||||||
|
- **Repo**:
|
||||||
|
- **Status**: active
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Primary language**: Go
|
||||||
|
- **UI layer**: HTMX + Templ (when applicable)
|
||||||
|
- **Fallback languages**: Python, TypeScript (justify in PR if used)
|
||||||
|
- **Build**: Task (taskfile.dev), not Make
|
||||||
|
- **Containers**: Docker (compose for dev, k3s for deploy)
|
||||||
|
- **Target infra**: koala (GPU workloads), iguana (services), flamingo (edge)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
- Go: follow `golines`, `gofumpt`, `golangci-lint` with project config
|
||||||
|
- Tests: table-driven, in `_test.go` next to source, `testify` for assertions
|
||||||
|
- Errors: wrap with `fmt.Errorf("operation: %w", err)`, no naked returns
|
||||||
|
- Naming: stdlib conventions, no stuttering (`http.Client` not `http.HTTPClient`)
|
||||||
|
|
||||||
|
### Architecture preferences
|
||||||
|
- Prefer standard library over frameworks (net/http over gin/echo)
|
||||||
|
- Dependency injection via constructor functions, not containers
|
||||||
|
- Configuration via environment variables, parsed at startup into a typed struct
|
||||||
|
- Structured logging via `slog`
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`
|
||||||
|
- Branch naming: `feat/short-description`, `fix/short-description`
|
||||||
|
- PRs: one concern per PR, description explains *why* not *what*
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- No secrets in code, ever — use env vars or SOPS-encrypted files
|
||||||
|
- Client data never leaves local network unless explicitly cleared
|
||||||
|
- Dependencies: audit with `govulncheck` before adding
|
||||||
|
|
||||||
|
## Knowledge base access
|
||||||
|
|
||||||
|
This project can query the shared knowledge base via MCP or HTTP:
|
||||||
|
|
||||||
|
- **MCP endpoint**: `mcp://localhost:3100/knowledge`
|
||||||
|
- **HTTP fallback**: `http://localhost:3100/api/v1/search`
|
||||||
|
- **Scoping**: queries are filtered to collection `personal` + `public`
|
||||||
|
|
||||||
|
## Agent instructions
|
||||||
|
|
||||||
|
When acting as a coding agent on this project:
|
||||||
|
|
||||||
|
1. Read this file and all `SKILL.md` files in `.skills/` before starting work
|
||||||
|
2. Run `task check` before committing (lint + test + vet)
|
||||||
|
3. If unsure about a convention, check `DECISIONS.md` or ask
|
||||||
|
4. Never modify files outside the project root without explicit permission
|
||||||
|
5. When adding a dependency, explain why in the commit message
|
||||||
|
6. For client projects: never send code or context to cloud APIs — use local models via LiteLLM
|
||||||
|
|
||||||
|
---
|
||||||
239
.cursorrules
Normal file
239
.cursorrules
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Cursor rules — auto-generated
|
||||||
|
# Do not edit. Run: task context:sync
|
||||||
|
|
||||||
|
# Agent context — Mathias workspace
|
||||||
|
|
||||||
|
<!-- Canonical root context for all AI coding agents.
|
||||||
|
Lives at: ~/dev/.context/AGENT.md
|
||||||
|
Applies to every project under ~/dev/ unless overridden.
|
||||||
|
|
||||||
|
Run `task context:sync` from ~/dev/ to regenerate harness-specific files.
|
||||||
|
Project-level context in .context/PROJECT.md layers on top of this. -->
|
||||||
|
|
||||||
|
## Who I am
|
||||||
|
|
||||||
|
I'm Mathias, a digital product manager and technology consultant based in Sweden.
|
||||||
|
I build software, research emerging tech, and deliver consulting engagements
|
||||||
|
for clients under NDA. I work across AI/ML, financial automation, web applications,
|
||||||
|
and climate/sustainability tech.
|
||||||
|
|
||||||
|
## How I work with agents
|
||||||
|
|
||||||
|
- I think like a product manager — I care about *why* before *how*
|
||||||
|
- I want agents to be opinionated and push back, not just execute blindly
|
||||||
|
- I prefer concise responses; skip ceremony and get to the point
|
||||||
|
- When I say "build this", I mean production-quality with tests, not a demo
|
||||||
|
- Ask me before making irreversible changes or adding heavy dependencies
|
||||||
|
- I work with confidential client data — never send it to cloud APIs unless I explicitly say it's OK
|
||||||
|
|
||||||
|
## Behavior rules
|
||||||
|
|
||||||
|
These rules apply to every task across every project, regardless of harness.
|
||||||
|
|
||||||
|
1. **No assumptions.** Don't hide confusion — surface it. Surface tradeoffs explicitly.
|
||||||
|
Think before coding; if the problem is unclear, ask or state assumptions before acting.
|
||||||
|
2. **Minimum viable code.** Solve with the smallest change that works. Nothing
|
||||||
|
speculative, no "while we're here" cleanups, no premature abstractions. Simplicity first.
|
||||||
|
3. **Surgical changes.** Touch only what the task requires. Leave unrelated code,
|
||||||
|
files, and formatting alone. Diffs should be small and reviewable.
|
||||||
|
4. **Goal-driven execution.** Define clear success criteria up front for every task.
|
||||||
|
Loop — implement, verify, refine — until those criteria are met. Don't claim
|
||||||
|
completion without evidence (tests pass, command output, observed behavior).
|
||||||
|
|
||||||
|
## Default stack
|
||||||
|
|
||||||
|
| Layer | Default | Fallback | Last resort |
|
||||||
|
|-------|---------|----------|-------------|
|
||||||
|
| Language | Go | Python | TypeScript, Java, C |
|
||||||
|
| UI | HTMX + Templ | Server-rendered HTML | React (only if SPA is justified) |
|
||||||
|
| Build | Task (taskfile.dev) | Make | — |
|
||||||
|
| Containers | Docker Compose (dev), k3s (prod) | — | — |
|
||||||
|
| DB | PostgreSQL + sqlc | SQLite | — |
|
||||||
|
| Search | Qdrant (vector), BM25 | — | — |
|
||||||
|
| Logging | slog (structured) | — | — |
|
||||||
|
| Testing | Table-driven, testify | — | — |
|
||||||
|
|
||||||
|
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- **Go style**: golines, gofumpt, golangci-lint
|
||||||
|
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return
|
||||||
|
- **Naming**: stdlib conventions, no stuttering
|
||||||
|
- **Architecture**: prefer stdlib over frameworks, constructor injection, env-var config parsed into typed structs
|
||||||
|
- **Git**: conventional commits (`feat:`, `fix:`, `chore:`), one concern per PR, PR describes *why* not *what*
|
||||||
|
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||||
|
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc are pre-approved; anything else needs justification in the commit message
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
Three machines on Tailscale:
|
||||||
|
|
||||||
|
| Machine | Role | Key specs |
|
||||||
|
|---------|------|-----------|
|
||||||
|
| koala | GPU inference, heavy compute | RTX 5070, runs llama-swap, Qdrant |
|
||||||
|
| iguana | Services, builds | M2 Ultra Mac |
|
||||||
|
| flamingo | Daily driver, edge | Mac mini, ~/dev is here |
|
||||||
|
|
||||||
|
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||||
|
- **Orchestration**: k3s cluster across all three machines
|
||||||
|
- **Networking**: Tailscale mesh
|
||||||
|
|
||||||
|
## Project landscape
|
||||||
|
|
||||||
|
All development repos live at `~/dev/` (softlink from `~/Documents/local-dev/`).
|
||||||
|
|
||||||
|
Organized in thematic folders:
|
||||||
|
|
||||||
|
| Folder | Focus | Count |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| `GO/` | Go web frameworks, API integrations, learning projects | ~10 |
|
||||||
|
| `AI/` | ML research, AI frameworks (FinRL, DSPy, crawl4ai) | ~6 |
|
||||||
|
| `AGENTS/` | Autonomous agents, coding agents, MCP servers, infra | ~15 |
|
||||||
|
| `QKX/` | Invoice processing, financial automation, payment systems | ~13 |
|
||||||
|
| `XT/` | Climate data, sustainability (Klimatkollen, Garbo) | ~2 |
|
||||||
|
|
||||||
|
See `~/dev/PROJECT_SUMMARY.md` for detailed descriptions of each project.
|
||||||
|
|
||||||
|
### Key active projects
|
||||||
|
|
||||||
|
- **super-koala** (`AGENTS/`) — multi-component agent stack with LangGraph, DSPy, MCP
|
||||||
|
- **azure-tiger** (`QKX/`) — invoice extraction → ISO 20022 payment instructions
|
||||||
|
- **gocrwl** (`AGENTS/`) — Go web crawler with containerized deployment
|
||||||
|
- **koala-ai-stack** (`AGENTS/`) — local AI server infrastructure management
|
||||||
|
- **klimatkollen** (`XT/`) — Swedish municipal climate data platform
|
||||||
|
|
||||||
|
## Knowledge base
|
||||||
|
|
||||||
|
When available, agents can query the shared knowledge base:
|
||||||
|
|
||||||
|
- **MCP**: `mcp://hyperguild.<TAILNET>.ts.net:3100/knowledge`
|
||||||
|
- **HTTP**: `http://hyperguild.<TAILNET>.ts.net:3100/api/v1/search`
|
||||||
|
|
||||||
|
<!-- TODO: replace <TAILNET> placeholder with the real Tailscale tailnet
|
||||||
|
name once hyperguild is deployed. Until then, agents that try to
|
||||||
|
reach the knowledge service on a host where it isn't running will
|
||||||
|
get DNS NXDOMAIN, which is the desired fail-loudly behavior. -->
|
||||||
|
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`
|
||||||
|
|
||||||
|
## Client work rules
|
||||||
|
|
||||||
|
When working on a project tagged with a client name:
|
||||||
|
1. Never send code, data, or context to cloud APIs — use local models only
|
||||||
|
2. Never reference other client projects or their data
|
||||||
|
3. Keep all artifacts within the client's git org / directory
|
||||||
|
4. Treat everything as confidential unless told otherwise
|
||||||
|
|
||||||
|
## Harness-agnostic principles
|
||||||
|
|
||||||
|
This context is designed to work with any AI coding tool:
|
||||||
|
- Claude Code, Cursor, Aider, Open WebUI, Charmbracelet Mods/Crush
|
||||||
|
- Pi Coding Agent, Mistral Vibe, Antigravity
|
||||||
|
- Any tool that accepts a system prompt or reads a markdown context file
|
||||||
|
|
||||||
|
The canonical source is always `.context/AGENT.md` (root) and `.context/PROJECT.md` (per-project).
|
||||||
|
Derived files are committed (see *How context propagates* below) so a `git pull` on any host yields full agent context with no setup.
|
||||||
|
|
||||||
|
## How context propagates
|
||||||
|
|
||||||
|
Canonical sources of truth:
|
||||||
|
- Universal: `~/dev/.context/AGENT.md` (this file)
|
||||||
|
- Project: `<repo>/.context/PROJECT.md` (per-repo)
|
||||||
|
|
||||||
|
Derived files (committed, regenerated by `task context:sync`):
|
||||||
|
- `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.aider.conventions.md`,
|
||||||
|
`.context/system-prompt.txt`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Edit a canonical file. Run `task context:sync`. Commit canonical and
|
||||||
|
derived together. Push.
|
||||||
|
2. On any other host, `git pull` brings both. Claude Code (tree-walking)
|
||||||
|
uses `CLAUDE.md`; Crush / Pi / Antigravity (cwd-only) use `AGENTS.md`;
|
||||||
|
Cursor uses `.cursorrules`; Aider uses `.aider.conventions.md`.
|
||||||
|
3. `task check` runs `context:sync` then asserts `git status --porcelain`
|
||||||
|
is empty over the derived files (catches both modified-tracked drift
|
||||||
|
and missing-untracked adapters). A drift fails the check with a
|
||||||
|
message telling you to stage the regenerated files.
|
||||||
|
|
||||||
|
Behavior rules in this file and per-project rules in `PROJECT.md` apply
|
||||||
|
unconditionally on every host, every harness.
|
||||||
|
|
||||||
|
## Engineering Skills
|
||||||
|
|
||||||
|
Shared engineering skills are available in `~/dev/.skills/`. Load on demand via the index.
|
||||||
|
|
||||||
|
See `~/dev/.skills/SKILLS_INDEX.md` for the full list with descriptions and "use when" triggers.
|
||||||
|
|
||||||
|
Key skills:
|
||||||
|
- **TDD**: always write tests first — load `tdd` skill
|
||||||
|
- **Code Review**: load `code-review` skill before any review
|
||||||
|
- **SOLID/Clean Code**: load `solid` or `clean-code` skill for design work
|
||||||
|
- **Problem first**: load `problem-analysis` skill before coding non-trivial features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project context
|
||||||
|
|
||||||
|
<!-- Canonical project context. Edit this, run `task context:sync`.
|
||||||
|
Root agent context from ~/dev/.context/AGENT.md is automatically
|
||||||
|
prepended for harnesses that don't walk the directory tree. -->
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: supervisor
|
||||||
|
- **Owner**: Mathias
|
||||||
|
- **Client**: personal
|
||||||
|
- **Repo**:
|
||||||
|
- **Status**: active
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Primary language**: Go
|
||||||
|
- **UI layer**: HTMX + Templ (when applicable)
|
||||||
|
- **Fallback languages**: Python, TypeScript (justify in PR if used)
|
||||||
|
- **Build**: Task (taskfile.dev), not Make
|
||||||
|
- **Containers**: Docker (compose for dev, k3s for deploy)
|
||||||
|
- **Target infra**: koala (GPU workloads), iguana (services), flamingo (edge)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
- Go: follow `golines`, `gofumpt`, `golangci-lint` with project config
|
||||||
|
- Tests: table-driven, in `_test.go` next to source, `testify` for assertions
|
||||||
|
- Errors: wrap with `fmt.Errorf("operation: %w", err)`, no naked returns
|
||||||
|
- Naming: stdlib conventions, no stuttering (`http.Client` not `http.HTTPClient`)
|
||||||
|
|
||||||
|
### Architecture preferences
|
||||||
|
- Prefer standard library over frameworks (net/http over gin/echo)
|
||||||
|
- Dependency injection via constructor functions, not containers
|
||||||
|
- Configuration via environment variables, parsed at startup into a typed struct
|
||||||
|
- Structured logging via `slog`
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`
|
||||||
|
- Branch naming: `feat/short-description`, `fix/short-description`
|
||||||
|
- PRs: one concern per PR, description explains *why* not *what*
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- No secrets in code, ever — use env vars or SOPS-encrypted files
|
||||||
|
- Client data never leaves local network unless explicitly cleared
|
||||||
|
- Dependencies: audit with `govulncheck` before adding
|
||||||
|
|
||||||
|
## Knowledge base access
|
||||||
|
|
||||||
|
This project can query the shared knowledge base via MCP or HTTP:
|
||||||
|
|
||||||
|
- **MCP endpoint**: `mcp://localhost:3100/knowledge`
|
||||||
|
- **HTTP fallback**: `http://localhost:3100/api/v1/search`
|
||||||
|
- **Scoping**: queries are filtered to collection `personal` + `public`
|
||||||
|
|
||||||
|
## Agent instructions
|
||||||
|
|
||||||
|
When acting as a coding agent on this project:
|
||||||
|
|
||||||
|
1. Read this file and all `SKILL.md` files in `.skills/` before starting work
|
||||||
|
2. Run `task check` before committing (lint + test + vet)
|
||||||
|
3. If unsure about a convention, check `DECISIONS.md` or ask
|
||||||
|
4. Never modify files outside the project root without explicit permission
|
||||||
|
5. When adding a dependency, explain why in the commit message
|
||||||
|
6. For client projects: never send code or context to cloud APIs — use local models via LiteLLM
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -13,15 +13,7 @@ brain/training-data/**/*.jsonl
|
|||||||
# Go
|
# Go
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
# ── Generated context files (adapter outputs) ──
|
|
||||||
# Canonical sources: .context/PROJECT.md + .skills/*/SKILL.md
|
|
||||||
# Everything below is disposable — regenerate with: task context:sync
|
|
||||||
AGENTS.md
|
|
||||||
CLAUDE.md
|
|
||||||
.cursorrules
|
|
||||||
.aider.conventions.md
|
|
||||||
.aider.conf.yml
|
.aider.conf.yml
|
||||||
.context/system-prompt.txt
|
|
||||||
|
|
||||||
# ── Sensitive ──
|
# ── Sensitive ──
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"supervisor": {
|
"supervisor": {
|
||||||
"command": "/Users/mathias/dev/AI/supervisor/bin/supervisor-bridge",
|
"type": "http",
|
||||||
"env": {
|
"url": "http://koala:30320/mcp"
|
||||||
"SUPERVISOR_URL": "http://koala:30320/mcp"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
236
AGENTS.md
Normal file
236
AGENTS.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Agent context — Mathias workspace
|
||||||
|
|
||||||
|
<!-- Canonical root context for all AI coding agents.
|
||||||
|
Lives at: ~/dev/.context/AGENT.md
|
||||||
|
Applies to every project under ~/dev/ unless overridden.
|
||||||
|
|
||||||
|
Run `task context:sync` from ~/dev/ to regenerate harness-specific files.
|
||||||
|
Project-level context in .context/PROJECT.md layers on top of this. -->
|
||||||
|
|
||||||
|
## Who I am
|
||||||
|
|
||||||
|
I'm Mathias, a digital product manager and technology consultant based in Sweden.
|
||||||
|
I build software, research emerging tech, and deliver consulting engagements
|
||||||
|
for clients under NDA. I work across AI/ML, financial automation, web applications,
|
||||||
|
and climate/sustainability tech.
|
||||||
|
|
||||||
|
## How I work with agents
|
||||||
|
|
||||||
|
- I think like a product manager — I care about *why* before *how*
|
||||||
|
- I want agents to be opinionated and push back, not just execute blindly
|
||||||
|
- I prefer concise responses; skip ceremony and get to the point
|
||||||
|
- When I say "build this", I mean production-quality with tests, not a demo
|
||||||
|
- Ask me before making irreversible changes or adding heavy dependencies
|
||||||
|
- I work with confidential client data — never send it to cloud APIs unless I explicitly say it's OK
|
||||||
|
|
||||||
|
## Behavior rules
|
||||||
|
|
||||||
|
These rules apply to every task across every project, regardless of harness.
|
||||||
|
|
||||||
|
1. **No assumptions.** Don't hide confusion — surface it. Surface tradeoffs explicitly.
|
||||||
|
Think before coding; if the problem is unclear, ask or state assumptions before acting.
|
||||||
|
2. **Minimum viable code.** Solve with the smallest change that works. Nothing
|
||||||
|
speculative, no "while we're here" cleanups, no premature abstractions. Simplicity first.
|
||||||
|
3. **Surgical changes.** Touch only what the task requires. Leave unrelated code,
|
||||||
|
files, and formatting alone. Diffs should be small and reviewable.
|
||||||
|
4. **Goal-driven execution.** Define clear success criteria up front for every task.
|
||||||
|
Loop — implement, verify, refine — until those criteria are met. Don't claim
|
||||||
|
completion without evidence (tests pass, command output, observed behavior).
|
||||||
|
|
||||||
|
## Default stack
|
||||||
|
|
||||||
|
| Layer | Default | Fallback | Last resort |
|
||||||
|
|-------|---------|----------|-------------|
|
||||||
|
| Language | Go | Python | TypeScript, Java, C |
|
||||||
|
| UI | HTMX + Templ | Server-rendered HTML | React (only if SPA is justified) |
|
||||||
|
| Build | Task (taskfile.dev) | Make | — |
|
||||||
|
| Containers | Docker Compose (dev), k3s (prod) | — | — |
|
||||||
|
| DB | PostgreSQL + sqlc | SQLite | — |
|
||||||
|
| Search | Qdrant (vector), BM25 | — | — |
|
||||||
|
| Logging | slog (structured) | — | — |
|
||||||
|
| Testing | Table-driven, testify | — | — |
|
||||||
|
|
||||||
|
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- **Go style**: golines, gofumpt, golangci-lint
|
||||||
|
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return
|
||||||
|
- **Naming**: stdlib conventions, no stuttering
|
||||||
|
- **Architecture**: prefer stdlib over frameworks, constructor injection, env-var config parsed into typed structs
|
||||||
|
- **Git**: conventional commits (`feat:`, `fix:`, `chore:`), one concern per PR, PR describes *why* not *what*
|
||||||
|
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||||
|
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc are pre-approved; anything else needs justification in the commit message
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
Three machines on Tailscale:
|
||||||
|
|
||||||
|
| Machine | Role | Key specs |
|
||||||
|
|---------|------|-----------|
|
||||||
|
| koala | GPU inference, heavy compute | RTX 5070, runs llama-swap, Qdrant |
|
||||||
|
| iguana | Services, builds | M2 Ultra Mac |
|
||||||
|
| flamingo | Daily driver, edge | Mac mini, ~/dev is here |
|
||||||
|
|
||||||
|
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||||
|
- **Orchestration**: k3s cluster across all three machines
|
||||||
|
- **Networking**: Tailscale mesh
|
||||||
|
|
||||||
|
## Project landscape
|
||||||
|
|
||||||
|
All development repos live at `~/dev/` (softlink from `~/Documents/local-dev/`).
|
||||||
|
|
||||||
|
Organized in thematic folders:
|
||||||
|
|
||||||
|
| Folder | Focus | Count |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| `GO/` | Go web frameworks, API integrations, learning projects | ~10 |
|
||||||
|
| `AI/` | ML research, AI frameworks (FinRL, DSPy, crawl4ai) | ~6 |
|
||||||
|
| `AGENTS/` | Autonomous agents, coding agents, MCP servers, infra | ~15 |
|
||||||
|
| `QKX/` | Invoice processing, financial automation, payment systems | ~13 |
|
||||||
|
| `XT/` | Climate data, sustainability (Klimatkollen, Garbo) | ~2 |
|
||||||
|
|
||||||
|
See `~/dev/PROJECT_SUMMARY.md` for detailed descriptions of each project.
|
||||||
|
|
||||||
|
### Key active projects
|
||||||
|
|
||||||
|
- **super-koala** (`AGENTS/`) — multi-component agent stack with LangGraph, DSPy, MCP
|
||||||
|
- **azure-tiger** (`QKX/`) — invoice extraction → ISO 20022 payment instructions
|
||||||
|
- **gocrwl** (`AGENTS/`) — Go web crawler with containerized deployment
|
||||||
|
- **koala-ai-stack** (`AGENTS/`) — local AI server infrastructure management
|
||||||
|
- **klimatkollen** (`XT/`) — Swedish municipal climate data platform
|
||||||
|
|
||||||
|
## Knowledge base
|
||||||
|
|
||||||
|
When available, agents can query the shared knowledge base:
|
||||||
|
|
||||||
|
- **MCP**: `mcp://hyperguild.<TAILNET>.ts.net:3100/knowledge`
|
||||||
|
- **HTTP**: `http://hyperguild.<TAILNET>.ts.net:3100/api/v1/search`
|
||||||
|
|
||||||
|
<!-- TODO: replace <TAILNET> placeholder with the real Tailscale tailnet
|
||||||
|
name once hyperguild is deployed. Until then, agents that try to
|
||||||
|
reach the knowledge service on a host where it isn't running will
|
||||||
|
get DNS NXDOMAIN, which is the desired fail-loudly behavior. -->
|
||||||
|
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`
|
||||||
|
|
||||||
|
## Client work rules
|
||||||
|
|
||||||
|
When working on a project tagged with a client name:
|
||||||
|
1. Never send code, data, or context to cloud APIs — use local models only
|
||||||
|
2. Never reference other client projects or their data
|
||||||
|
3. Keep all artifacts within the client's git org / directory
|
||||||
|
4. Treat everything as confidential unless told otherwise
|
||||||
|
|
||||||
|
## Harness-agnostic principles
|
||||||
|
|
||||||
|
This context is designed to work with any AI coding tool:
|
||||||
|
- Claude Code, Cursor, Aider, Open WebUI, Charmbracelet Mods/Crush
|
||||||
|
- Pi Coding Agent, Mistral Vibe, Antigravity
|
||||||
|
- Any tool that accepts a system prompt or reads a markdown context file
|
||||||
|
|
||||||
|
The canonical source is always `.context/AGENT.md` (root) and `.context/PROJECT.md` (per-project).
|
||||||
|
Derived files are committed (see *How context propagates* below) so a `git pull` on any host yields full agent context with no setup.
|
||||||
|
|
||||||
|
## How context propagates
|
||||||
|
|
||||||
|
Canonical sources of truth:
|
||||||
|
- Universal: `~/dev/.context/AGENT.md` (this file)
|
||||||
|
- Project: `<repo>/.context/PROJECT.md` (per-repo)
|
||||||
|
|
||||||
|
Derived files (committed, regenerated by `task context:sync`):
|
||||||
|
- `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.aider.conventions.md`,
|
||||||
|
`.context/system-prompt.txt`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Edit a canonical file. Run `task context:sync`. Commit canonical and
|
||||||
|
derived together. Push.
|
||||||
|
2. On any other host, `git pull` brings both. Claude Code (tree-walking)
|
||||||
|
uses `CLAUDE.md`; Crush / Pi / Antigravity (cwd-only) use `AGENTS.md`;
|
||||||
|
Cursor uses `.cursorrules`; Aider uses `.aider.conventions.md`.
|
||||||
|
3. `task check` runs `context:sync` then asserts `git status --porcelain`
|
||||||
|
is empty over the derived files (catches both modified-tracked drift
|
||||||
|
and missing-untracked adapters). A drift fails the check with a
|
||||||
|
message telling you to stage the regenerated files.
|
||||||
|
|
||||||
|
Behavior rules in this file and per-project rules in `PROJECT.md` apply
|
||||||
|
unconditionally on every host, every harness.
|
||||||
|
|
||||||
|
## Engineering Skills
|
||||||
|
|
||||||
|
Shared engineering skills are available in `~/dev/.skills/`. Load on demand via the index.
|
||||||
|
|
||||||
|
See `~/dev/.skills/SKILLS_INDEX.md` for the full list with descriptions and "use when" triggers.
|
||||||
|
|
||||||
|
Key skills:
|
||||||
|
- **TDD**: always write tests first — load `tdd` skill
|
||||||
|
- **Code Review**: load `code-review` skill before any review
|
||||||
|
- **SOLID/Clean Code**: load `solid` or `clean-code` skill for design work
|
||||||
|
- **Problem first**: load `problem-analysis` skill before coding non-trivial features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project context
|
||||||
|
|
||||||
|
<!-- Canonical project context. Edit this, run `task context:sync`.
|
||||||
|
Root agent context from ~/dev/.context/AGENT.md is automatically
|
||||||
|
prepended for harnesses that don't walk the directory tree. -->
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: supervisor
|
||||||
|
- **Owner**: Mathias
|
||||||
|
- **Client**: personal
|
||||||
|
- **Repo**:
|
||||||
|
- **Status**: active
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Primary language**: Go
|
||||||
|
- **UI layer**: HTMX + Templ (when applicable)
|
||||||
|
- **Fallback languages**: Python, TypeScript (justify in PR if used)
|
||||||
|
- **Build**: Task (taskfile.dev), not Make
|
||||||
|
- **Containers**: Docker (compose for dev, k3s for deploy)
|
||||||
|
- **Target infra**: koala (GPU workloads), iguana (services), flamingo (edge)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
- Go: follow `golines`, `gofumpt`, `golangci-lint` with project config
|
||||||
|
- Tests: table-driven, in `_test.go` next to source, `testify` for assertions
|
||||||
|
- Errors: wrap with `fmt.Errorf("operation: %w", err)`, no naked returns
|
||||||
|
- Naming: stdlib conventions, no stuttering (`http.Client` not `http.HTTPClient`)
|
||||||
|
|
||||||
|
### Architecture preferences
|
||||||
|
- Prefer standard library over frameworks (net/http over gin/echo)
|
||||||
|
- Dependency injection via constructor functions, not containers
|
||||||
|
- Configuration via environment variables, parsed at startup into a typed struct
|
||||||
|
- Structured logging via `slog`
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`
|
||||||
|
- Branch naming: `feat/short-description`, `fix/short-description`
|
||||||
|
- PRs: one concern per PR, description explains *why* not *what*
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- No secrets in code, ever — use env vars or SOPS-encrypted files
|
||||||
|
- Client data never leaves local network unless explicitly cleared
|
||||||
|
- Dependencies: audit with `govulncheck` before adding
|
||||||
|
|
||||||
|
## Knowledge base access
|
||||||
|
|
||||||
|
This project can query the shared knowledge base via MCP or HTTP:
|
||||||
|
|
||||||
|
- **MCP endpoint**: `mcp://localhost:3100/knowledge`
|
||||||
|
- **HTTP fallback**: `http://localhost:3100/api/v1/search`
|
||||||
|
- **Scoping**: queries are filtered to collection `personal` + `public`
|
||||||
|
|
||||||
|
## Agent instructions
|
||||||
|
|
||||||
|
When acting as a coding agent on this project:
|
||||||
|
|
||||||
|
1. Read this file and all `SKILL.md` files in `.skills/` before starting work
|
||||||
|
2. Run `task check` before committing (lint + test + vet)
|
||||||
|
3. If unsure about a convention, check `DECISIONS.md` or ask
|
||||||
|
4. Never modify files outside the project root without explicit permission
|
||||||
|
5. When adding a dependency, explain why in the commit message
|
||||||
|
6. For client projects: never send code or context to cloud APIs — use local models via LiteLLM
|
||||||
65
CLAUDE.md
Normal file
65
CLAUDE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Project context
|
||||||
|
|
||||||
|
<!-- Canonical project context. Edit this, run `task context:sync`.
|
||||||
|
Root agent context from ~/dev/.context/AGENT.md is automatically
|
||||||
|
prepended for harnesses that don't walk the directory tree. -->
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: supervisor
|
||||||
|
- **Owner**: Mathias
|
||||||
|
- **Client**: personal
|
||||||
|
- **Repo**:
|
||||||
|
- **Status**: active
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Primary language**: Go
|
||||||
|
- **UI layer**: HTMX + Templ (when applicable)
|
||||||
|
- **Fallback languages**: Python, TypeScript (justify in PR if used)
|
||||||
|
- **Build**: Task (taskfile.dev), not Make
|
||||||
|
- **Containers**: Docker (compose for dev, k3s for deploy)
|
||||||
|
- **Target infra**: koala (GPU workloads), iguana (services), flamingo (edge)
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
- Go: follow `golines`, `gofumpt`, `golangci-lint` with project config
|
||||||
|
- Tests: table-driven, in `_test.go` next to source, `testify` for assertions
|
||||||
|
- Errors: wrap with `fmt.Errorf("operation: %w", err)`, no naked returns
|
||||||
|
- Naming: stdlib conventions, no stuttering (`http.Client` not `http.HTTPClient`)
|
||||||
|
|
||||||
|
### Architecture preferences
|
||||||
|
- Prefer standard library over frameworks (net/http over gin/echo)
|
||||||
|
- Dependency injection via constructor functions, not containers
|
||||||
|
- Configuration via environment variables, parsed at startup into a typed struct
|
||||||
|
- Structured logging via `slog`
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`
|
||||||
|
- Branch naming: `feat/short-description`, `fix/short-description`
|
||||||
|
- PRs: one concern per PR, description explains *why* not *what*
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- No secrets in code, ever — use env vars or SOPS-encrypted files
|
||||||
|
- Client data never leaves local network unless explicitly cleared
|
||||||
|
- Dependencies: audit with `govulncheck` before adding
|
||||||
|
|
||||||
|
## Knowledge base access
|
||||||
|
|
||||||
|
This project can query the shared knowledge base via MCP or HTTP:
|
||||||
|
|
||||||
|
- **MCP endpoint**: `mcp://localhost:3100/knowledge`
|
||||||
|
- **HTTP fallback**: `http://localhost:3100/api/v1/search`
|
||||||
|
- **Scoping**: queries are filtered to collection `personal` + `public`
|
||||||
|
|
||||||
|
## Agent instructions
|
||||||
|
|
||||||
|
When acting as a coding agent on this project:
|
||||||
|
|
||||||
|
1. Read this file and all `SKILL.md` files in `.skills/` before starting work
|
||||||
|
2. Run `task check` before committing (lint + test + vet)
|
||||||
|
3. If unsure about a convention, check `DECISIONS.md` or ask
|
||||||
|
4. Never modify files outside the project root without explicit permission
|
||||||
|
5. When adding a dependency, explain why in the commit message
|
||||||
|
6. For client projects: never send code or context to cloud APIs — use local models via LiteLLM
|
||||||
16
README.md
16
README.md
@@ -10,9 +10,9 @@ into a searchable brain.
|
|||||||
```
|
```
|
||||||
Your Claude Code session (in any project)
|
Your Claude Code session (in any project)
|
||||||
│
|
│
|
||||||
│ MCP tools (over stdio bridge → HTTP)
|
│ MCP over HTTP (Tailscale)
|
||||||
▼
|
▼
|
||||||
supervisor :3200 — skill workers: tdd, retrospective
|
supervisor :3200 (NodePort 30320 on koala) — skill workers: tdd, retrospective
|
||||||
ingestion :3300 — brain HTTP API: query wiki, write notes
|
ingestion :3300 — brain HTTP API: query wiki, write notes
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -55,18 +55,18 @@ Create `.mcp.json` in your project root:
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"supervisor": {
|
"supervisor": {
|
||||||
"command": "/Users/mathias/dev/AI/supervisor/bin/supervisor-bridge",
|
"type": "http",
|
||||||
"env": {
|
"url": "http://koala:30320/mcp"
|
||||||
"SUPERVISOR_URL": "http://localhost:3200/mcp"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the bridge binary once: `task bridge:build`
|
The supervisor MCP server is reachable over Tailscale at `koala:30320` (NodePort
|
||||||
|
to the in-cluster service on port 3200). No local binary or stdio shim is
|
||||||
|
required — Claude Code talks to it directly via HTTP.
|
||||||
|
|
||||||
Then open Claude Code in your project — run `/mcp` to confirm `supervisor` is listed.
|
Open Claude Code in your project — run `/mcp` to confirm `supervisor` is listed.
|
||||||
|
|
||||||
## A typical TDD session
|
## A typical TDD session
|
||||||
|
|
||||||
|
|||||||
23
Taskfile.yml
23
Taskfile.yml
@@ -12,9 +12,6 @@ tasks:
|
|||||||
desc: Regenerate all harness-specific context files
|
desc: Regenerate all harness-specific context files
|
||||||
cmds:
|
cmds:
|
||||||
- bash scripts/context-sync.sh
|
- bash scripts/context-sync.sh
|
||||||
sources:
|
|
||||||
- .context/PROJECT.md
|
|
||||||
- .skills/*/SKILL.md
|
|
||||||
|
|
||||||
context:sync:claude:
|
context:sync:claude:
|
||||||
cmds: [bash scripts/context-sync.sh claude]
|
cmds: [bash scripts/context-sync.sh claude]
|
||||||
@@ -57,7 +54,6 @@ tasks:
|
|||||||
desc: Build all binaries
|
desc: Build all binaries
|
||||||
cmds:
|
cmds:
|
||||||
- task: supervisor:build
|
- task: supervisor:build
|
||||||
- task: bridge:build
|
|
||||||
- task: ingestion:build
|
- task: ingestion:build
|
||||||
|
|
||||||
supervisor:build:
|
supervisor:build:
|
||||||
@@ -65,11 +61,6 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- go build -trimpath -ldflags="-s -w -X main.version={{.VERSION}}" -o bin/supervisor ./cmd/supervisor
|
- go build -trimpath -ldflags="-s -w -X main.version={{.VERSION}}" -o bin/supervisor ./cmd/supervisor
|
||||||
|
|
||||||
bridge:build:
|
|
||||||
desc: Build stdio↔HTTP bridge for Claude Code MCP integration
|
|
||||||
cmds:
|
|
||||||
- go build -trimpath -ldflags="-s -w" -o bin/supervisor-bridge ./cmd/bridge
|
|
||||||
|
|
||||||
ingestion:build:
|
ingestion:build:
|
||||||
desc: Build ingestion server binary
|
desc: Build ingestion server binary
|
||||||
dir: ingestion
|
dir: ingestion
|
||||||
@@ -79,8 +70,20 @@ tasks:
|
|||||||
# ── Quality ────────────────────────────────────────────────────────────────
|
# ── Quality ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
check:
|
check:
|
||||||
desc: Run all checks (lint + test + vet) across all modules
|
desc: Run all checks (context freshness + lint + test + vet) across all modules
|
||||||
cmds:
|
cmds:
|
||||||
|
- task: context:sync
|
||||||
|
- cmd: |
|
||||||
|
drift=$(git status --porcelain -- AGENTS.md CLAUDE.md .cursorrules .aider.conventions.md .context/system-prompt.txt 2>/dev/null)
|
||||||
|
if [ -n "$drift" ]; then
|
||||||
|
echo "ERROR: derived adapters drifted from canonical context." >&2
|
||||||
|
echo "$drift" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Run: git add AGENTS.md CLAUDE.md .cursorrules .aider.conventions.md .context/system-prompt.txt" >&2
|
||||||
|
echo " git commit -m 'chore: re-sync context adapters'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ context: canonical and adapters are in sync"
|
||||||
- task: lint
|
- task: lint
|
||||||
- task: test
|
- task: test
|
||||||
- task: vet
|
- task: vet
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
// bridge is a stdio↔HTTP adapter that lets Claude Code connect to the
|
|
||||||
// supervisor MCP server via the stdio transport.
|
|
||||||
//
|
|
||||||
// Claude Code spawns this binary as a subprocess and communicates over
|
|
||||||
// stdin/stdout. Each newline-delimited JSON-RPC message from stdin is
|
|
||||||
// forwarded to the supervisor HTTP server and the response is written back.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// SUPERVISOR_URL=http://localhost:3200/mcp bridge
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
url := os.Getenv("SUPERVISOR_URL")
|
|
||||||
if url == "" {
|
|
||||||
url = "http://localhost:3200/mcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Bytes()
|
|
||||||
if len(bytes.TrimSpace(line)) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(line))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "bridge: build request: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "bridge: request failed: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, _ = io.Copy(os.Stdout, resp.Body)
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
_, _ = os.Stdout.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "bridge: scanner: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,6 +68,7 @@ func main() {
|
|||||||
mux.HandleFunc("POST /write", h.Write)
|
mux.HandleFunc("POST /write", h.Write)
|
||||||
mux.HandleFunc("POST /ingest", h.Ingest)
|
mux.HandleFunc("POST /ingest", h.Ingest)
|
||||||
mux.HandleFunc("POST /ingest-path", h.IngestPath)
|
mux.HandleFunc("POST /ingest-path", h.IngestPath)
|
||||||
|
mux.HandleFunc("POST /ingest-raw", h.IngestRaw)
|
||||||
mux.HandleFunc("POST /backfill-refs", h.BackfillRefs)
|
mux.HandleFunc("POST /backfill-refs", h.BackfillRefs)
|
||||||
|
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
|
|||||||
@@ -272,6 +272,48 @@ func (h *Handler) IngestPath(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, ingestResponse{Pages: allPages, Warnings: allWarnings})
|
writeJSON(w, ingestResponse{Pages: allPages, Warnings: allWarnings})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ingestRawRequest struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Pages []pipeline.RawPage `json:"pages"`
|
||||||
|
DryRun bool `json:"dry_run"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IngestRaw handles POST /ingest-raw — run the pipeline on pre-parsed RawPages,
|
||||||
|
// skipping the LLM extraction step. Use when the caller has already produced
|
||||||
|
// structured page data (e.g. from a more capable model or manual curation).
|
||||||
|
func (h *Handler) IngestRaw(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req ingestRawRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Source) == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "source is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Pages) == 0 {
|
||||||
|
writeError(w, http.StatusBadRequest, "pages is required and must be non-empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pipeline.RunRaw(h.brainDir, req.Source, req.Pages, req.DryRun)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("ingest-raw failed", "source", req.Source, "err", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "ingest error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pages := result.Pages
|
||||||
|
if pages == nil {
|
||||||
|
pages = []string{}
|
||||||
|
}
|
||||||
|
warnings := result.Warnings
|
||||||
|
if warnings == nil {
|
||||||
|
warnings = []string{}
|
||||||
|
}
|
||||||
|
writeJSON(w, ingestResponse{Pages: pages, Warnings: warnings})
|
||||||
|
}
|
||||||
|
|
||||||
// BackfillRefs handles POST /backfill-refs — injects source back-references
|
// BackfillRefs handles POST /backfill-refs — injects source back-references
|
||||||
// into all concept and entity pages based on existing wiki/sources/ pages.
|
// into all concept and entity pages based on existing wiki/sources/ pages.
|
||||||
func (h *Handler) BackfillRefs(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) BackfillRefs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -226,6 +226,85 @@ func TestIngestPath_File(t *testing.T) {
|
|||||||
assert.NotEmpty(t, pagesSlice)
|
assert.NotEmpty(t, pagesSlice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// POST /ingest-raw
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestIngestRaw_Validation(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
body map[string]any
|
||||||
|
}{
|
||||||
|
{"missing source", map[string]any{"pages": []any{map[string]any{"title": "X", "type": "concept", "content": "x"}}}},
|
||||||
|
{"missing pages", map[string]any{"source": "test-source"}},
|
||||||
|
{"empty pages", map[string]any{"source": "test-source", "pages": []any{}}},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, h := setup(t)
|
||||||
|
body, _ := json.Marshal(tc.body)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/ingest-raw", bytes.NewReader(body))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.IngestRaw(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngestRaw_Success(t *testing.T) {
|
||||||
|
dir, h := setup(t)
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"source": "test-article",
|
||||||
|
"pages": []any{
|
||||||
|
map[string]any{"title": "Test Article", "type": "source", "subtype": "article", "domain": "Testing", "content": "## Summary\n\nThis is a test article about [[Test Concept]].\n"},
|
||||||
|
map[string]any{"title": "Test Concept", "type": "concept", "domain": "Testing", "content": "A concept for testing.\n"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/ingest-raw", bytes.NewReader(body))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.IngestRaw(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
pages := resp["pages"].([]any)
|
||||||
|
assert.Len(t, pages, 2)
|
||||||
|
|
||||||
|
// Verify files were written
|
||||||
|
sourcePath := filepath.Join(dir, "wiki", "sources", "test-article.md")
|
||||||
|
assert.FileExists(t, sourcePath)
|
||||||
|
conceptPath := filepath.Join(dir, "wiki", "concepts", "test-concept.md")
|
||||||
|
assert.FileExists(t, conceptPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngestRaw_DryRun(t *testing.T) {
|
||||||
|
dir, h := setup(t)
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"source": "dry-run-test",
|
||||||
|
"pages": []any{
|
||||||
|
map[string]any{"title": "Dry Run Source", "type": "source", "subtype": "article", "content": "Content."},
|
||||||
|
},
|
||||||
|
"dry_run": true,
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/ingest-raw", bytes.NewReader(body))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.IngestRaw(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
pages := resp["pages"].([]any)
|
||||||
|
assert.NotEmpty(t, pages)
|
||||||
|
|
||||||
|
// Verify no files were written
|
||||||
|
sourcePath := filepath.Join(dir, "wiki", "sources", "dry-run-test.md")
|
||||||
|
assert.NoFileExists(t, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIngestPath_Directory(t *testing.T) {
|
func TestIngestPath_Directory(t *testing.T) {
|
||||||
_, h := setup(t)
|
_, h := setup(t)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ type RawPage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseRawPages parses LLM output as a JSON array of RawPage objects.
|
// ParseRawPages parses LLM output as a JSON array of RawPage objects.
|
||||||
// If the array is truncated mid-object (token limit), it salvages all complete objects.
|
// If the output contains invalid JSON escape sequences (e.g. \. from Markdown),
|
||||||
|
// it attempts repair before falling back to truncation recovery.
|
||||||
func ParseRawPages(output string) ([]RawPage, []string) {
|
func ParseRawPages(output string) ([]RawPage, []string) {
|
||||||
output = strings.TrimSpace(output)
|
output = strings.TrimSpace(output)
|
||||||
if output == "" {
|
if output == "" {
|
||||||
@@ -27,23 +28,30 @@ func ParseRawPages(output string) ([]RawPage, []string) {
|
|||||||
|
|
||||||
output = stripFences(output)
|
output = stripFences(output)
|
||||||
|
|
||||||
|
// Fast path: valid JSON.
|
||||||
var pages []RawPage
|
var pages []RawPage
|
||||||
if err := json.Unmarshal([]byte(output), &pages); err == nil {
|
if err := json.Unmarshal([]byte(output), &pages); err == nil {
|
||||||
return pages, nil
|
return pages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Repair pass: fix invalid escape sequences (e.g. \. \d from Markdown content).
|
||||||
|
repaired := repairJSON(output)
|
||||||
|
if err := json.Unmarshal([]byte(repaired), &pages); err == nil {
|
||||||
|
return pages, []string{"repaired invalid JSON escape sequences in LLM output"}
|
||||||
|
}
|
||||||
|
|
||||||
// Truncation recovery: find last `}` that closes a complete object.
|
// Truncation recovery: find last `}` that closes a complete object.
|
||||||
idx := strings.LastIndex(output, "}")
|
idx := strings.LastIndex(repaired, "}")
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return nil, []string{"LLM output contained no complete JSON objects"}
|
return nil, []string{"LLM output contained no complete JSON objects"}
|
||||||
}
|
}
|
||||||
|
|
||||||
start := strings.Index(output, "[")
|
start := strings.Index(repaired, "[")
|
||||||
if start < 0 {
|
if start < 0 {
|
||||||
return nil, []string{"LLM output contained no JSON array opening bracket"}
|
return nil, []string{"LLM output contained no JSON array opening bracket"}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate := output[start:idx+1] + "]"
|
candidate := repaired[start:idx+1] + "]"
|
||||||
if err := json.Unmarshal([]byte(candidate), &pages); err != nil {
|
if err := json.Unmarshal([]byte(candidate), &pages); err != nil {
|
||||||
return nil, []string{fmt.Sprintf("truncation recovery failed: %v", err)}
|
return nil, []string{fmt.Sprintf("truncation recovery failed: %v", err)}
|
||||||
}
|
}
|
||||||
@@ -51,6 +59,45 @@ func ParseRawPages(output string) ([]RawPage, []string) {
|
|||||||
return pages, []string{fmt.Sprintf("LLM output was truncated; recovered %d page(s)", len(pages))}
|
return pages, []string{fmt.Sprintf("LLM output was truncated; recovered %d page(s)", len(pages))}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// repairJSON replaces invalid JSON escape sequences (e.g. \. \d \p) with
|
||||||
|
// a properly escaped backslash followed by the same character.
|
||||||
|
// It iterates byte-by-byte to correctly skip already-valid escape sequences
|
||||||
|
// (including \\) without requiring lookbehind support.
|
||||||
|
func repairJSON(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
i := 0
|
||||||
|
for i < len(s) {
|
||||||
|
if s[i] != '\\' {
|
||||||
|
b.WriteByte(s[i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We have a backslash. Peek at the next character.
|
||||||
|
if i+1 >= len(s) {
|
||||||
|
// Trailing backslash — emit as-is.
|
||||||
|
b.WriteByte(s[i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next := s[i+1]
|
||||||
|
switch next {
|
||||||
|
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u':
|
||||||
|
// Valid JSON escape sequence — emit both characters as-is.
|
||||||
|
b.WriteByte(s[i])
|
||||||
|
b.WriteByte(next)
|
||||||
|
i += 2
|
||||||
|
default:
|
||||||
|
// Invalid escape — double the backslash.
|
||||||
|
b.WriteByte('\\')
|
||||||
|
b.WriteByte('\\')
|
||||||
|
b.WriteByte(next)
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func stripFences(s string) string {
|
func stripFences(s string) string {
|
||||||
for _, prefix := range []string{"```json\n", "```json\r\n", "```\n", "```\r\n"} {
|
for _, prefix := range []string{"```json\n", "```json\r\n", "```\n", "```\r\n"} {
|
||||||
if strings.HasPrefix(s, prefix) {
|
if strings.HasPrefix(s, prefix) {
|
||||||
|
|||||||
@@ -59,3 +59,29 @@ func TestParseRawPages_MissingTitle(t *testing.T) {
|
|||||||
assert.Empty(t, warnings)
|
assert.Empty(t, warnings)
|
||||||
assert.Empty(t, pages[0].Title)
|
assert.Empty(t, pages[0].Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseRawPages_InvalidEscapeRepaired(t *testing.T) {
|
||||||
|
// LLM copied markdown escaped list numbers (\.) into JSON — invalid escape
|
||||||
|
raw := "[{\"title\":\"Foo\",\"type\":\"concept\",\"content\":\"Step 4\\. Do it.\"}]"
|
||||||
|
pages, warnings := ParseRawPages(raw)
|
||||||
|
require.Len(t, pages, 1)
|
||||||
|
assert.Equal(t, "Foo", pages[0].Title)
|
||||||
|
assert.Contains(t, pages[0].Content, `4\.`)
|
||||||
|
assert.NotEmpty(t, warnings) // repair warning
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepairJSON_FixesInvalidEscapes(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{`{"a":"foo\.bar"}`, `{"a":"foo\\.bar"}`},
|
||||||
|
{`{"a":"\\n is fine"}`, `{"a":"\\n is fine"}`}, // valid \n untouched
|
||||||
|
{`{"a":"\d+ items"}`, `{"a":"\\d+ items"}`},
|
||||||
|
{`{"a":"already \\ escaped"}`, `{"a":"already \\ escaped"}`}, // valid \\ untouched
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := repairJSON(tc.in)
|
||||||
|
assert.Equal(t, tc.want, got, "input: %s", tc.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,11 +59,31 @@ func Run(ctx context.Context, cfg Config, brainDir, content, source string, dryR
|
|||||||
allWarnings = append(allWarnings, warnings...)
|
allWarnings = append(allWarnings, warnings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
pages, buildWarnings := BuildPages(allRaw, sourceSlug, date)
|
return buildAndWrite(allRaw, sourceSlug, date, brainDir, source, inventory, allWarnings, dryRun)
|
||||||
allWarnings = append(allWarnings, buildWarnings...)
|
}
|
||||||
|
|
||||||
|
// RunRaw runs the pipeline on pre-parsed RawPages, skipping the LLM extraction
|
||||||
|
// step. Use this when the caller has already produced the structured RawPage data
|
||||||
|
// (e.g. from a more capable model or manual curation).
|
||||||
|
func RunRaw(brainDir, source string, rawPages []RawPage, dryRun bool) (Result, error) {
|
||||||
|
inventory, err := wiki.LoadInventory(brainDir)
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, fmt.Errorf("load inventory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSlug := wiki.Slug(source)
|
||||||
|
date := time.Now().UTC().Format("2006-01-02")
|
||||||
|
|
||||||
|
return buildAndWrite(rawPages, sourceSlug, date, brainDir, source, inventory, nil, dryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAndWrite runs BuildPages through write for both Run and RunRaw.
|
||||||
|
func buildAndWrite(rawPages []RawPage, sourceSlug, date, brainDir, source string, inventory map[wiki.PageType][]wiki.Entry, warnings []string, dryRun bool) (Result, error) {
|
||||||
|
pages, buildWarnings := BuildPages(rawPages, sourceSlug, date)
|
||||||
|
warnings = append(warnings, buildWarnings...)
|
||||||
resolved := Resolve(pages, inventory)
|
resolved := Resolve(pages, inventory)
|
||||||
canonicalized, linkWarnings := CanonicalizeLinks(resolved, inventory)
|
canonicalized, linkWarnings := CanonicalizeLinks(resolved, inventory)
|
||||||
allWarnings = append(allWarnings, linkWarnings...)
|
warnings = append(warnings, linkWarnings...)
|
||||||
withRefs := injectSourceRefs(canonicalized, inventory, brainDir)
|
withRefs := injectSourceRefs(canonicalized, inventory, brainDir)
|
||||||
merged := mergeAll(withRefs)
|
merged := mergeAll(withRefs)
|
||||||
|
|
||||||
@@ -83,14 +103,14 @@ func Run(ctx context.Context, cfg Config, brainDir, content, source string, dryR
|
|||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
if err := wiki.RebuildIndex(brainDir, date); err != nil {
|
if err := wiki.RebuildIndex(brainDir, date); err != nil {
|
||||||
allWarnings = append(allWarnings, fmt.Sprintf("rebuild index: %v", err))
|
warnings = append(warnings, fmt.Sprintf("rebuild index: %v", err))
|
||||||
}
|
}
|
||||||
if err := wiki.AppendLog(brainDir, source, written, allWarnings, date); err != nil {
|
if err := wiki.AppendLog(brainDir, source, written, warnings, date); err != nil {
|
||||||
allWarnings = append(allWarnings, fmt.Sprintf("append log: %v", err))
|
warnings = append(warnings, fmt.Sprintf("append log: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result{Pages: written, Warnings: allWarnings}, nil
|
return Result{Pages: written, Warnings: warnings}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeAll deduplicates pages by path, merging content from later occurrences.
|
// mergeAll deduplicates pages by path, merging content from later occurrences.
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON-RPC 2.0 notifications (no id) must not receive a response.
|
||||||
|
if req.ID == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var result any
|
var result any
|
||||||
var rpcErr *rpcError
|
var rpcErr *rpcError
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mathiasbq/supervisor/internal/mcp"
|
"github.com/mathiasbq/supervisor/internal/mcp"
|
||||||
@@ -76,3 +77,39 @@ func TestMCPUnknownMethod(t *testing.T) {
|
|||||||
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||||
assert.NotNil(t, resp["error"])
|
assert.NotNil(t, resp["error"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMCPNotificationKnownMethodGetsNoResponseBody(t *testing.T) {
|
||||||
|
reg := registry.New()
|
||||||
|
srv := mcp.NewServer(reg)
|
||||||
|
|
||||||
|
// JSON-RPC 2.0 notification: "id" field absent. Per spec, server MUST NOT
|
||||||
|
// reply. notifications/initialized is part of the standard MCP handshake.
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/mcp", jsonBody(t, map[string]any{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "notifications/initialized",
|
||||||
|
}))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Empty(t, strings.TrimSpace(rr.Body.String()),
|
||||||
|
"notifications must not receive a response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPNotificationUnknownMethodGetsNoResponseBody(t *testing.T) {
|
||||||
|
reg := registry.New()
|
||||||
|
srv := mcp.NewServer(reg)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/mcp", jsonBody(t, map[string]any{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "notifications/totally-unknown",
|
||||||
|
}))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Empty(t, strings.TrimSpace(rr.Body.String()),
|
||||||
|
"unknown notifications must also receive no response body")
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
return s.query(ctx, args)
|
return s.query(ctx, args)
|
||||||
case "brain_write":
|
case "brain_write":
|
||||||
return s.write(ctx, args)
|
return s.write(ctx, args)
|
||||||
|
case "brain_ingest_raw":
|
||||||
|
return s.ingestRaw(ctx, args)
|
||||||
case "brain_ingest":
|
case "brain_ingest":
|
||||||
return s.ingest(ctx, args)
|
return s.ingest(ctx, args)
|
||||||
case "brain_search":
|
case "brain_search":
|
||||||
@@ -98,6 +100,33 @@ func (s *Skill) ingest(ctx context.Context, args json.RawMessage) (json.RawMessa
|
|||||||
return nil, fmt.Errorf("either content+source or path is required")
|
return nil, fmt.Errorf("either content+source or path is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ingestRawArgs struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Pages []any `json:"pages"`
|
||||||
|
DryRun bool `json:"dry_run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Skill) ingestRaw(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
var a ingestRawArgs
|
||||||
|
if err := json.Unmarshal(args, &a); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse args: %w", err)
|
||||||
|
}
|
||||||
|
if s.cfg.IngestSvcURL == "" {
|
||||||
|
return nil, fmt.Errorf("brain_ingest_raw: INGEST_SVC_URL not configured")
|
||||||
|
}
|
||||||
|
if a.Source == "" {
|
||||||
|
return nil, fmt.Errorf("source is required")
|
||||||
|
}
|
||||||
|
if len(a.Pages) == 0 {
|
||||||
|
return nil, fmt.Errorf("pages is required and must be non-empty")
|
||||||
|
}
|
||||||
|
return s.postTo(ctx, s.cfg.IngestSvcURL+"/ingest-raw", map[string]any{
|
||||||
|
"source": a.Source,
|
||||||
|
"pages": a.Pages,
|
||||||
|
"dry_run": a.DryRun,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type searchArgs struct {
|
type searchArgs struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Collection string `json:"collection,omitempty"`
|
Collection string `json:"collection,omitempty"`
|
||||||
|
|||||||
@@ -55,6 +55,32 @@ func (s *Skill) Tools() []registry.ToolDef {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if s.cfg.IngestSvcURL != "" {
|
if s.cfg.IngestSvcURL != "" {
|
||||||
|
tools = append(tools, registry.ToolDef{
|
||||||
|
Name: "brain_ingest_raw",
|
||||||
|
Description: "Ingest pre-structured pages into the brain wiki, bypassing the LLM extraction step. " +
|
||||||
|
"Use when you (the calling agent) have already extracted entities, concepts, and content from a source. " +
|
||||||
|
"Provide source (human-readable name) and pages (array of {title, type, subtype, domain, content} objects). " +
|
||||||
|
"The pipeline computes slugs, paths, frontmatter, wikilink canonicalization, and source back-references. " +
|
||||||
|
"Returns the list of wiki pages written.",
|
||||||
|
InputSchema: schema([]string{"source", "pages"}, map[string]any{
|
||||||
|
"source": map[string]any{"type": "string", "description": "human-readable name for the source, e.g. 'shape-up-book'"},
|
||||||
|
"pages": map[string]any{
|
||||||
|
"type": "array",
|
||||||
|
"items": map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"required": []string{"title", "type", "content"},
|
||||||
|
"properties": map[string]any{
|
||||||
|
"title": map[string]any{"type": "string", "description": "page title, e.g. 'Hash Encoding'"},
|
||||||
|
"type": map[string]any{"type": "string", "enum": []string{"source", "concept", "entity"}, "description": "page type"},
|
||||||
|
"subtype": map[string]any{"type": "string", "description": "entity: person|company|tool|model|framework|technology; source: article|pdf|book|video|note|project"},
|
||||||
|
"domain": map[string]any{"type": "string", "description": "knowledge domain, e.g. 'Machine Learning'"},
|
||||||
|
"content": map[string]any{"type": "string", "description": "markdown body — no frontmatter, use [[Display Name]] for wikilinks"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dry_run": map[string]any{"type": "boolean"},
|
||||||
|
}),
|
||||||
|
})
|
||||||
tools = append(tools, registry.ToolDef{
|
tools = append(tools, registry.ToolDef{
|
||||||
Name: "brain_ingest",
|
Name: "brain_ingest",
|
||||||
Description: "Ingest content into the brain wiki (brain/wiki/). Calls an LLM to produce structured wiki pages. " +
|
Description: "Ingest content into the brain wiki (brain/wiki/). Calls an LLM to produce structured wiki pages. " +
|
||||||
|
|||||||
Reference in New Issue
Block a user