Template
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9bbbae3bb | ||
|
|
7c70489efe | ||
|
|
73c4f1c1de | ||
|
|
ba4adebd8d | ||
|
|
650644ebe3 | ||
|
|
ef4e47bf00 | ||
|
|
bada904c8d | ||
|
|
3a7b70ef62 | ||
|
|
4d1fa1d488 | ||
|
|
8a22a28ab4 | ||
|
|
04fa3cf36e | ||
|
|
59b1a3a4ec | ||
|
|
e2810b9209 |
+1
-1
@@ -5,7 +5,7 @@
|
||||
- **Name**: __PROJECT_NAME__
|
||||
- **Owner**: Mathias
|
||||
- **Client**: personal
|
||||
- **Repo**: gitea.d-ma.be/mathias/__PROJECT_NAME__
|
||||
- **Repo**: __MODULE_PATH__
|
||||
- **Status**: active
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"knowledge": {
|
||||
"url": "http://localhost:3100/mcp",
|
||||
"description": "Project knowledge base — vector + graph retrieval"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Skill: 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
|
||||
@@ -0,0 +1,26 @@
|
||||
# Skill: 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.
|
||||
@@ -0,0 +1,368 @@
|
||||
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.
|
||||
|
||||
0. **Pre-task ritual — before ANY implementation (non-negotiable).** Run this before writing a single line:
|
||||
- **Query the brain** (`brain_query`) for the domain + symptom. If the result changes your approach, surface it before acting. 5 seconds beats 5 hours.
|
||||
- **Load the relevant skill** — see trigger table in *Engineering Skills* below.
|
||||
- **Write the failing test first.** Name the test before the function. If the target is untestable (e.g. `main()` wiring), extract the logic into a testable function first. No implementation without a red test.
|
||||
- **State the observable success criterion** — what specific behavior, output, or passing test proves this is done?
|
||||
|
||||
**TDD is non-negotiable.** "Tests pass" is not proof of correctness — only proof the tests ran. Write tests that would catch the bug before writing code that fixes it.
|
||||
|
||||
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).
|
||||
5. **Trunk-Based Development — commit directly to main.** Every commit is one
|
||||
logical change (one tool, one fix, one test) with passing tests. Main is always
|
||||
deployable. Never create long-lived feature branches.
|
||||
|
||||
**The PR trigger is RISK, not concurrency.** `risk/low` work commits straight to
|
||||
main with CI as the gate — that is most work and is unchanged. `risk/medium` and
|
||||
above opens a PR, even solo, even with nobody waiting to review it. This mirrors
|
||||
the tiered merge model in `assessor-loop`
|
||||
`method/transfer-to-a-regulated-organisation.md` §4.1, so the estate practises
|
||||
what it sells instead of merging everything on green and claiming otherwise.
|
||||
|
||||
**Concurrency is NOT a reason to branch.** Measured 2026-08-29 across three real
|
||||
collisions between parallel sessions: two were not branching problems at all — a
|
||||
generated artifact written to the same path by both (branching turns that into a
|
||||
merge conflict on a 33k-line JSON), and uncommitted work in a shared checkout
|
||||
(a per-session worktree fixes it) — and the third was an ordinary non-fast-forward
|
||||
push resolved by rebase in seconds. Use a worktree per session on shared repos and
|
||||
timestamp generated artifacts. Do not reach for a branch.
|
||||
|
||||
Branch+PR flow carries documented costs on solo repos: merging auto-closes gated
|
||||
issues, review comments live on a thread separate from the issue's, and a
|
||||
merged-and-deleted branch cannot be amended (brain:
|
||||
`gitea-pr-merge-autoclose-and-review-comment-footguns`). Those are the price of
|
||||
the `risk/medium`+ lane — paid deliberately, not incurred by accident.
|
||||
|
||||
**Exception — external contributor or client four-eyes requirement:** Use PR flow
|
||||
when a human reviewer outside the project is required. Document the reason in
|
||||
PROJECT.md.
|
||||
|
||||
6. **Close the loop — every substantive task ends with the same ritual.** Shipping
|
||||
the code is not the end of the task; capturing it is. Run this unprompted:
|
||||
- **Tag + bump SemVer** on the change (annotated tag; minor for a feature or
|
||||
new/changed ADR, patch for a fix; docs in the same commit). Check the repo's
|
||||
actual last tag — stated versions in docs drift stale.
|
||||
- **Push** main and the tag (CI is the gate).
|
||||
- **Persist generalizable learnings to the brain** (`brain_write`, wing/hall) —
|
||||
the reusable patterns and the footguns that would bite anyone again, never
|
||||
project status. See *Knowledge base — when to write* below.
|
||||
- **File discovered-but-deferred work as tracker issues** on the project's own
|
||||
repo — token-budget gaps, recorded ADR limitations, v2 follow-ups. Don't let
|
||||
"out of scope, recorded" rot in a commit message; make it a ticket with a
|
||||
source pointer.
|
||||
- Surface the brain entries and issue numbers in the closing summary so the
|
||||
trail is auditable.
|
||||
|
||||
**Chat is not a durable store. Never rely on Mathias remembering anything.**
|
||||
Every gap, caveat, risk, open question and next step must land in one of four
|
||||
places before the turn ends: **the brain**, **a repo** (code, docs, ADR),
|
||||
**a tracker issue**, or **a plan file**. A finding stated only in conversation is
|
||||
lost the moment the session closes — and this estate has repeatedly proved that a
|
||||
remembered convention is an unreliable control.
|
||||
|
||||
This applies hardest to what is tempting to leave as a closing remark: "worth
|
||||
knowing X isn't enforced yet", "this is a convention, not a gate", "we should
|
||||
revisit Y". Those are exactly the items that need a locator. If it is worth
|
||||
saying in a summary, it is worth writing where it can be found again without you.
|
||||
|
||||
## CAD sprint patterns
|
||||
|
||||
Two CAD sprint archetypes (defs: brain `wiki/homelab/decisions/continuous-agentic-development-cad-concept-2026-06-16`):
|
||||
- **Inception Sprint** — birth a new repo. Oath-gated close; tag only once every clause holds. Brain: `.../inception-sprint-and-oath`.
|
||||
- **Sweep Sprint** — clear an existing backlog. Parallel read-only research per issue → sequential one-issue-per-commit execution. Brain: `.../sweep-sprint-and-oath`.
|
||||
|
||||
Shared: decisions that are yours, not the agent's, surface as `DECISION NEEDED` (question/options/recommendation/blocks) — ask live if you're present, else comment and wait, never act past it. Live-breaking regressions get `git revert`, never forward-fixed; host-network work skips even read-only research (solo, synchronous, you present). Rule 6's closing ritual only fires once every Oath clause genuinely holds — blocked = descoped-and-tracked, never marked done.
|
||||
|
||||
## 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 | pgvector (vector), BM25 | Qdrant (when >1M vectors or hybrid retrieval) | — |
|
||||
| Logging | slog (structured) | stdlib `logging` w/ structured `extra=` (or structlog) | — |
|
||||
| Testing | Table-driven, testify | — | — |
|
||||
| Agents (Go) | google.golang.org/adk + pkg/litellm adapter | — | — |
|
||||
|
||||
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||
|
||||
## Code conventions
|
||||
|
||||
- **Go style**: golines, gofumpt, golangci-lint
|
||||
- **Python style** (fallback language): ruff (format+lint, one tool), mypy --strict (non-negotiable,
|
||||
matches Go's static typing discipline), pytest + pytest-cov (table-driven via
|
||||
`@pytest.mark.parametrize`), uv (venv+deps+lock, one tool), pydantic-settings (typed env-var config
|
||||
— same principle as Go's typed structs), src-layout + `pyproject.toml` only (no `setup.py`)
|
||||
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return.
|
||||
Python: `raise X from e` (exception chaining, same principle) — never bare `except`, never silent `pass`
|
||||
- **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:`), commit directly to main,
|
||||
one logical change per commit, CI is the quality gate
|
||||
- **Never**: long-lived feature branches, PRs for solo work, direct push without
|
||||
passing `task check` locally first
|
||||
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc, google.golang.org/adk (agent projects only) are pre-approved; anything else needs justification in the commit message
|
||||
|
||||
## Secret handling (every harness, every command)
|
||||
|
||||
Tool output is persisted: terminal → `~/.claude/projects` transcripts →
|
||||
claudewatcher → brain/wiki → gitea history. A secret printed once is
|
||||
searchable forever, and clearing it means rotating the key. So:
|
||||
|
||||
1. **Never print, echo, log, or transform a secret to inspect it.** No
|
||||
`base64`/`xxd`/`cat` of a key, and never pipe a secret through a transform
|
||||
to defeat `op run`'s output masking (it masks raw values; base64 hides them
|
||||
from the mask — that exact trick leaked a key on 2026-06-11).
|
||||
2. **Secrets stay in the subprocess.** Reference them only as env vars consumed
|
||||
*inside* `op run --env-file ~/.op-env -- <cmd>`. Never place a literal secret
|
||||
in a command's argv (it lands in the tool call and the transcript).
|
||||
3. **Existence check without revealing the value:** `[ -n "$X" ] && echo set` —
|
||||
never `${X:-...}` (returns the value when set) and never echo a substring of it.
|
||||
4. **Cross-host secrets:** run the secret-consuming command on the host that has
|
||||
the secret; do not forward a raw key over ssh argv/stdout.
|
||||
5. If a secret does leak into output, say so immediately and flag it for rotation —
|
||||
don't bury it.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
Three machines on Tailscale:
|
||||
|
||||
| Machine | Role | Key specs |
|
||||
|---------|------|-----------|
|
||||
| koala | GPU inference, heavy compute | RTX 5070, runs k3s + llama-swap + shared postgres18/pgvector |
|
||||
| iguana | Services, builds | M2 Ultra Mac |
|
||||
| flamingo | Daily driver, edge | Apple Silicon Mac, ~/dev is here |
|
||||
|
||||
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||
- **Orchestration**: k3s on koala only — single control-plane node, no workers (verified 2026-08-30). iguana and flamingo are on the tailnet but are not cluster members
|
||||
- **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 — actively use it
|
||||
|
||||
A persistent brain (BM25 search + LLM-synthesised Q&A) survives across sessions,
|
||||
hosts, and harnesses. It holds 100+ hard-won entries: infra incident postmortems,
|
||||
Go pitfalls, framework gotchas, design principles, ADRs. **It is not optional
|
||||
reference material — query it actively, not just when explicitly told.**
|
||||
|
||||
### When to query (treat as a reflex)
|
||||
|
||||
- **Before** starting a non-trivial task — search for prior art with the symptom
|
||||
AND the system component ("how did we solve X in Y?"). 5 seconds beats 5 hours.
|
||||
- **When debugging** — search for the error string, the stack frame, the affected
|
||||
service. Past you may have already paid this tax.
|
||||
- **Before adopting** a pattern, library, framework, or model name — check if it
|
||||
was tried and rejected, or what the integration footguns are.
|
||||
- **When making architectural decisions** — search for the domain + "ADR" or
|
||||
"decision" to find prior reasoning before re-deriving it.
|
||||
- **When a recommendation feels novel** — challenge yourself: "has this been
|
||||
documented?" The brain often has it.
|
||||
|
||||
### When to write
|
||||
|
||||
After you discover something that **future-you would forget** and that **isn't
|
||||
recoverable from the code, git log, or PR description alone**:
|
||||
|
||||
- Bugs whose root cause is non-obvious and generalisable beyond this project.
|
||||
- Framework / library / model-name quirks that bit you and would bite anyone.
|
||||
- Design principles validated under fire (e.g. "every `_get` needs a `_list`").
|
||||
- Postmortems for incidents: what broke, why, how diagnosed, what to do next time.
|
||||
|
||||
DON'T write project status, sprint progress, PR summaries, or "what I did this
|
||||
session" — those rot fast and the originals are in git/gitea anyway. Brain
|
||||
entries that age well are about *why*, *how to avoid*, and *what to do when*.
|
||||
|
||||
### How to access (per harness)
|
||||
|
||||
| Harness | Query | Write |
|
||||
|---------|-------|-------|
|
||||
| **Claude Code, Claude Desktop** | `brain_query` (BM25), `brain_answer` (LLM-synth + sources) MCP tools | `brain_write` MCP tool |
|
||||
| **Crush, Pi, Antigravity, other MCP-capable** | same MCP server: `ingestion-brain` (via the `mcp__*_brain__*` namespace once authenticated) | same |
|
||||
| **Anything HTTP-only (curl, scripts)** | `POST https://brain-mcp.d-ma.be/query` with `{"query":"..."}` (auth via `BRAIN_MCP_TOKEN`) | `POST .../write` with `{"content":"...","filename":"..."}` |
|
||||
| **Browser / human inspection** | `https://git.d-ma.be/mathias/hyperguild` → `knowledge/` and `wiki/` markdown files |
|
||||
|
||||
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`.
|
||||
- **Routing**: brain_answer's LLM uses berget.ai as primary, iguana ollama as
|
||||
fallback. Both are configurable in the `supervisor/ingestion-deployment.yaml`
|
||||
on the koala k3s cluster; don't hardcode local-only model names into the
|
||||
berget URL (see knowledge entry on namespace mismatches).
|
||||
|
||||
### Quick reflex checks
|
||||
|
||||
If you find yourself about to say any of these out loud, you owe yourself a brain query first:
|
||||
|
||||
- "I think the issue might be..."
|
||||
- "Let me try X and see..."
|
||||
- "I'll just write a script to..."
|
||||
- "This is probably a new bug..."
|
||||
- "Has anyone done this before?" — *yes, probably, go check.*
|
||||
|
||||
## 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
|
||||
|
||||
Supported harnesses (2026-09-01) — Cursor and Aider were dropped; do not
|
||||
re-add adapters for them:
|
||||
- Claude Code, Antigravity, OpenCode, Mistral Vibe, Crush, Pi Coding Agent
|
||||
- Open WebUI, Charmbracelet Mods, or any tool that takes a system prompt string
|
||||
- Any other tool that reads `AGENTS.md`, which is the de facto convention
|
||||
|
||||
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`, `.context/system-prompt.txt`
|
||||
|
||||
Repos still carrying `.cursorrules`, `.aider.conventions.md` or
|
||||
`.aider.conf.yml` are stale: nothing regenerates them. Delete them the next time
|
||||
you touch the repo, along with their entries in that repo's `task check`
|
||||
assertion.
|
||||
|
||||
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. Which file a harness reads does
|
||||
NOT split cleanly by lookup strategy, so do not reason from the strategy:
|
||||
- `CLAUDE.md` — Claude Code (walks up the tree)
|
||||
- `AGENTS.md` — Crush, Pi, Antigravity (cwd-only), OpenCode (project root,
|
||||
global at `~/.config/opencode/AGENTS.md`), and Mistral Vibe, which **walks
|
||||
up** from cwd and takes the first `AGENTS.md` it finds
|
||||
(user-level `~/.vibe/AGENTS.md`)
|
||||
- `.context/system-prompt.txt` — Open WebUI, Mods, raw API calls; not read by
|
||||
any harness, it is for pasting
|
||||
|
||||
Vibe tree-walks for `AGENTS.md`, so "tree-walkers use CLAUDE.md" is false.
|
||||
Each repo's `AGENTS.md` embeds the canonical and is the first one found from
|
||||
inside that repo, so this works — but by construction, not by luck.
|
||||
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 live in the **`mathias/skills`** repo (`git.d-ma.be/mathias/skills`). Load at task start — not "on demand" but on schedule, before writing code. Browse `SKILLS_INDEX.md` for the full list.
|
||||
|
||||
**Claude Code** installs the repo as a plugin from the marketplace the same repo hosts. That path also carries the `secret-guard` and `pre-task-ritual` hooks and the brain/gitea/infra MCP servers, which symlinks cannot:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||
/plugin install skills@mathias
|
||||
```
|
||||
|
||||
Skills are **namespaced** under Claude Code: load `tdd` as `skills:tdd`. The trigger table below uses bare names — prefix them.
|
||||
|
||||
**Crush, Pi and OpenCode** have no plugin system and are wired by `install.sh` as one directory symlink per harness via `~/.agents/skills`. Do not clone the checkout into `~/dev`: that folder is iCloud-shared between the Macs and spans two path roots (`/Users/mathias` vs `/home/mathias` on koala), so a checkout there is a git working tree with two concurrent writers, and any absolute symlink stored in it is broken on one side by construction (skills#23). Leave `SKILLS_CHECKOUT_DIR` at its host-local default.
|
||||
|
||||
Check a host with `task doctor` in the skills repo; it also runs automatically at session start via the plugin.
|
||||
|
||||
**Skill trigger table — load before starting, not after getting stuck:**
|
||||
|
||||
| Task type | Load |
|
||||
|-----------|------|
|
||||
| Any feature or bug fix | `tdd` |
|
||||
| Refactor or design | `clean-code` or `solid` |
|
||||
| Debug | `problem-analysis` |
|
||||
| Review code or PRs | `code-review` |
|
||||
| Frame a problem before coding | `problem-analysis` |
|
||||
|
||||
---
|
||||
|
||||
# __PROJECT_NAME__
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: __PROJECT_NAME__
|
||||
- **Owner**: Mathias
|
||||
- **Client**: personal
|
||||
- **Repo**: __MODULE_PATH__
|
||||
- **Status**: active
|
||||
|
||||
## Stack
|
||||
|
||||
Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build an OCI image with buildctl and push it to the local registry.
|
||||
#
|
||||
# CANONICAL COPY: mathias/infra ci-templates/build-image.sh (infra#309).
|
||||
# Every repo carries a byte-identical copy at .gitea/build-image.sh, written by
|
||||
# `task ci:sync` and held in place by the drift gate. DO NOT EDIT THE COPIES --
|
||||
# edit here, re-sync, and the gate will tell you which repos still disagree.
|
||||
#
|
||||
# Why this exists: this step was hand-copied into a dozen repos, so migrating it
|
||||
# off rootless buildah (infra#241) meant eleven near-identical commits, eleven
|
||||
# verifications, and three separate rediscoveries of the same fix in the two
|
||||
# repo templates and the gitea-ci skill. The differences between the copies were
|
||||
# incidental -- a label here, a build-arg there -- never intentional. Those are
|
||||
# inputs now, not forks of the implementation.
|
||||
#
|
||||
# Why buildctl and not buildah: the gitea-runner pod runs with
|
||||
# `capabilities: drop [ALL]` and `allowPrivilegeEscalation: false`, which
|
||||
# unconditionally blocks the setuid-root exec that rootless buildah's uid/gid
|
||||
# remapping needs, and the pod has no OCI runtime at all. buildctl is a thin
|
||||
# client -- it hands the build to the host's buildkitd over a socket, so the
|
||||
# privileged layer work happens on the host and this pod's securityContext is
|
||||
# not in the picture. buildah is absent from the runner image as of baked-v14
|
||||
# and its absence is asserted at pod start (infra#308).
|
||||
#
|
||||
# Inputs, all via environment:
|
||||
#
|
||||
# IMAGE (required) image name, e.g. cobalt-dingo
|
||||
# REGISTRY default localhost:5000
|
||||
# SHA_TAG default `git rev-parse --short HEAD`
|
||||
# VERSION_TAG optional extra tag, e.g. v1.2.3 on a tag build
|
||||
# PUSH_LATEST default 1; set 0 to skip the :latest tag
|
||||
# CONTEXT default . -- build context directory
|
||||
# DOCKERFILE_DIR default $CONTEXT -- directory holding the Dockerfile
|
||||
# DOCKERFILE_NAME optional, when the file is not named `Dockerfile`
|
||||
# TARGET optional, multi-stage build target (--opt target=)
|
||||
# BUILD_ARGS optional, space-separated K=V pairs
|
||||
# BUILD_SECRETS optional, space-separated buildkit secret specs, each
|
||||
# `id=NAME,src=PATH`. Pass the SPEC only -- the caller
|
||||
# writes the secret to PATH itself. A secret VALUE must
|
||||
# never be passed here: it would land in this script's
|
||||
# argv and therefore in the CI log.
|
||||
# EXTRA_LABELS optional, space-separated K=V pairs
|
||||
# GIT_SHA optional, stamped as org.opencontainers.image.revision
|
||||
# IMAGE_SOURCE optional, stamped as org.opencontainers.image.source
|
||||
#
|
||||
# Note on shell style below: every conditional is a full `if` block, never
|
||||
# `[ -n "$X" ] && cmd`. Under `set -e` the && form exits the script whenever the
|
||||
# test is false, which is a real bug this estate has already shipped once.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${IMAGE:-}" ]; then
|
||||
echo "build-image: IMAGE is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
REGISTRY="${REGISTRY:-localhost:5000}"
|
||||
BUILDKIT_HOST="${BUILDKIT_HOST:-unix:///run/buildkit/buildkitd.sock}"
|
||||
CONTEXT="${CONTEXT:-.}"
|
||||
DOCKERFILE_DIR="${DOCKERFILE_DIR:-$CONTEXT}"
|
||||
PUSH_LATEST="${PUSH_LATEST:-1}"
|
||||
VERSION_TAG="${VERSION_TAG:-}"
|
||||
|
||||
if [ -z "${SHA_TAG:-}" ]; then
|
||||
SHA_TAG="$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
|
||||
REF="${REGISTRY}/${IMAGE}:${SHA_TAG}"
|
||||
|
||||
TAR_FILE="$(mktemp)"
|
||||
trap 'rm -f "$TAR_FILE"' EXIT
|
||||
|
||||
args=(
|
||||
--frontend dockerfile.v0
|
||||
--local "context=${CONTEXT}"
|
||||
--local "dockerfile=${DOCKERFILE_DIR}"
|
||||
)
|
||||
|
||||
if [ -n "${DOCKERFILE_NAME:-}" ]; then
|
||||
args+=(--opt "filename=${DOCKERFILE_NAME}")
|
||||
fi
|
||||
if [ -n "${TARGET:-}" ]; then
|
||||
args+=(--opt "target=${TARGET}")
|
||||
fi
|
||||
for spec in ${BUILD_SECRETS:-}; do
|
||||
# Opaque id=NAME,src=PATH -- buildkit reads the file, nothing is echoed.
|
||||
args+=(--secret "${spec}")
|
||||
done
|
||||
if [ -n "${GIT_SHA:-}" ]; then
|
||||
args+=(--opt "label:org.opencontainers.image.revision=${GIT_SHA}")
|
||||
fi
|
||||
if [ -n "${IMAGE_SOURCE:-}" ]; then
|
||||
args+=(--opt "label:org.opencontainers.image.source=${IMAGE_SOURCE}")
|
||||
fi
|
||||
for kv in ${BUILD_ARGS:-}; do
|
||||
args+=(--opt "build-arg:${kv}")
|
||||
done
|
||||
for kv in ${EXTRA_LABELS:-}; do
|
||||
args+=(--opt "label:${kv}")
|
||||
done
|
||||
|
||||
echo "build-image: building ${REF}"
|
||||
buildctl --addr "$BUILDKIT_HOST" build "${args[@]}" --output "type=oci,dest=${TAR_FILE}"
|
||||
|
||||
# buildctl has no registry-push equivalent to `buildah push`, so skopeo copies
|
||||
# the OCI archive up -- once per tag, from the same archive, so every tag points
|
||||
# at bit-identical content.
|
||||
push_tag() {
|
||||
echo "build-image: pushing ${1}"
|
||||
skopeo copy --dest-tls-verify=false "oci-archive:${TAR_FILE}" "docker://${1}"
|
||||
}
|
||||
|
||||
push_tag "$REF"
|
||||
if [ "$PUSH_LATEST" = "1" ]; then
|
||||
push_tag "${REGISTRY}/${IMAGE}:latest"
|
||||
fi
|
||||
if [ -n "$VERSION_TAG" ]; then
|
||||
push_tag "${REGISTRY}/${IMAGE}:${VERSION_TAG}"
|
||||
fi
|
||||
|
||||
echo "✓ build-image: ${REF} pushed"
|
||||
+56
-15
@@ -11,8 +11,31 @@ env:
|
||||
IMAGE: __PROJECT_NAME__
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
name: Detect unsubstituted template
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
is_template: ${{ steps.detect.outputs.is_template }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: detect
|
||||
# The detection lives in scripts/is-template.sh so it can be TESTED.
|
||||
# It was inline here twice and wrong both times (#9, #10): the grep
|
||||
# named a placeholder literally, and the scaffolder substitutes every
|
||||
# file including this one, so in a generated repo the guard matched
|
||||
# forever and check/build/deploy all skipped while the run went green.
|
||||
#
|
||||
# An inline shell block in a workflow cannot be run by a test, which is
|
||||
# why the second fix repeated the first bug under a comment warning
|
||||
# about it. scripts/tests/is-template_test.sh now substitutes the guard
|
||||
# and asserts it still answers correctly.
|
||||
run: |
|
||||
echo "is_template=$(bash scripts/is-template.sh go.mod)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check:
|
||||
name: Lint / Test / Vet
|
||||
needs: guard
|
||||
if: needs.guard.outputs.is_template != 'true'
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -34,9 +57,9 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build & Import
|
||||
needs: check
|
||||
needs: [guard, check]
|
||||
if: needs.guard.outputs.is_template != 'true' && github.event_name != 'pull_request'
|
||||
runs-on: self-hosted
|
||||
if: github.event_name != 'pull_request'
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.sha-tag }}
|
||||
steps:
|
||||
@@ -48,24 +71,35 @@ jobs:
|
||||
SHA=$(git rev-parse --short HEAD)
|
||||
echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# infra#309: the implementation lives in .gitea/build-image.sh, a
|
||||
# byte-identical copy of mathias/infra's ci-templates/build-image.sh.
|
||||
# Do not edit it here -- edit the canonical file, run `task ci:sync`,
|
||||
# and infra's drift gate will name any repo still carrying an old copy.
|
||||
#
|
||||
# This matters more in a template than anywhere else: every repo
|
||||
# generated from here inherits its CI, so a template that carries the
|
||||
# build LOGIC manufactures a new copy of every bug in it. That is
|
||||
# precisely how rootless buildah (infra#241) reached a dozen repos, and
|
||||
# why the fix then had to be rediscovered here weeks later. A template
|
||||
# that carries a CALL manufactures nothing.
|
||||
#
|
||||
# NEW REPOS: add yourself to mathias/infra's ci-templates/adopters.txt so
|
||||
# the gate keeps this copy current. Until you do, the copy is unmanaged
|
||||
# and will silently fall behind.
|
||||
#
|
||||
# `bash <script>` rather than executing it directly: files written via
|
||||
# the Gitea API land 0644, and CI that depends on a file mode is a trap.
|
||||
- name: Build and push to local registry
|
||||
run: |
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
buildah build \
|
||||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||
-t ${REF} \
|
||||
-t ${REGISTRY}/${{ env.IMAGE }}:latest \
|
||||
.
|
||||
buildah push --tls-verify=false ${REF}
|
||||
buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest
|
||||
echo "✓ Image pushed to ${REF}"
|
||||
env:
|
||||
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
run: bash .gitea/build-image.sh
|
||||
|
||||
deploy:
|
||||
name: Deploy via GitOps
|
||||
needs: build
|
||||
needs: [guard, build]
|
||||
if: needs.guard.outputs.is_template != 'true' && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
runs-on: self-hosted
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- name: Update image tag in infra repo
|
||||
env:
|
||||
@@ -73,6 +107,13 @@ jobs:
|
||||
DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# infra#311: fail on an empty tag BEFORE patching anything. The
|
||||
# grep verification further down CANNOT catch it -- with IMAGE_TAG
|
||||
# empty its pattern matches the very corruption it exists to reject.
|
||||
# The result is `image: <repo>:` in the manifest, which is invalid
|
||||
# YAML, so Flux fails the ENTIRE apps Kustomization and every app
|
||||
# stops reconciling, not just this one. That happened on 2026-08-25.
|
||||
[ -n "${IMAGE_TAG:-}" ] || { echo "IMAGE_TAG is empty - refusing to patch"; exit 1; }
|
||||
mkdir -p ~/.ssh
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/id_infra
|
||||
chmod 600 ~/.ssh/id_infra
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,361 @@
|
||||
# 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.
|
||||
|
||||
0. **Pre-task ritual — before ANY implementation (non-negotiable).** Run this before writing a single line:
|
||||
- **Query the brain** (`brain_query`) for the domain + symptom. If the result changes your approach, surface it before acting. 5 seconds beats 5 hours.
|
||||
- **Load the relevant skill** — see trigger table in *Engineering Skills* below.
|
||||
- **Write the failing test first.** Name the test before the function. If the target is untestable (e.g. `main()` wiring), extract the logic into a testable function first. No implementation without a red test.
|
||||
- **State the observable success criterion** — what specific behavior, output, or passing test proves this is done?
|
||||
|
||||
**TDD is non-negotiable.** "Tests pass" is not proof of correctness — only proof the tests ran. Write tests that would catch the bug before writing code that fixes it.
|
||||
|
||||
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).
|
||||
5. **Trunk-Based Development — commit directly to main.** Every commit is one
|
||||
logical change (one tool, one fix, one test) with passing tests. Main is always
|
||||
deployable. Never create long-lived feature branches.
|
||||
|
||||
**The PR trigger is RISK, not concurrency.** `risk/low` work commits straight to
|
||||
main with CI as the gate — that is most work and is unchanged. `risk/medium` and
|
||||
above opens a PR, even solo, even with nobody waiting to review it. This mirrors
|
||||
the tiered merge model in `assessor-loop`
|
||||
`method/transfer-to-a-regulated-organisation.md` §4.1, so the estate practises
|
||||
what it sells instead of merging everything on green and claiming otherwise.
|
||||
|
||||
**Concurrency is NOT a reason to branch.** Measured 2026-08-29 across three real
|
||||
collisions between parallel sessions: two were not branching problems at all — a
|
||||
generated artifact written to the same path by both (branching turns that into a
|
||||
merge conflict on a 33k-line JSON), and uncommitted work in a shared checkout
|
||||
(a per-session worktree fixes it) — and the third was an ordinary non-fast-forward
|
||||
push resolved by rebase in seconds. Use a worktree per session on shared repos and
|
||||
timestamp generated artifacts. Do not reach for a branch.
|
||||
|
||||
Branch+PR flow carries documented costs on solo repos: merging auto-closes gated
|
||||
issues, review comments live on a thread separate from the issue's, and a
|
||||
merged-and-deleted branch cannot be amended (brain:
|
||||
`gitea-pr-merge-autoclose-and-review-comment-footguns`). Those are the price of
|
||||
the `risk/medium`+ lane — paid deliberately, not incurred by accident.
|
||||
|
||||
**Exception — external contributor or client four-eyes requirement:** Use PR flow
|
||||
when a human reviewer outside the project is required. Document the reason in
|
||||
PROJECT.md.
|
||||
|
||||
6. **Close the loop — every substantive task ends with the same ritual.** Shipping
|
||||
the code is not the end of the task; capturing it is. Run this unprompted:
|
||||
- **Tag + bump SemVer** on the change (annotated tag; minor for a feature or
|
||||
new/changed ADR, patch for a fix; docs in the same commit). Check the repo's
|
||||
actual last tag — stated versions in docs drift stale.
|
||||
- **Push** main and the tag (CI is the gate).
|
||||
- **Persist generalizable learnings to the brain** (`brain_write`, wing/hall) —
|
||||
the reusable patterns and the footguns that would bite anyone again, never
|
||||
project status. See *Knowledge base — when to write* below.
|
||||
- **File discovered-but-deferred work as tracker issues** on the project's own
|
||||
repo — token-budget gaps, recorded ADR limitations, v2 follow-ups. Don't let
|
||||
"out of scope, recorded" rot in a commit message; make it a ticket with a
|
||||
source pointer.
|
||||
- Surface the brain entries and issue numbers in the closing summary so the
|
||||
trail is auditable.
|
||||
|
||||
**Chat is not a durable store. Never rely on Mathias remembering anything.**
|
||||
Every gap, caveat, risk, open question and next step must land in one of four
|
||||
places before the turn ends: **the brain**, **a repo** (code, docs, ADR),
|
||||
**a tracker issue**, or **a plan file**. A finding stated only in conversation is
|
||||
lost the moment the session closes — and this estate has repeatedly proved that a
|
||||
remembered convention is an unreliable control.
|
||||
|
||||
This applies hardest to what is tempting to leave as a closing remark: "worth
|
||||
knowing X isn't enforced yet", "this is a convention, not a gate", "we should
|
||||
revisit Y". Those are exactly the items that need a locator. If it is worth
|
||||
saying in a summary, it is worth writing where it can be found again without you.
|
||||
|
||||
## CAD sprint patterns
|
||||
|
||||
Two CAD sprint archetypes (defs: brain `wiki/homelab/decisions/continuous-agentic-development-cad-concept-2026-06-16`):
|
||||
- **Inception Sprint** — birth a new repo. Oath-gated close; tag only once every clause holds. Brain: `.../inception-sprint-and-oath`.
|
||||
- **Sweep Sprint** — clear an existing backlog. Parallel read-only research per issue → sequential one-issue-per-commit execution. Brain: `.../sweep-sprint-and-oath`.
|
||||
|
||||
Shared: decisions that are yours, not the agent's, surface as `DECISION NEEDED` (question/options/recommendation/blocks) — ask live if you're present, else comment and wait, never act past it. Live-breaking regressions get `git revert`, never forward-fixed; host-network work skips even read-only research (solo, synchronous, you present). Rule 6's closing ritual only fires once every Oath clause genuinely holds — blocked = descoped-and-tracked, never marked done.
|
||||
|
||||
## 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 | pgvector (vector), BM25 | Qdrant (when >1M vectors or hybrid retrieval) | — |
|
||||
| Logging | slog (structured) | stdlib `logging` w/ structured `extra=` (or structlog) | — |
|
||||
| Testing | Table-driven, testify | — | — |
|
||||
| Agents (Go) | google.golang.org/adk + pkg/litellm adapter | — | — |
|
||||
|
||||
Exploratory: Rust, Zig — I'll tell you when I want these.
|
||||
|
||||
## Code conventions
|
||||
|
||||
- **Go style**: golines, gofumpt, golangci-lint
|
||||
- **Python style** (fallback language): ruff (format+lint, one tool), mypy --strict (non-negotiable,
|
||||
matches Go's static typing discipline), pytest + pytest-cov (table-driven via
|
||||
`@pytest.mark.parametrize`), uv (venv+deps+lock, one tool), pydantic-settings (typed env-var config
|
||||
— same principle as Go's typed structs), src-layout + `pyproject.toml` only (no `setup.py`)
|
||||
- **Errors**: `fmt.Errorf("operation: %w", err)` — never naked, never log-and-return.
|
||||
Python: `raise X from e` (exception chaining, same principle) — never bare `except`, never silent `pass`
|
||||
- **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:`), commit directly to main,
|
||||
one logical change per commit, CI is the quality gate
|
||||
- **Never**: long-lived feature branches, PRs for solo work, direct push without
|
||||
passing `task check` locally first
|
||||
- **Security**: no secrets in code, govulncheck before adding deps, SOPS for encrypted config
|
||||
- **Dependencies**: prefer stdlib. testify, slog, templ, sqlc, google.golang.org/adk (agent projects only) are pre-approved; anything else needs justification in the commit message
|
||||
|
||||
## Secret handling (every harness, every command)
|
||||
|
||||
Tool output is persisted: terminal → `~/.claude/projects` transcripts →
|
||||
claudewatcher → brain/wiki → gitea history. A secret printed once is
|
||||
searchable forever, and clearing it means rotating the key. So:
|
||||
|
||||
1. **Never print, echo, log, or transform a secret to inspect it.** No
|
||||
`base64`/`xxd`/`cat` of a key, and never pipe a secret through a transform
|
||||
to defeat `op run`'s output masking (it masks raw values; base64 hides them
|
||||
from the mask — that exact trick leaked a key on 2026-06-11).
|
||||
2. **Secrets stay in the subprocess.** Reference them only as env vars consumed
|
||||
*inside* `op run --env-file ~/.op-env -- <cmd>`. Never place a literal secret
|
||||
in a command's argv (it lands in the tool call and the transcript).
|
||||
3. **Existence check without revealing the value:** `[ -n "$X" ] && echo set` —
|
||||
never `${X:-...}` (returns the value when set) and never echo a substring of it.
|
||||
4. **Cross-host secrets:** run the secret-consuming command on the host that has
|
||||
the secret; do not forward a raw key over ssh argv/stdout.
|
||||
5. If a secret does leak into output, say so immediately and flag it for rotation —
|
||||
don't bury it.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
Three machines on Tailscale:
|
||||
|
||||
| Machine | Role | Key specs |
|
||||
|---------|------|-----------|
|
||||
| koala | GPU inference, heavy compute | RTX 5070, runs k3s + llama-swap + shared postgres18/pgvector |
|
||||
| iguana | Services, builds | M2 Ultra Mac |
|
||||
| flamingo | Daily driver, edge | Apple Silicon Mac, ~/dev is here |
|
||||
|
||||
- **Model routing**: LiteLLM in front of llama-swap (local) + cloud APIs (when permitted)
|
||||
- **Orchestration**: k3s on koala only — single control-plane node, no workers (verified 2026-08-30). iguana and flamingo are on the tailnet but are not cluster members
|
||||
- **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 — actively use it
|
||||
|
||||
A persistent brain (BM25 search + LLM-synthesised Q&A) survives across sessions,
|
||||
hosts, and harnesses. It holds 100+ hard-won entries: infra incident postmortems,
|
||||
Go pitfalls, framework gotchas, design principles, ADRs. **It is not optional
|
||||
reference material — query it actively, not just when explicitly told.**
|
||||
|
||||
### When to query (treat as a reflex)
|
||||
|
||||
- **Before** starting a non-trivial task — search for prior art with the symptom
|
||||
AND the system component ("how did we solve X in Y?"). 5 seconds beats 5 hours.
|
||||
- **When debugging** — search for the error string, the stack frame, the affected
|
||||
service. Past you may have already paid this tax.
|
||||
- **Before adopting** a pattern, library, framework, or model name — check if it
|
||||
was tried and rejected, or what the integration footguns are.
|
||||
- **When making architectural decisions** — search for the domain + "ADR" or
|
||||
"decision" to find prior reasoning before re-deriving it.
|
||||
- **When a recommendation feels novel** — challenge yourself: "has this been
|
||||
documented?" The brain often has it.
|
||||
|
||||
### When to write
|
||||
|
||||
After you discover something that **future-you would forget** and that **isn't
|
||||
recoverable from the code, git log, or PR description alone**:
|
||||
|
||||
- Bugs whose root cause is non-obvious and generalisable beyond this project.
|
||||
- Framework / library / model-name quirks that bit you and would bite anyone.
|
||||
- Design principles validated under fire (e.g. "every `_get` needs a `_list`").
|
||||
- Postmortems for incidents: what broke, why, how diagnosed, what to do next time.
|
||||
|
||||
DON'T write project status, sprint progress, PR summaries, or "what I did this
|
||||
session" — those rot fast and the originals are in git/gitea anyway. Brain
|
||||
entries that age well are about *why*, *how to avoid*, and *what to do when*.
|
||||
|
||||
### How to access (per harness)
|
||||
|
||||
| Harness | Query | Write |
|
||||
|---------|-------|-------|
|
||||
| **Claude Code, Claude Desktop** | `brain_query` (BM25), `brain_answer` (LLM-synth + sources) MCP tools | `brain_write` MCP tool |
|
||||
| **Crush, Pi, Antigravity, other MCP-capable** | same MCP server: `ingestion-brain` (via the `mcp__*_brain__*` namespace once authenticated) | same |
|
||||
| **Anything HTTP-only (curl, scripts)** | `POST https://brain-mcp.d-ma.be/query` with `{"query":"..."}` (auth via `BRAIN_MCP_TOKEN`) | `POST .../write` with `{"content":"...","filename":"..."}` |
|
||||
| **Browser / human inspection** | `https://git.d-ma.be/mathias/hyperguild` → `knowledge/` and `wiki/` markdown files |
|
||||
|
||||
- **Scoping**: defaults to `public` collection; client projects filter to `{client}` + `public`.
|
||||
- **Routing**: brain_answer's LLM uses berget.ai as primary, iguana ollama as
|
||||
fallback. Both are configurable in the `supervisor/ingestion-deployment.yaml`
|
||||
on the koala k3s cluster; don't hardcode local-only model names into the
|
||||
berget URL (see knowledge entry on namespace mismatches).
|
||||
|
||||
### Quick reflex checks
|
||||
|
||||
If you find yourself about to say any of these out loud, you owe yourself a brain query first:
|
||||
|
||||
- "I think the issue might be..."
|
||||
- "Let me try X and see..."
|
||||
- "I'll just write a script to..."
|
||||
- "This is probably a new bug..."
|
||||
- "Has anyone done this before?" — *yes, probably, go check.*
|
||||
|
||||
## 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
|
||||
|
||||
Supported harnesses (2026-09-01) — Cursor and Aider were dropped; do not
|
||||
re-add adapters for them:
|
||||
- Claude Code, Antigravity, OpenCode, Mistral Vibe, Crush, Pi Coding Agent
|
||||
- Open WebUI, Charmbracelet Mods, or any tool that takes a system prompt string
|
||||
- Any other tool that reads `AGENTS.md`, which is the de facto convention
|
||||
|
||||
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`, `.context/system-prompt.txt`
|
||||
|
||||
Repos still carrying `.cursorrules`, `.aider.conventions.md` or
|
||||
`.aider.conf.yml` are stale: nothing regenerates them. Delete them the next time
|
||||
you touch the repo, along with their entries in that repo's `task check`
|
||||
assertion.
|
||||
|
||||
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. Which file a harness reads does
|
||||
NOT split cleanly by lookup strategy, so do not reason from the strategy:
|
||||
- `CLAUDE.md` — Claude Code (walks up the tree)
|
||||
- `AGENTS.md` — Crush, Pi, Antigravity (cwd-only), OpenCode (project root,
|
||||
global at `~/.config/opencode/AGENTS.md`), and Mistral Vibe, which **walks
|
||||
up** from cwd and takes the first `AGENTS.md` it finds
|
||||
(user-level `~/.vibe/AGENTS.md`)
|
||||
- `.context/system-prompt.txt` — Open WebUI, Mods, raw API calls; not read by
|
||||
any harness, it is for pasting
|
||||
|
||||
Vibe tree-walks for `AGENTS.md`, so "tree-walkers use CLAUDE.md" is false.
|
||||
Each repo's `AGENTS.md` embeds the canonical and is the first one found from
|
||||
inside that repo, so this works — but by construction, not by luck.
|
||||
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 live in the **`mathias/skills`** repo (`git.d-ma.be/mathias/skills`). Load at task start — not "on demand" but on schedule, before writing code. Browse `SKILLS_INDEX.md` for the full list.
|
||||
|
||||
**Claude Code** installs the repo as a plugin from the marketplace the same repo hosts. That path also carries the `secret-guard` and `pre-task-ritual` hooks and the brain/gitea/infra MCP servers, which symlinks cannot:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||
/plugin install skills@mathias
|
||||
```
|
||||
|
||||
Skills are **namespaced** under Claude Code: load `tdd` as `skills:tdd`. The trigger table below uses bare names — prefix them.
|
||||
|
||||
**Crush, Pi and OpenCode** have no plugin system and are wired by `install.sh` as one directory symlink per harness via `~/.agents/skills`. Do not clone the checkout into `~/dev`: that folder is iCloud-shared between the Macs and spans two path roots (`/Users/mathias` vs `/home/mathias` on koala), so a checkout there is a git working tree with two concurrent writers, and any absolute symlink stored in it is broken on one side by construction (skills#23). Leave `SKILLS_CHECKOUT_DIR` at its host-local default.
|
||||
|
||||
Check a host with `task doctor` in the skills repo; it also runs automatically at session start via the plugin.
|
||||
|
||||
**Skill trigger table — load before starting, not after getting stuck:**
|
||||
|
||||
| Task type | Load |
|
||||
|-----------|------|
|
||||
| Any feature or bug fix | `tdd` |
|
||||
| Refactor or design | `clean-code` or `solid` |
|
||||
| Debug | `problem-analysis` |
|
||||
| Review code or PRs | `code-review` |
|
||||
| Frame a problem before coding | `problem-analysis` |
|
||||
|
||||
---
|
||||
|
||||
# __PROJECT_NAME__
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: __PROJECT_NAME__
|
||||
- **Owner**: Mathias
|
||||
- **Client**: personal
|
||||
- **Repo**: __MODULE_PATH__
|
||||
- **Status**: active
|
||||
|
||||
## Stack
|
||||
|
||||
Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions.
|
||||
@@ -0,0 +1,13 @@
|
||||
# __PROJECT_NAME__
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: __PROJECT_NAME__
|
||||
- **Owner**: Mathias
|
||||
- **Client**: personal
|
||||
- **Repo**: __MODULE_PATH__
|
||||
- **Status**: active
|
||||
|
||||
## Stack
|
||||
|
||||
Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions.
|
||||
@@ -24,3 +24,15 @@ tasks:
|
||||
- golangci-lint run ./...
|
||||
- go vet ./...
|
||||
- go test ./... -race -count=1
|
||||
# The template guard decides whether CI runs at all in every child repo,
|
||||
# so its regression test belongs in the same gate as the Go tests.
|
||||
- bash scripts/tests/is-template_test.sh
|
||||
|
||||
context:sync:
|
||||
desc: Regenerate all harness-specific context files
|
||||
cmds:
|
||||
- bash scripts/context-sync.sh
|
||||
context:sync:claude:
|
||||
cmds: [bash scripts/context-sync.sh claude]
|
||||
context:sync:agents:
|
||||
cmds: [bash scripts/context-sync.sh agents]
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/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
|
||||
#
|
||||
# Cursor and Aider adapters removed 2026-09-01 — neither is a supported harness
|
||||
# any more. Supported: Claude Code, Antigravity, OpenCode, Mistral Vibe, Crush,
|
||||
# Pi. All read CLAUDE.md or AGENTS.md; system-prompt.txt remains for Open WebUI,
|
||||
# Mods and raw API use.
|
||||
#
|
||||
# Usage: ./scripts/context-sync.sh [--force] [adapter...]
|
||||
# Task: task context:sync
|
||||
#
|
||||
# Override root context: ROOT_CONTEXT=~/dev/.context/AGENT.md ./scripts/context-sync.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Parse --force flag and collect adapter names separately
|
||||
FORCE=false
|
||||
ADAPTERS=()
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--force) FORCE=true ;;
|
||||
*) ADAPTERS+=("$_arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROJECT_FILE=".context/PROJECT.md"
|
||||
|
||||
# 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
|
||||
|
||||
# Pre-flight: reject unfilled {{...}} placeholders unless --force
|
||||
if [ "$FORCE" = false ]; then
|
||||
_placeholders=$(grep -n '{{[^}]*}}' "$PROJECT_FILE" 2>/dev/null || true)
|
||||
if [ -n "$_placeholders" ]; then
|
||||
echo "Error: unfilled placeholders in $PROJECT_FILE:" >&2
|
||||
while IFS= read -r _match; do
|
||||
_lineno="${_match%%:*}"
|
||||
_content="${_match#*:}"
|
||||
_token=$(printf '%s' "$_content" | grep -o '{{[^}]*}}' | head -1)
|
||||
echo " $PROJECT_FILE:$_lineno: unfilled placeholder $_token" >&2
|
||||
done <<< "$_placeholders"
|
||||
echo "" >&2
|
||||
echo "Fill these placeholders, then re-run: task context:sync" >&2
|
||||
echo "To bypass validation: bash scripts/context-sync.sh --force" >&2
|
||||
exit 1
|
||||
fi
|
||||
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)"
|
||||
}
|
||||
|
||||
# ── 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() {
|
||||
# Ensure baseline file exists with project-specific knowledge server
|
||||
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
|
||||
fi
|
||||
|
||||
# Merge root mcp-servers.json if found alongside root AGENT.md
|
||||
local root_mcp=""
|
||||
if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then
|
||||
local candidate
|
||||
candidate="$(dirname "$ROOT_CONTEXT")/mcp-servers.json"
|
||||
[ -f "$candidate" ] && root_mcp="$candidate"
|
||||
fi
|
||||
|
||||
if [ -z "$root_mcp" ]; then
|
||||
echo " → .context/mcp.json (exists, no root mcp-servers.json found)"
|
||||
return
|
||||
fi
|
||||
|
||||
# Root servers take precedence over project entries on key conflict
|
||||
local root_servers count updated
|
||||
root_servers=$(jq '.servers' "$root_mcp")
|
||||
count=$(printf '%s' "$root_servers" | jq 'keys | length')
|
||||
updated=$(jq --argjson root "$root_servers" \
|
||||
'.mcpServers = (.mcpServers + $root)' \
|
||||
.context/mcp.json)
|
||||
printf '%s\n' "$updated" > .context/mcp.json
|
||||
echo " → .context/mcp.json (merged $count root servers)"
|
||||
}
|
||||
|
||||
echo "Syncing project context from $PROJECT_FILE..."
|
||||
|
||||
if [ ${#ADAPTERS[@]} -eq 0 ]; then
|
||||
generate_claude
|
||||
generate_agents
|
||||
generate_system_prompt
|
||||
generate_mcp
|
||||
else
|
||||
for adapter in "${ADAPTERS[@]}"; do
|
||||
case "$adapter" in
|
||||
claude) generate_claude ;;
|
||||
agents) generate_agents ;;
|
||||
prompt|system|openwebui|owui|generic) generate_system_prompt ;;
|
||||
mcp) generate_mcp ;;
|
||||
*) echo "Unknown adapter: $adapter" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Is this checkout still a raw, unsubstituted template?
|
||||
#
|
||||
# Prints "true" or "false". cd.yml's guard job reads it and skips check/build/
|
||||
# deploy when true, so that a template repo does not try to build itself.
|
||||
#
|
||||
# WHY A PATTERN AND NOT A LITERAL
|
||||
#
|
||||
# The scaffolders (gitea-mcp create_project_from_template, local-dev
|
||||
# scripts/new-project.sh) substitute placeholders in EVERY file of the tree —
|
||||
# including this one. So a guard that greps a whole placeholder token is
|
||||
# rewritten by the very substitution it exists to detect: in the template it
|
||||
# greps the module-path placeholder, and in the generated repo that same line
|
||||
# has become a grep for the real module path — which the generated go.mod
|
||||
# declares on its first line. It matches forever, every job below skips, and the
|
||||
# run reports green having built nothing.
|
||||
#
|
||||
# That shipped twice: #9 (cad-atlas) and #10 (openbanking-api), the second one
|
||||
# sitting directly under a comment warning against exactly this.
|
||||
#
|
||||
# Note this comment names no placeholder literally, for the same reason. A
|
||||
# comment that gets substituted stops explaining the thing it documents — and
|
||||
# scripts/tests/is-template_test.sh enforces that, which is how the first draft
|
||||
# of this file was caught.
|
||||
#
|
||||
# The regex below matches the SHAPE of a placeholder and contains no complete
|
||||
# token, so there is nothing for a scaffolder to rewrite. It also catches any
|
||||
# other leftover placeholder, not just the two the scaffolders know about.
|
||||
#
|
||||
# scripts/tests/is-template_test.sh substitutes a copy of this file and asserts
|
||||
# it still answers correctly — that case is the regression test for the class.
|
||||
set -euo pipefail
|
||||
|
||||
target="${1:-go.mod}"
|
||||
|
||||
if [ ! -f "$target" ]; then
|
||||
# Never answer "false" here. A missing target would skip CI on a repo that is
|
||||
# not a template at all, which is the same fake-green failure by another road.
|
||||
echo "is-template: no such file: $target" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if grep -qE '__[A-Z][A-Z0-9_]*__' "$target"; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for scripts/is-template.sh
|
||||
#
|
||||
# This guard decides whether a checkout is still a raw template. Every job in
|
||||
# cd.yml keys off it, so when it answers wrongly the entire pipeline skips and
|
||||
# reports green — which is how `cad-atlas` and `openbanking-api` were born with
|
||||
# CI that had never built, tested or deployed them (#9, #10).
|
||||
#
|
||||
# The defect both times was the same: the guard grepped for a literal
|
||||
# placeholder, and the scaffolder substitutes placeholders in EVERY file,
|
||||
# including the guard itself. So in a generated repo the check became
|
||||
# `grep -q 'git.d-ma.be/mathias/<name>' go.mod` against a go.mod that reads
|
||||
# `module git.d-ma.be/mathias/<name>` — a match that can never fail.
|
||||
#
|
||||
# The third test below is the one that matters: it runs a scaffolder-style
|
||||
# substitution OVER THE GUARD and asserts it still answers correctly. Both
|
||||
# shipped bugs would have failed it.
|
||||
#
|
||||
# Usage: ./scripts/tests/is-template_test.sh
|
||||
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SCRIPT="$ROOT/scripts/is-template.sh"
|
||||
|
||||
pass=0; fail=0
|
||||
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
|
||||
bad() { printf ' FAIL %s\n %s\n' "$1" "${2:-}"; fail=$((fail+1)); }
|
||||
|
||||
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
||||
echo "is-template tests"
|
||||
|
||||
# Placeholder tokens are ASSEMBLED, never written whole. The scaffolder rewrites
|
||||
# every file in the tree, this one included -- a fixture containing a literal
|
||||
# token would be substituted before the test ran, and the suite would fail in
|
||||
# every generated repo while passing here. Which is the same class of bug the
|
||||
# guard itself had.
|
||||
U="__"
|
||||
ph() { printf '%s%s%s' "$U" "$1" "$U"; }
|
||||
MODULE_PH="$(ph MODULE_PATH)"
|
||||
PROJECT_PH="$(ph PROJECT_NAME)"
|
||||
OTHER_PH="$(ph SOME_OTHER)"
|
||||
|
||||
if [ -f "$SCRIPT" ]; then ok "script exists"
|
||||
else bad "script exists" "$SCRIPT missing"; echo; echo "$pass passed, $fail failed"; exit 1; fi
|
||||
|
||||
# A raw template still carries placeholders.
|
||||
printf 'module git.d-ma.be/mathias/%s\n\ngo 1.26\n' "$MODULE_PH" > "$tmp/raw.go.mod"
|
||||
got="$(bash "$SCRIPT" "$tmp/raw.go.mod")"
|
||||
[ "$got" = "true" ] && ok "raw template is detected" \
|
||||
|| bad "raw template is detected" "got '$got', want 'true'"
|
||||
|
||||
# A generated repo does not. This is the assertion both shipped guards failed.
|
||||
printf 'module git.d-ma.be/mathias/openbanking-api\n\ngo 1.26\n' > "$tmp/real.go.mod"
|
||||
got="$(bash "$SCRIPT" "$tmp/real.go.mod")"
|
||||
[ "$got" = "false" ] && ok "a generated repo is NOT detected as a template" \
|
||||
|| bad "a generated repo is NOT detected as a template" "got '$got', want 'false'"
|
||||
|
||||
# THE ONE THAT MATTERS.
|
||||
#
|
||||
# Copy the guard and run a scaffolder-style substitution over it, exactly as
|
||||
# create_project_from_template and new-project.sh do to every file in the tree.
|
||||
# The substituted guard must still answer correctly. A guard that greps a whole
|
||||
# placeholder token gets rewritten here and starts matching forever.
|
||||
cp "$SCRIPT" "$tmp/substituted.sh"
|
||||
sed -i \
|
||||
-e "s|$MODULE_PH|git.d-ma.be/mathias/openbanking-api|g" \
|
||||
-e "s|$PROJECT_PH|openbanking-api|g" \
|
||||
"$tmp/substituted.sh"
|
||||
|
||||
got="$(bash "$tmp/substituted.sh" "$tmp/real.go.mod")"
|
||||
if [ "$got" = "false" ]; then
|
||||
ok "the guard survives being substituted itself"
|
||||
else
|
||||
bad "the guard survives being substituted itself" \
|
||||
"after substitution it answered '$got' for a generated repo — this is #9/#10"
|
||||
fi
|
||||
|
||||
got="$(bash "$tmp/substituted.sh" "$tmp/raw.go.mod")"
|
||||
[ "$got" = "true" ] && ok "the substituted guard still detects a raw template" \
|
||||
|| bad "the substituted guard still detects a raw template" "got '$got'"
|
||||
|
||||
# Belt and braces: the source must not contain a complete substitutable token.
|
||||
if grep -qE "$(printf '%s(MODULE_PATH|PROJECT_NAME)%s' "$U" "$U")" "$SCRIPT"; then
|
||||
bad "the guard contains no substitutable placeholder token" \
|
||||
"found one in $SCRIPT — the scaffolder will rewrite it"
|
||||
else
|
||||
ok "the guard contains no substitutable placeholder token"
|
||||
fi
|
||||
|
||||
# Any leftover placeholder counts, not just the two the scaffolder knows about —
|
||||
# a half-substituted tree is not a working repo either.
|
||||
printf 'module git.d-ma.be/mathias/thing\n\n// %s\ngo 1.26\n' "$OTHER_PH" > "$tmp/partial.go.mod"
|
||||
got="$(bash "$SCRIPT" "$tmp/partial.go.mod")"
|
||||
[ "$got" = "true" ] && ok "any leftover placeholder counts" \
|
||||
|| bad "any leftover placeholder counts" "got '$got', want 'true'"
|
||||
|
||||
# A missing file is not "this is a real repo" — failing open here would skip CI.
|
||||
bash "$SCRIPT" "$tmp/nope.go.mod" >/dev/null 2>&1
|
||||
rc=$?
|
||||
[ "$rc" -ne 0 ] && ok "a missing target fails loudly rather than answering false" \
|
||||
|| bad "a missing target fails loudly rather than answering false" "exit 0"
|
||||
|
||||
echo
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user