chore: scaffold supervisor from project template

This commit is contained in:
Mathias Bergqvist
2026-04-16 21:50:53 +02:00
commit 92c9c42e24
11 changed files with 555 additions and 0 deletions

65
.context/PROJECT.md Normal file
View 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

8
.context/mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"knowledge": {
"url": "http://localhost:3100/mcp",
"description": "Project knowledge base — vector + graph retrieval"
}
}
}

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Binaries
bin/
*.exe
# Go
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
.context/system-prompt.txt
# ── Sensitive ──
.env
.env.*
*.key
*.pem
secrets/
# IDE
.idea/
.vscode/
*.swp
*~
# OS
.DS_Store
Thumbs.db

1
.skills-shared Symbolic link
View File

@@ -0,0 +1 @@
/Users/mathias/dev/.skills

View File

@@ -0,0 +1,42 @@
---
name: go-patterns
description: Go project patterns — endpoint checklist, error handling, HTMX responses, dependency policy. Use when writing Go code, adding endpoints, or reviewing Go PRs.
---
# Go project patterns
## New endpoint checklist
1. Define request/response types in `types.go`
2. Write handler in `handlers.go` using `http.HandlerFunc`
3. Add route in `routes.go`
4. Write table-driven test in `handlers_test.go`
5. Run `task check` before committing
## Error handling pattern
```go
if err != nil {
return fmt.Errorf("descriptiveOperation: %w", err)
}
```
Never log and return — do one or the other.
## HTMX response pattern
```go
func (h *Handler) ListItems(w http.ResponseWriter, r *http.Request) {
items, err := h.store.List(r.Context())
if err != nil {
http.Error(w, "failed to list items", http.StatusInternalServerError)
return
}
if r.Header.Get("HX-Request") == "true" {
h.templates.Render(w, "items/_list", items)
return
}
h.templates.Render(w, "items/index", items)
}
```
## Dependency policy
- Prefer stdlib: `net/http`, `encoding/json`, `database/sql`
- Allowed without justification: `testify`, `slog`, `templ`, `sqlc`
- Needs justification in commit message: anything else

View File

@@ -0,0 +1,31 @@
---
name: htmx-patterns
description: HTMX conventions — default attributes, form patterns, validation errors, hypermedia-first API design. Use when writing HTMX templates or Go handlers that return HTML fragments.
---
# HTMX patterns
## Default attributes
Always include on interactive elements:
- `hx-indicator` for loading states
- `hx-swap="innerHTML"` as default (explicit over implicit)
- `hx-target` pointing to a specific ID, never `this` in production
## Form pattern
```html
<form hx-post="/items" hx-target="#item-list" hx-swap="beforeend" hx-indicator="#spinner">
<input type="text" name="title" required>
<button type="submit">Add</button>
<span id="spinner" class="htmx-indicator">...</span>
</form>
```
## Server-sent validation errors
Return 422 with the error fragment, swap into the form's error container:
```html
hx-target-422="#form-errors"
```
## Prefer hypermedia over JSON
If the endpoint returns data for display, return an HTML fragment.
Only use JSON for machine-to-machine APIs or when a non-browser client needs it.

53
DECISIONS.md Normal file
View File

@@ -0,0 +1,53 @@
# Decisions log
Record *why* things are the way they are. Future-you will thank present-you.
---
## 2026-04-08 — AGENTS.md as cross-tool standard, not CLAUDE.md
**Context**: Multiple tools (Crush, Pi, Antigravity) read `AGENTS.md` natively. Claude Code reads `CLAUDE.md`. Building on `CLAUDE.md` as the primary format locks into one vendor.
**Decision**: Canonical source is `.context/AGENT.md` (root) and `.context/PROJECT.md` (per-project). The adapter script generates both `AGENTS.md` and `CLAUDE.md` — identical content, two filenames. Crush, Pi, and Antigravity read `AGENTS.md`; Claude Code reads `CLAUDE.md`.
**Consequences**: One canonical file serves five+ tools. Adding a new tool that reads `AGENTS.md` requires zero adapter work.
## 2026-04-08 — Agent Skills standard (SKILL.md in folders) over flat markdown
**Context**: Claude Code, Pi, Crush, and Antigravity all support the Agent Skills open standard: a folder containing `SKILL.md` with frontmatter (`name`, `description`). Skills are discovered on-demand — only the description enters context, full instructions load when triggered.
**Decision**: Skills live in `.skills/{name}/SKILL.md` at project level. This replaces the earlier `.context/skills/{name}.md` flat-file approach.
**Consequences**: Skills are cross-compatible without adaptation. Pi auto-discovers them from `.pi/skills/` (symlink). Crush reads them natively. Progressive disclosure keeps context window lean.
## 2026-04-08 — Go + HTMX as default stack
**Context**: Need a default that's fast to prototype, easy to deploy as a single binary, and doesn't require a Node/npm toolchain for the UI layer.
**Decision**: Go with HTMX + Templ for server-rendered UI. Python as fallback for ML/data tasks. TypeScript only when a project genuinely needs a rich client-side SPA.
**Consequences**: Simpler deployment and dependency management. Agents need Go-specific skills.
## 2026-04-08 — Task over Make
**Context**: Makefiles have arcane syntax and poor cross-platform support.
**Decision**: Use Taskfile (taskfile.dev) — YAML-based, cross-platform, supports task dependencies.
**Consequences**: One extra binary to install. All project automation in `Taskfile.yml`.
## 2026-04-08 — Qdrant over ChromaDB for vector store
**Context**: Need collection-level isolation for client separation, payload filtering, runs well in k3s.
**Decision**: Qdrant. Native collection isolation, rich filtering, mature gRPC API.
**Consequences**: More operational complexity than Chroma, but isolation is non-negotiable for client work.
## 2026-04-08 — Mistral Vibe gets its own adapter
**Context**: Vibe doesn't read `AGENTS.md` — it uses `~/.vibe/prompts/` and `~/.vibe/agents/` with TOML config.
**Decision**: The root context-sync generates a `mathias.md` prompt and `mathias.toml` agent config in `~/.vibe/`. This is the one tool that needs a custom adapter path.
**Consequences**: Run `vibe --agent mathias` to use your conventions. Other Vibe users on the machine aren't affected.

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
# Project template
Harness-agnostic project scaffold using the Agent Skills open standard.
## Quick start
```bash
degit mathias/project-template my-new-project
cd my-new-project
task init
```
## Structure
```
.context/
├── PROJECT.md ← Canonical project context (edit this)
├── mcp.json ← MCP server config (generated on first sync)
└── system-prompt.txt ← Generated: generic system prompt
.skills/
├── go-patterns/
│ └── SKILL.md ← Agent Skills standard format
└── htmx-patterns/
└── SKILL.md
scripts/
└── context-sync.sh ← Adapter generator (finds root AGENT.md automatically)
Taskfile.yml ← Task runner config
DECISIONS.md ← Why things are the way they are
```
## Generated files (gitignored)
| File | Consumer | Notes |
|------|----------|-------|
| `AGENTS.md` | Crush, Pi, Antigravity | Root + project concatenated |
| `CLAUDE.md` | Claude Code | Project-only (inherits root via tree walk) |
| `.cursorrules` | Cursor | Root + project concatenated |
| `.aider.conventions.md` | Aider | Root + project concatenated |
| `.context/system-prompt.txt` | Open WebUI, Mods, generic | Root + project concatenated |
## How root context works
The script walks up from the project directory looking for `~/dev/.context/AGENT.md`.
- **Claude Code**: inherits natively (reads every `CLAUDE.md` up the tree) → project CLAUDE.md is project-only
- **Everything else**: can't walk the tree → script concatenates root + project into each generated file
## Skills
Skills use the [Agent Skills open standard](https://github.com/badlogic/pi-skills). Each skill is a folder with a `SKILL.md` containing frontmatter:
```yaml
---
name: my-skill
description: What this skill does. When to use it.
---
# Instructions here
```
Supported natively by Claude Code, Pi, Crush, and Antigravity. No adapter needed for skills.
### Adding a skill
```bash
mkdir .skills/my-new-skill
# Create .skills/my-new-skill/SKILL.md with frontmatter + instructions
```
### Using pi-skills (cross-compatible)
```bash
# User-level (all projects)
git clone https://github.com/badlogic/pi-skills ~/.pi/agent/skills/pi-skills
# Symlink for Claude Code
ln -s ~/.pi/agent/skills/pi-skills/brave-search ~/.claude/skills/brave-search
```
## Usage with specific tools
**Claude Code**: `task context:sync:claude` → reads `CLAUDE.md` + discovers `.skills/*/SKILL.md`
**Crush**: `task context:sync:agents` → reads `AGENTS.md` + discovers `.skills/*/SKILL.md`
**Pi**: `task context:sync:agents` → reads `AGENTS.md` + discovers `.skills/*/SKILL.md` (or symlink `.skills/` to `.pi/skills/`)
**Antigravity**: `task context:sync:agents` → reads `AGENTS.md` + discovers `.skills/*/SKILL.md`
**Cursor**: `task context:sync:cursor` → reads `.cursorrules`
**Mistral Vibe**: Run root-level `task context:sync:vibe` once → `vibe --agent mathias`
**Open WebUI / Mods**: Copy `.context/system-prompt.txt` into a preset or pipe it
**Any other tool**: Point at `.context/PROJECT.md` directly — it's human-readable markdown

57
Taskfile.yml Normal file
View File

@@ -0,0 +1,57 @@
version: '3'
vars:
PROJECT_NAME: '{{.PROJECT_NAME | default "myproject"}}'
tasks:
context:sync:
desc: Regenerate all harness-specific context files
cmds:
- bash scripts/context-sync.sh
sources:
- .context/PROJECT.md
- .skills/*/SKILL.md
context:sync:claude:
cmds: [bash scripts/context-sync.sh claude]
context:sync:agents:
cmds: [bash scripts/context-sync.sh agents]
context:sync:cursor:
cmds: [bash scripts/context-sync.sh cursor]
dev:
desc: Start development server
cmds:
- go run ./cmd/server
build:
desc: Build the binary
cmds:
- go build -o bin/{{.PROJECT_NAME}} ./cmd/server
check:
desc: Run all checks (lint + test + vet)
cmds:
- task: lint
- task: test
- task: vet
lint:
cmds: [golangci-lint run ./...]
test:
cmds: [go test -race -count=1 ./...]
vet:
cmds:
- go vet ./...
- govulncheck ./... || true
up:
desc: Start containers
cmds: [docker compose up -d]
down:
cmds: [docker compose down]
init:
desc: Initialize a new project from this template
cmds:
- bash scripts/init.sh

153
scripts/context-sync.sh Executable file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Generates harness-specific context files from .context/PROJECT.md
# Project-level script — run from a project directory.
#
# For Claude Code: generates project-only CLAUDE.md (it inherits root via tree walk)
# For everything else: concatenates root AGENT.md + project PROJECT.md
#
# Usage: ./scripts/context-sync.sh [adapter...]
# Task: task context:sync
#
# Override root context: ROOT_CONTEXT=~/dev/.context/AGENT.md ./scripts/context-sync.sh
set -euo pipefail
PROJECT_FILE=".context/PROJECT.md"
SKILLS_DIR=".skills"
# Walk up to find root .context/AGENT.md
find_root_context() {
local dir
dir="$(pwd)"
while [ "$dir" != "/" ]; do
dir="$(dirname "$dir")"
if [ -f "$dir/.context/AGENT.md" ]; then
echo "$dir/.context/AGENT.md"
return
fi
done
echo ""
}
ROOT_CONTEXT="${ROOT_CONTEXT:-$(find_root_context)}"
if [ ! -f "$PROJECT_FILE" ]; then
echo "Error: $PROJECT_FILE not found. Are you in a project root?"
exit 1
fi
if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then
echo " Root context: $ROOT_CONTEXT"
else
echo " No root AGENT.md found (project context only)"
fi
# Emit root context + separator
root_block() {
if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then
cat "$ROOT_CONTEXT"
echo ""
echo "---"
echo ""
fi
}
# ── Claude Code ──────────────────────────────────────────────
# Claude Code walks up the tree — it finds ~/dev/CLAUDE.md automatically.
# Project-level CLAUDE.md only needs project-specific context.
generate_claude() {
cat "$PROJECT_FILE" > CLAUDE.md
echo " → CLAUDE.md (project-only; Claude Code inherits root)"
}
# ── AGENTS.md (Crush, Pi, Antigravity) ──────────────────────
# These tools read AGENTS.md from cwd but don't walk up.
# Concatenate root + project.
generate_agents() {
{ root_block; cat "$PROJECT_FILE"; } > AGENTS.md
echo " → AGENTS.md (root + project; Crush, Pi, Antigravity)"
}
# ── Cursor ───────────────────────────────────────────────────
generate_cursor() {
{
echo "# Cursor rules — auto-generated"
echo "# Do not edit. Run: task context:sync"
echo ""
root_block
cat "$PROJECT_FILE"
} > .cursorrules
echo " → .cursorrules (root + project)"
}
# ── Aider ────────────────────────────────────────────────────
generate_aider() {
{ root_block; cat "$PROJECT_FILE"; } > .aider.conventions.md
if [ ! -f .aider.conf.yml ]; then
cat > .aider.conf.yml << 'YAML'
read: .aider.conventions.md
auto-commits: false
YAML
fi
echo " → .aider.conventions.md (root + project)"
}
# ── Generic system prompt (Open WebUI, Mods, etc.) ──────────
generate_system_prompt() {
{
echo "You are a coding assistant working on a specific project."
echo "Follow all conventions from both the root agent context and project context."
echo ""
echo "---"
echo ""
root_block
cat "$PROJECT_FILE"
echo ""
echo "---"
} > .context/system-prompt.txt
echo " → .context/system-prompt.txt (root + project)"
}
# ── MCP config ───────────────────────────────────────────────
generate_mcp() {
if [ ! -f .context/mcp.json ]; then
cat > .context/mcp.json << 'JSON'
{
"mcpServers": {
"knowledge": {
"url": "http://localhost:3100/mcp",
"description": "Project knowledge base — vector + graph retrieval"
}
}
}
JSON
echo " → .context/mcp.json (new)"
else
echo " → .context/mcp.json (exists, skipped)"
fi
}
echo "Syncing project context from $PROJECT_FILE..."
if [ $# -eq 0 ]; then
generate_claude
generate_agents
generate_cursor
generate_aider
generate_system_prompt
generate_mcp
else
for adapter in "$@"; do
case "$adapter" in
claude) generate_claude ;;
agents) generate_agents ;;
cursor) generate_cursor ;;
aider) generate_aider ;;
prompt|system|openwebui|owui|generic) generate_system_prompt ;;
mcp) generate_mcp ;;
*) echo "Unknown adapter: $adapter" ;;
esac
done
fi
echo "Done."

14
scripts/init.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Project name:"
read name
echo "Client tag (or 'personal'):"
read client
sed -i '' "s/{{PROJECT_NAME}}/$name/g" .context/PROJECT.md 2>/dev/null || sed -i "s/{{PROJECT_NAME}}/$name/g" .context/PROJECT.md
sed -i '' "s/{{CLIENT_TAG or \"personal\"}}/$client/g" .context/PROJECT.md 2>/dev/null || sed -i "s/{{CLIENT_TAG or \"personal\"}}/$client/g" .context/PROJECT.md
sed -i '' "s/{{CLIENT_TAG}}/$client/g" .context/PROJECT.md 2>/dev/null || sed -i "s/{{CLIENT_TAG}}/$client/g" .context/PROJECT.md
task context:sync
echo "✓ Project initialized: $name (client: $client)"