From 92c9c42e2482b9a5c3c8a62e26390255b1f6dcaa Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Thu, 16 Apr 2026 21:50:53 +0200 Subject: [PATCH] chore: scaffold supervisor from project template --- .context/PROJECT.md | 65 ++++++++++++++ .context/mcp.json | 8 ++ .gitignore | 33 +++++++ .skills-shared | 1 + .skills/go-patterns/SKILL.md | 42 +++++++++ .skills/htmx-patterns/SKILL.md | 31 +++++++ DECISIONS.md | 53 ++++++++++++ README.md | 98 +++++++++++++++++++++ Taskfile.yml | 57 ++++++++++++ scripts/context-sync.sh | 153 +++++++++++++++++++++++++++++++++ scripts/init.sh | 14 +++ 11 files changed, 555 insertions(+) create mode 100644 .context/PROJECT.md create mode 100644 .context/mcp.json create mode 100644 .gitignore create mode 120000 .skills-shared create mode 100644 .skills/go-patterns/SKILL.md create mode 100644 .skills/htmx-patterns/SKILL.md create mode 100644 DECISIONS.md create mode 100644 README.md create mode 100644 Taskfile.yml create mode 100755 scripts/context-sync.sh create mode 100755 scripts/init.sh diff --git a/.context/PROJECT.md b/.context/PROJECT.md new file mode 100644 index 0000000..3c89c4e --- /dev/null +++ b/.context/PROJECT.md @@ -0,0 +1,65 @@ +# Project context + + + +## 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 diff --git a/.context/mcp.json b/.context/mcp.json new file mode 100644 index 0000000..cd0f1bc --- /dev/null +++ b/.context/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "knowledge": { + "url": "http://localhost:3100/mcp", + "description": "Project knowledge base — vector + graph retrieval" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e53ab5d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.skills-shared b/.skills-shared new file mode 120000 index 0000000..c8bd8ce --- /dev/null +++ b/.skills-shared @@ -0,0 +1 @@ +/Users/mathias/dev/.skills \ No newline at end of file diff --git a/.skills/go-patterns/SKILL.md b/.skills/go-patterns/SKILL.md new file mode 100644 index 0000000..e423120 --- /dev/null +++ b/.skills/go-patterns/SKILL.md @@ -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 diff --git a/.skills/htmx-patterns/SKILL.md b/.skills/htmx-patterns/SKILL.md new file mode 100644 index 0000000..3c28a68 --- /dev/null +++ b/.skills/htmx-patterns/SKILL.md @@ -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 +
+ + + ... +
+``` + +## 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. diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..fe95101 --- /dev/null +++ b/DECISIONS.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1291d5c --- /dev/null +++ b/README.md @@ -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 diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..e978fca --- /dev/null +++ b/Taskfile.yml @@ -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 diff --git a/scripts/context-sync.sh b/scripts/context-sync.sh new file mode 100755 index 0000000..a3f9468 --- /dev/null +++ b/scripts/context-sync.sh @@ -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." diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100755 index 0000000..3831dac --- /dev/null +++ b/scripts/init.sh @@ -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)"