18 Commits

Author SHA1 Message Date
Paul O'Reilly
916acd39e1 docs(memory): add /spawn-session skill reference
Per the skill convention, document /spawn-session (purpose, usage, how it
wraps claude-tmux, gotchas) and index it in MEMORY.md.

Claude-Session: https://claude.ai/code/session_014oa7T7z1h5n34vu8vX9zGT
2026-07-25 16:29:46 +12:00
Paul O'Reilly
5623f75c3b docs: impeccable skill maintenance/install reference + CLAUDE.md pointer row
Claude-Session: https://claude.ai/code/session_01DTLFEx7r7AKsLerGtsnu8g
2026-07-23 09:39:51 +12:00
Paul O'Reilly
091fae73e8 Pre-flight: check ssh-add -l before asking user to load a key
Keys are often pre-loaded. Added explicit check-first guidance and
a note to use grep -i (case-insensitive) — a case-sensitive grep
will silently miss keys whose comments use different capitalisation.
2026-07-18 11:00:52 +12:00
Paul O'Reilly
24dcf3bcfc docs: update ask-minimax-usage.md model reference to M3
Claude-Session: https://claude.ai/code/session_01XtWRazqRbMnMRa4iV3zbVx
2026-07-17 21:22:41 +12:00
Paul O'Reilly
7838db4f26 feat(hooks): allow projects to opt out of require-plan-file check
Add a sentinel-file escape hatch: if `.claude/skip-plan-file-check`
exists in the project's cwd, the ExitPlanMode hook exits 0 without
demanding a local *-PLAN.md file.

Needed by projects (e.g. agent-runtimes) that manage plans via an
external system (work-items CP) rather than local repo files.
2026-07-12 11:40:07 +12:00
Paul O'Reilly
b41677f831 CLAUDE.md: add Gitea CI visibility section pointing at gitea-agent-companion (gac --help) 2026-07-07 17:52:19 +12:00
Paul O'Reilly
d547fe08e0 distill: fix + expand tracked-project list in settings.yaml
Correct two stale paths (octopus/customers/{duel-image,slb} -> octopus/{duel-image,slb})
and add 5 previously-untracked projects to distill scope: agent-runtimes-deploy,
cluster-apps/agent-runtimes, hugo-gabby-oreilly-counselling-content,
hugo-oreillyconsulting-integration, octopus/policy-demo.
2026-07-02 15:59:08 +12:00
Paul O'Reilly
d13beca5d7 docs: insights-driven guardrails — SSH pre-flight, bash safety, infra approval, dispatch pre-flight, session end, ask-minimax fallback 2026-06-12 21:37:26 +12:00
Paul O'Reilly
f41c22d0ac Add question-reframing guidance to CLAUDE.md; commit accumulated project files
- CLAUDE.md: add "Question the question" and "One clarifying question" rules
  to Tone and Interaction — XY problem detection, false premise checks, and
  explicit reframe pattern before answering
- Add claude/ detail-file directory (topic docs referenced from CLAUDE.md)
- Add ABOUT.md, FUTURE.md
- Update memory/, scripts/, settings.yaml with accumulated session changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:37:28 +12:00
Paul O'Reilly
6dfa20c47c End-of-session: transcript tracking log, context update, README docs
- Add session log (2026-04-13) covering transcript backup tracking system
- Add context/transcript-tracking.md: first-run verification checklist for
  the Sonnet subagent path in /log
- Update CONTEXT.md with transcript tracking entry
- Update README.md: document extract-transcripts.py and list-transcripts-here.sh;
  note that install-hooks.sh now covers skill-helper scripts too
- Prune 3 reflected logs older than retention_days (7): 2026-03-23, -25, -29

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:09:22 +12:00
Paul O'Reilly
6fd0dac218 Add transcript backup tracking system and pre-compact hook improvements
- pre-compact-backup.sh: derive transcript path from session_id+cwd (no
  longer relies on transcript_path field that Claude Code stopped providing);
  register each backup in tracking.json after saving
- extract-transcripts.py: new script managing tracking.json — register,
  list, extract conversation text, mark-processed modes
- list-transcripts-here.sh: thin wrapper for extract-transcripts --list
  using $(pwd); needed because SKILL.md bang commands reject $() substitution
- install-hooks.sh: now also symlinks skill-helper scripts into
  ~/.claude/scripts/ via a curated SKILL_HELPERS list
- Memory docs: new script-extract-transcripts.md, script-list-transcripts-here.md;
  updated skill-log.md, script-install-hooks.md, MEMORY.md index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:06:09 +12:00
Paul O'Reilly
52b0f2b5c6 Add session log and reflect: API design research session
Session log for 2026-03-29 covering API/LLM security research, agent-runtimes
audit, and review skill creation. Reflected into:
- decisions.md: best-practices topic split, review skills read-only design
- process-lessons.md: background agents for research, Edit context
  disambiguation, best-practices repo location

Pruned 2 old reflected logs (2026-03-17, 2026-03-18).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:22:37 +13:00
Paul O'Reilly
884b416d38 Add memory files for review-plan and review-spec skills
Skill documentation for the new plan/spec review skills. Each file
covers purpose, usage, how it works, referenced best practice files,
and gotchas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:11:35 +13:00
Paul O'Reilly
3b32ee194b Add git pull before planning work to session start flow
Projects are now worked on from multiple machines and by container
agents, so local state can be behind the remote. Pull with --ff-only
on project entry and warn if it fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:51:38 +13:00
Paul O'Reilly
cace9600ca Migrate best practices to skynet/best-practices repo
Remove all best-practices topic files and .distill-state.json from
claude-foundations. BESTPRACTICES.md now redirects to the standalone
repo at ~/dev/claude/projects/best-practices/. Remove the now-unnecessary
sync-best-practices.sh script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:48:51 +13:00
Paul O'Reilly
25918bf028 Add database selection best practice: SQLite is not a production database
Hard rule: any service with a FQDN, multiple consumers, or concurrent
access MUST use PostgreSQL from day one. Documents the cost of "we'll
migrate later" based on the Gitea SQLite→PostgreSQL migration that cost
nearly a full day of productivity.

Extracted from cluster-bootstrap gitea-scaling session (2026-03-28).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:23:22 +13:00
Paul O'Reilly
d4eafeb38e Add security architecture best practice: server boundary rule
No server-side credential may cross the server boundary to the client.
The only credentials that cross are the client's own identity (MFA, OIDC, etc.).

Covers: proxy + identity mapping pattern, defense in depth layers,
anti-patterns (browser tokens, shared service accounts, embedded keys),
presigned URL exception for direct uploads, CMS real-world example.

Extracted from hugo-accelerator M4 where this principle drove the
CMS proxy architecture (Authelia + per-user Gitea tokens server-side).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:23:22 +13:00
e78bf5a936 Merge pull request 'Reference skynet/best-practices repo' (#1) from agent/cf-bp-redirect into main 2026-03-27 23:21:21 +00:00
72 changed files with 1516 additions and 2318 deletions

View File

@@ -1,6 +1,6 @@
{
"version": 1,
"last_run": "2026-03-24T10:36:46Z",
"last_run": "2026-03-28T22:15:17Z",
"processed": {
"log/2026-03-12.233744.md": "ca482f03914a3b4dec89c2dbf6e0f2e5",
"log/2026-03-13.100758.md": "65c5b65fdd5984c036f1de53f8c82f2a",
@@ -8,6 +8,8 @@
"log/2026-03-15.225345.md": "ae9366aed0b4c87ecbbe535e00cb2cb2",
"log/2026-03-17.103204.md": "56c04b01459f80134e680150c338e9eb",
"log/2026-03-18.002319.md": "68ab608a650421dcc663fd4ae7131fc9",
"log/2026-03-23.123545.md": "2610a8adb53e4b831e3e4b0c1d7cad44"
"log/2026-03-23.123545.md": "2610a8adb53e4b831e3e4b0c1d7cad44",
"log/2026-03-25.110612.md": "adb467a37c6da84adb5ab5bbf20c773a",
"log/2026-03-29.111336.md": "87ad0d241f002605863f66d7fb36c863"
}
}

1
ABOUT.md Normal file
View File

@@ -0,0 +1 @@
description: Shared hooks, formatters, and knowledge pipeline for Claude Code sessions

View File

@@ -1,28 +1,9 @@
> **Note:** Best practices are now maintained in [skynet/best-practices](https://gitea.oreillyit.nz/skynet/best-practices).
> AI agents with /best-practices mounted should read /best-practices/INDEX.md instead.
> The content below is kept as a local copy for context-load compatibility.
# Best Practices
# Best Practices Index
Best practices have moved to their own repository: **[skynet/best-practices](https://gitea.oreillyit.nz/skynet/best-practices)**
Generalised best practices extracted from real project work. Each topic file is self-contained — read only the files relevant to the current project.
- **Local clone:** `~/dev/claude/projects/best-practices/`
- **Index:** `~/dev/claude/BESTPRACTICES.md` (symlink to the clone)
- **Container agents:** cloned to `/best-practices` by the entrypoint
## Topics
- [Validation & Deployment](best-practices/validation.md) — Validate locally, deploy once; full-chain testing; pre-flight checks
- [Secrets Management](best-practices/secrets-management.md) — SOPS + age, credential handling, file naming, encryption gotchas
- [Git & Source Control](best-practices/git-source-control.md) — Commit practices, GitOps workflows, remote conventions
- [Kubernetes Patterns](best-practices/kubernetes.md) — Volume mounts, deployment strategies, naming, bootstrap ordering
- [Helm Charts](best-practices/helm.md) — Schema validation, version verification, values structure
- [Ansible](best-practices/ansible.md) — Inventory, templates, idempotency, credential safety
- [Scripting](best-practices/scripting.md) — Shell conventions, verification scripts, idempotency, colour output
- [Documentation Standards](best-practices/documentation.md) — CLAUDE.md, MEMORY.md, FUTURE.md, README.md structure and tiered memory
- [Milestones & Reflections](best-practices/milestones.md) — Milestone workflow, verification, reflection process
- [Debugging Methodology](best-practices/debugging.md) — Systematic diagnosis, full-chain testing, common pitfalls
- [Claude Code Skills](best-practices/skills-development.md) — Skill authoring, context injection, tool restrictions
- [Linting & Formatting](best-practices/linting.md) — Tool choices per language, PostToolUse hook, pre-commit integration, formatter contract
- [Spec-Driven Development](best-practices/spec-driven-development.md) — Spec structure, requirement numbering, test-first workflow, context tiers, anti-patterns
- [Test-Driven Development](best-practices/test-driven-development.md) — Edge case discovery, property-based testing, mutation testing, AI agent testing patterns, test architecture
- [Networking & Infrastructure](best-practices/networking.md) — nftables safety, systemd socket activation, Docker forwarding, TLS SNI vs Host header, wildcard certs
- [Docker UID Matching](best-practices/docker-uid-matching.md) — UID wrapper entrypoint for mounted volumes, gosu pattern, when to use vs K8s securityContext
- [Octopus Process Templates](best-practices/octopus-process-templates.md) — OCL syntax, step template references, channel scoping, parameters, versioning, Platform Hub patterns
- [Platform Hub Knowledge Base](PLATFORMHUB.md) — Comprehensive guide: architecture, OCL syntax, template patterns, gotchas, API reference
Read `BESTPRACTICES.md` in the best-practices repo for the full topic index.

340
CLAUDE.md
View File

@@ -1,8 +1,39 @@
# CLAUDE.md — General Project Guidelines
This file is the always-on guardrails. Topic-specific detail lives in [`claude/`](./claude/) (symlinked from `~/dev/claude/claude/`). Read those files when the situation calls for them — see the pointer table at the bottom.
## Tone and Interaction
**No hedge words.** Less is more.
**No intro paragraph.** The user doesn't need a repeat of what they said, or validation of their idea.
**No validation.** Give direct and honest feedback. Do not validate an idea just because it exists.
**Question the question.** Before answering, check: is this the right question? Common failure modes:
- **XY problem**: the question asks how to do X, but X is a means to an unstated goal Y with a better path. Name the Y and address that.
- **False premise**: the question assumes something that may be wrong. State the assumption and challenge it before answering.
- **Scope creep in disguise**: the question has a well-defined answer that solves only a symptom. Note the larger problem if it's obvious.
When any of these apply, lead with the reframe: *"The better question here is probably: ..."* — then answer that.
**One clarifying question, not many.** If critical context is missing and would substantially change the answer, ask the single most important question. Don't fire off a list.
## Session Start
1. **Immediately** (without waiting for user input) use the directory tree already provided by `context-load` (the `TREE` section in your context) to identify project directories under `~/dev/claude/` (excluding `secrets/`). Present a numbered menu like:
**Before showing any picker, check for auto-selection signals:**
- **From cwd:** if `$PWD` is `~/dev/claude/<name>` or starts with `~/dev/claude/<name>/` (where `<name>` is a known project directory, not `secrets/`), that project is auto-selected.
- **From active-mode.env:** if `$CLAUDE_CONFIG_DIR` is set and `$CLAUDE_CONFIG_DIR/active-mode.env` exists, read it. If `CLAUDE_PROJECT` is non-empty, that project is auto-selected. Also read `CLAUDE_MODE` and `CLAUDE_MODE_TAG` for the announcement below.
- **Cwd takes precedence** if both signals are present and disagree.
If a project was auto-selected, **skip the picker** and open your first response with:
> Auto-selected project **&lt;name&gt;**. [Engagement mode: **&lt;mode-tag&gt;**] — starting load now.
Then proceed directly to step 2. Otherwise:
1. **Immediately** (without waiting for user input) use the project CLAUDE.md paths listed in the `CLAUDE.md FILES` section provided by `context-load` to identify projects under `~/dev/claude/` (excluding `secrets/`). Present a numbered menu like:
> What are we working on today?
>
@@ -12,14 +43,14 @@
> N-1. **No project right now** — just chat
> N. **New project!** — start something new
Do **not** run shell commands to list directories — the tree is already in your context. Include a brief description if the project has a CLAUDE.md or README.md you can glean one from.
Include a brief description from each project's ABOUT.md/CLAUDE.md/README.md. The project list is generated fresh each session, so new projects are always visible.
2. Based on the user's choice:
- **Existing project**: `cd` into the directory, read all `.md` files, and read `~/dev/claude/secrets/` (read-only reference — review every file to refresh context). Then read `BESTPRACTICES.md` (loaded automatically by `context-load`) and load any topic files relevant to the selected project's technology stack. Ask clarifying questions if anything is unclear or incomplete, and note context in MEMORY.md.
- **No project right now**: Do nothing further — just respond normally.
- **New project!**: Follow the "New Projects" section below. Also read `~/dev/claude/secrets/` as above. Read `BESTPRACTICES.md` (loaded automatically by `context-load`) and load topic files relevant to the new project's technology stack.
2. Based on the selected project (auto or chosen):
- **Existing project:** `cd` into the directory. **Pull the latest from the remote** (`git pull --ff-only`) before reading any files — work may have been pushed from another machine or agent. If the pull fails (diverged history, uncommitted changes), warn the user and ask how to proceed. Then read all `.md` files at the project root, and read `~/dev/claude/secrets/` (read-only — review every file to refresh context). Then read `BESTPRACTICES.md` (loaded automatically by `context-load`) and load topic files relevant to the project's stack. Ask clarifying questions if anything is unclear.
- **No project right now:** do nothing further — just respond normally.
- **New project!** Follow [`claude/new-projects.md`](./claude/new-projects.md). Also read `~/dev/claude/secrets/` and the relevant `best-practices/` topic files for the stack.
3. **Set the status line topic** after the user selects a project or describes their task. See the "Status Line" section below.
3. **Set the status line topic** once the user has selected a project or described their task. See [`claude/status-line.md`](./claude/status-line.md).
## Secrets (`~/dev/claude/secrets/`)
@@ -28,283 +59,54 @@
- Files in `~/dev/claude/secrets/` are **read-only**. Never edit them.
- **Never** copy, echo, write, or reproduce secret values into any other file — not MEMORY.md, not CLAUDE.md, not commit messages, not scripts, not tool output, nowhere.
- **Never** include secret values in git commits, diffs, or changelogs of any project.
- **Never** pass secret values as command-line arguments (visible in `ps` output). Use `@file` references, environment variables sourced at runtime, or stdin.
- **Never** pass secret values as command-line arguments (visible in `ps`). Use `@file` references, env vars sourced at runtime, or stdin.
- When a task requires a secret, read it at execution time from the secrets folder and use it ephemerally. Do not cache or persist the value.
- It is acceptable to reference the **existence** of a secret file (e.g., "credentials are in `~/dev/claude/secrets/gitea/ai_enablement`") but never its contents.
- Referencing the **existence** of a secret file is fine (e.g., "credentials are in `~/dev/claude/secrets/gitea/ai_enablement`") — referencing the contents is not.
## New Projects
## Gitea CI visibility
If this is a new project:
1. Create a new directory under `~/dev/claude/<project-name>/`
2. **Ask the user** which Gitea user/org the repo should be created under (e.g., `homelab`, a personal user, etc.) before setting up the remote
3. Create the initial standard files:
- **CLAUDE.md** — Project-specific architecture, conventions, repo structure, and working instructions for Claude
- **MEMORY.md** — Persistent learnings, gotchas, reflections, and process improvements
- **FUTURE.md** — Ideas and improvements not on the active roadmap (Problem/Idea/Open questions/Depends on format)
- **README.md** — Human-readable overview, quick start, milestones, and scripts reference
4. Parse the other CLAUDE.md files from sibling project folders in `~/dev/claude/`, and based on the type of project being considered, bring over related practices, guidelines, and learnings
5. **For coding projects with multiple milestones:** Read `best-practices/spec-driven-development.md` and `best-practices/test-driven-development.md`. Create `SPEC.md` and `spec/` directory. Write specs before writing code. See the "Spec-Driven Development" section below.
## Source Control
- All projects are hosted on **Gitea** (`gitea.oreillyit.nz`) as the primary remote — prefer this hostname over `gitea.homelab.internal` (same instance, but the public name enables external access)
- Migrate existing remotes from `gitea.homelab.internal` to `gitea.oreillyit.nz` when convenient
- AI-focused projects go under the **`skynet`** org; infrastructure projects under **`homelab`**
- SSH workflows preferred. SSH config uses host aliases per Gitea user:
- `gitea.oreillyit.nz-homelab` → authenticates as `cluster-administrator` (key: `~/.ssh/gitea-cluster-admin`)
- `gitea.oreillyit.nz-ai-enablement` → authenticates as `ai_enablement` (key: `~/.ssh/gitea.ai-enablement`)
- Git remote URLs use the alias: `git@gitea.oreillyit.nz-<user>:<org>/<repo>.git`
- Example: `git@gitea.oreillyit.nz-ai-enablement:skynet/custom-claude-skills.git`
- Optionally push-mirror to GitHub for public visibility
- Use meaningful commit messages; prefer small, focused commits over large batches
- Enable pre-commit hooks where appropriate (secret detection, linting, formatting)
- Never commit secrets in plaintext — use SOPS + age or equivalent encryption
## Documentation Standards
Every project maintains up to six standard markdown files. All root-level `.md` files are automatically loaded by `context-load` at session start, so they should be **thin indexes** pointing to detail files in subdirectories — not large documents themselves.
### CLAUDE.md (per-project)
The primary reference for Claude sessions. Should contain:
- Project overview and architecture
- Repository structure (keep updated as the project evolves)
- Key design decisions with rationale
- Conventions and coding standards
- Environment details (IPs, URLs, credentials references)
- Common operations / how-to recipes
### MEMORY.md (Tiered Memory System)
Long-running projects accumulate significant context. To keep MEMORY.md useful rather than bloated, use a **tiered memory** structure:
**MEMORY.md** is a **thin index only** — one-line descriptions with links to topic files in `memory/`. No content lives in MEMORY.md itself. Think of it as a card catalog.
**memory/** contains the actual content, split by topic:
- `memory/project-status.md` — Current milestone, what's next, blockers
- `memory/network.md` — IPs, VIPs, subnets, topology
- `memory/gotchas-<topic>.md` — Gotchas grouped by technology (e.g., `gotchas-cilium.md`, `gotchas-authelia.md`)
- `memory/process-lessons.md` — How-to-work-with-this-repo lessons for Claude
- `memory/m<N>-reflection.md` — One file per milestone reflection (these are time-bound, so per-file is natural)
- `memory/decisions.md` — Architecture and design decisions made during planning
**Principles:**
- **Split by topic, not by time.** A Cilium gotcha belongs in `gotchas-cilium.md` whether discovered in M5 or M8.
- **Milestone reflections are the exception** — inherently time-bound, one file per milestone.
- **Index descriptions matter.** They're used to decide what to read. "Cilium L2/LB gotchas and externalTrafficPolicy quirks" beats "cluster stuff".
- **Prune aggressively.** If a gotcha was fixed (e.g., chart version upgraded past the bug), delete it. Stale memory is worse than no memory.
- **Each memory file should be self-contained and greppable.** Include enough context that the file makes sense on its own.
- **Deduplicate with CLAUDE.md.** Conventions and patterns that are stable should live in CLAUDE.md. Memory files are for learnings, gotchas, and reflections that accumulate over time. If something in memory has graduated to a stable convention, move it to CLAUDE.md and remove it from memory.
**When reading memory at session start:** Read MEMORY.md (the index), then selectively read topic files relevant to the current task. Don't read all memory files unless doing a broad review.
**When writing memory after a milestone:** Create the reflection file, update any affected topic files (new gotchas, updated status), and update the index.
### CONTEXT.md (Active Work Focus)
Tells Claude (and independent agents) what the project is currently working on. Follows the same index pattern as MEMORY.md.
**CONTEXT.md** is a **thin index** — one-line descriptions with links to detail files in `context/`. It answers "what should I focus on right now?"
**context/** contains the detail files, one per active work stream:
- `context/<topic>.md` — Background, status, what to test, future direction for that work stream
**Principles:**
- **Keep it current.** Remove entries when work is complete. CONTEXT.md should reflect what's actively in progress, not a history of past work.
- **Link, don't inline.** The index stays small so `context-load` doesn't bloat the system prompt. Detail files can be as long as needed.
- **Orient agents.** CONTEXT.md is the primary mechanism for pointing independent agents (including those in Docker containers) at the right work. Each agent gets the full context load, with its starting prompt referencing the relevant CONTEXT.md entry.
- **Complement, don't duplicate.** CLAUDE.md has stable conventions. MEMORY.md has accumulated learnings. CONTEXT.md has the *current* focus and active tasks.
### BESTPRACTICES.md (Best Practices Index)
Index of generalised best practices extracted from real project work via the `/distill-best-practices` skill. Only exists in `claude-foundations` — other projects inherit it via `context-load` walking up the directory hierarchy.
**BESTPRACTICES.md** is a **thin index** — one-line descriptions with links to topic files in `best-practices/`.
**best-practices/** contains self-contained topic files (e.g., `kubernetes.md`, `helm.md`, `secrets-management.md`). Read only the files relevant to the current project's technology stack.
The `/distill-best-practices` skill maintains both the topic files and the index.
### FUTURE.md
Backlog of improvement ideas, each with:
- **Problem:** What's painful or manual today
- **Idea:** What the improvement looks like
- **Open questions:** Unknowns to research before starting
- **Depends on:** Other items or milestones that should come first
### README.md
Human-readable project documentation:
- Architecture summary
- Quick start / setup instructions
- Milestone table with status
- Scripts section listing every script with purpose and usage
## Script & Skill Documentation
Every script in `claude-foundations` and every skill in `custom-claude-skills` must have a corresponding memory file in `claude-foundations/memory/`:
- **Scripts:** `memory/script-<name>.md` — Purpose, usage, how it works, gotchas
- **Skills:** `memory/skill-<name>.md` — Purpose, usage, how it works, gotchas, which projects use it
Each memory file should be self-contained and referenced from `claude-foundations/MEMORY.md` (the index). This ensures that future sessions can quickly understand what tooling exists and how it works, without having to read every script and SKILL.md from scratch.
**When creating a new script or skill:** Create the memory file and update the MEMORY.md index as part of the same commit.
**When creating or editing a skill:** Run `validate-skill <path/to/SKILL.md>` before committing. The validator catches known restriction violations that have repeatedly broken skills — `$VAR` in paths, `${VAR}` syntax, `$()` substitution, uncovered binaries in `allowed-tools`, and more. A skill must pass with zero errors before it is committed. Warnings should be reviewed but are acceptable.
## Knowledge Distillation Pipeline
Three skills form a continuous learning pipeline across projects:
1. **`/log`** — Run at end of session. Captures decisions, gotchas, open questions to `memory/log/YYYY-MM-DD.<HHMMSS>.md` in the current project. Also prunes old reflected logs.
2. **`/reflect-logs`** — Run periodically. Processes unprocessed session logs into topic memory files (`memory/gotchas-*.md`, `memory/process-lessons.md`, etc.). Flags stale entries. Tracks state in `.reflection-state.json`.
3. **`/distill-best-practices`** — Run from any project. Reads changed memory files across all tracked projects and proposes updates to the `claude-foundations` repo's `best-practices/` folder. Tracks state in `best-practices/.distill-state.json`.
### State Files
- **`.reflection-state.json`** — Per-project, tracks which logs have been reflected on (md5 hashes of log file content)
- **`best-practices/.distill-state.json`** — In claude-foundations, tracks git SHAs per project at time of last distillation
- **`settings.yaml`** — In claude-foundations, configures log retention (default 7 days), max logs per reflection run, and tracked project list
### Log Format
Session logs use structured markdown with parseable section headers: Summary, Decisions, Gotchas Discovered (tagged with `[topic]` for routing), Open Questions, Key Context, Process Notes. Empty sections are omitted.
### Pruning
- Reflected logs older than `log.retention_days` (default: 7) are automatically deleted by `/log`
- Unreflected logs older than `log.warn_unreflected_days` (default: 14) trigger a warning instead of deletion
- `/reflect-logs` flags stale memory entries (version-specific bugs that have been fixed, manual processes that have been automated)
## Plan Mode
When working in plan mode (`permission_mode: plan`):
**Before calling `ExitPlanMode`**, always write the complete plan to a file in the project root:
- **Filename:** `[MILESTONE]-[PURPOSE]-PLAN.md` — e.g. `M2-auth-PLAN.md`, `M3-monitoring-PLAN.md`
- MILESTONE: the milestone identifier (e.g. `M2`) or a short label if not milestone-scoped (e.g. `initial`)
- PURPOSE: a short kebab-case description of what the plan covers
- **Contents:** the full plan as developed in the planning conversation — steps, decisions, rationale, open questions
- **Location:** project root (same directory as CLAUDE.md)
This file becomes the implementation reference for the session that follows plan mode.
**Keep the plan updated during implementation.** After completing each phase or significant step, update the PLAN.md file:
- Mark the phase/step status as **Complete**
- Add key commits, references, or artifacts produced
- Document deviations from the original plan (what changed and why)
- Note verification results
This ensures the plan stays accurate as a living document — useful for resuming across sessions, reflecting on the milestone, and understanding what actually happened vs. what was planned.
## Spec-Driven Development
**Any coding project with multiple milestones MUST have specs before code.** This is a hard requirement, not a suggestion. The workflow is: Plan → Spec → Test → Code.
**At project start (or when starting a new milestone), always read:**
- `best-practices/spec-driven-development.md` — spec structure, requirement numbering, scenarios, maintenance
- `best-practices/test-driven-development.md` — edge case discovery, property-based testing, AI agent testing patterns
### Required Artifacts
Every multi-milestone coding project must have:
1. **`SPEC.md`** — Index file at the project root. Lists all spec files with a "when to read" column. Same thin-index pattern as MEMORY.md.
2. **`spec/` directory** — One spec file per subsystem. Each spec follows the required structure: Overview, Responsibilities, Dependencies, Data Model, Requirements (numbered), Scenarios (given/when/then).
3. **Numbered requirements** — Each spec uses a prefix (e.g., `IG-1` for ingestion, `DB-1` for database). Requirements must be independently testable and unambiguous.
4. **Test files that reference spec IDs** — Every test function name includes its requirement ID: `test_ig3_dedup_by_message_ts`.
### The Workflow
1. **Plan** — Architecture decisions, milestone breakdown, technology choices (PLAN.md)
2. **Spec** — Detailed contracts, data models, interfaces, requirements, scenarios (spec/)
3. **Test** — Write tests from the spec before code exists. They should all fail.
4. **Code** — Implement until tests pass. Minimum code to satisfy the spec.
5. **Update** — If implementation reveals spec issues, update spec → test → code in that order.
### When to Write Specs
- **Before M1 implementation begins** — write specs for all subsystems in M1's scope
- **Before each subsequent milestone** — write specs for new subsystems, update existing specs for changes
- **Spec changes and test changes ship in the same commit**
- **Code changes that affect interfaces require spec changes in the same commit**
### What Does NOT Need a Spec
- Infrastructure-only projects (Helm values, Kustomize manifests, Ansible playbooks) — these are declarative, not behavioral
- Single-script utilities — a well-commented script with a test is sufficient
- Documentation-only changes
## Milestones
Break projects into numbered milestones (M1, M2, ...). Every milestone completion MUST include:
1. **Verification script** (`scripts/verify-m<N>.sh`) — automated checks confirming all milestone outcomes. Scripts should be idempotent, non-destructive, and return non-zero on failure. Use colour output (green/red) for pass/fail.
2. **Milestone reflection** in `memory/m<N>-reflection.md` — review the entire conversation and capture:
- Process improvements (what slowed us down, wrong assumptions, backtracking)
- Key knowledge for reproduction (gotchas, version quirks, debugging detours)
- Scripts and automation opportunities (repeated command sequences → scripts)
- Future improvement ideas (add to FUTURE.md)
- Update affected topic files in `memory/` (new gotchas, updated status) and the MEMORY.md index
3. **Updated README.md** — scripts section, milestone table, any new setup steps
4. **Updated CLAUDE.md** — repo structure, conventions, new patterns discovered
## Version Management
- Use the latest stable version of dependencies unless pinned for a reason
- Verify versions from live sources (`helm search repo`, upstream docs, package registries) — don't rely on memory
- Document the reason in a comment if a version is intentionally pinned below latest
- Check compatibility matrices before upgrading (e.g., Talos ↔ Kubernetes, framework ↔ runtime)
## Secrets Management
- SOPS + age is the standard encryption tool across all projects
- The `.sops.yaml` at the repo root defines path-based encryption rules
- Filenames containing `secret` trigger SOPS encryption via pre-commit hooks
- Non-secret files must NOT contain `secret` in their name
- Keep unencrypted secrets in `local_secrets/` (gitignored)
## Scripting Conventions
- All scripts live in `scripts/` and run from the repository root
- Scripts should be idempotent and safe to re-run
- Use colour output for pass/fail indicators in verification scripts
- Verification scripts should check for default/insecure credentials and print remediation instructions on failure
- Scripts should exit non-zero on failure so `&&` chains work naturally
- **Never hardcode secrets, tokens, or access keys in scripts.** Accept them via environment variables, stdin, or `@file` references. If a script needs a secret at runtime, read it from `~/dev/claude/secrets/` or accept it as a parameter — never embed it.
When working with Gitea (watching Actions runs, checking whether a release/container built, diagnosing failing CI), use `gitea-agent-companion` — alias `gac`, installed in `~/sbin`, source at `projects/gitea-agent-companion`. Run `gitea-agent-companion --help` first: the help text is the full reference (verbs, flags, token discovery, exit codes). Canonical post-push pattern: `gac tail <owner/repo> --until run-complete --timeout 600`; on failure, `gac inspect <owner/repo> --run <N> --logs`.
## Process Principles
These are hard-won lessons from real project work:
Hard-won lessons that apply across every project:
- **Read the gotcha before you diagnose.** When an issue surfaces (HTTP 4xx/5xx, hung connection, perpetual `OutOfSync`, "permission denied", silent drop, etc.), the FIRST step is to grep `~/dev/claude/projects/best-practices/<topic>.md` and the project's `memory/gotchas-*.md` for keywords matching the symptom AND the technologies involved. Search both the *symptom* (e.g., `permission denied`, `30s timeout`, `not matching`) and the *infrastructure* (e.g., `cilium`, `ipBlock`, `ESO`, `apiserver`). Do this BEFORE forming your first hypothesis. Turns a multi-hour investigation into a 5-minute lookup. If the gotcha isn't there, capture the new one when you solve it.
- **Validate locally, deploy once.** Don't use the live environment as a test bed. Catch errors with local validation tools before pushing (`helm template`, `kustomize build`, lint/typecheck, `docker inspect`, etc.). Batch fixes locally, push once.
- **Check before you act.** Before writing firewall/network rules, check actual routing (`ip route get`). Before running config management with variables, ensure values are real, not placeholders. Before assuming a container has a shell, `docker inspect` it.
- **Check before you act.** Before writing firewall/network rules, check actual routing (`ip route get`). Before running config management with variables, ensure values are real, not placeholders. Before assuming a container has a shell, `docker inspect` it. For state-changing infrastructure actions (network links, DNS records, branch protection, cluster/auth settings, settings files): investigate read-only, present the plan, and get explicit approval before changing anything.
- **Test the full chain immediately.** After wiring up a new service or endpoint, test end-to-end from the user's perspective right away. Don't assume intermediate steps working means the whole chain works.
- **Verify scripts should be environment-resilient.** Avoid needing sudo or special access. Test from the accessible side of a connection. Use `curl --resolve` to bypass DNS/proxy layers when testing direct connectivity.
- **Automate repeated sequences.** If you run the same 3+ commands in sequence more than once, it should become a script.
- **Reflect after milestones.** Don't just finish — review what happened, what went wrong, what can be improved. Write it down so future sessions benefit.
- **Reflect after milestones.** Don't just finish — review what happened, what went wrong, what can be improved. Write it down.
- **Pre-flight before push.** Before any `git push`: derive the SSH alias from `git remote get-url origin`, then run `ssh-add -l` and inspect the full output — keys are often pre-loaded, so always check before asking the user to load one. Use `grep -i` (case-insensitive) when scanning for the relevant key name. Only ask if the key is genuinely absent. Passphrase-protected keys must be `ssh-add`ed by the user — hand off, don't retry. If a push fails: commit locally, say so explicitly, note the recovery step in CONTEXT.md. Details: `claude/source-control.md`.
- **Everyday shell/edit footguns.** `grep` exits 1 on no match (breaks `set -e` chains); Edit `replace_all` matches substrings — make the old string unique. Full list: `claude/scripting-conventions.md`.
## Status Line
## Session End
A persistent bar at the bottom of Claude Code shows the current topic, model, and context usage: `[Model Name] topic | N% context`.
Run `/end-session` (session log + CONTEXT.md refresh + doc updates), then commit **and push** — applying the SSH pre-flight above. A session is not finished while completed work sits unpushed. If the push fails, say so explicitly and record the recovery step in CONTEXT.md before stopping.
### Setting the Topic
## Version & Secrets Management (compact rules)
After the user selects a project or describes their task (i.e., after the first response where the status line has had a chance to run), set the topic:
- Use the latest stable version of dependencies unless pinned for a reason. Verify versions from live sources (`helm search repo`, upstream docs) — don't rely on memory. Document the reason in a comment if pinned below latest. Check compatibility matrices before upgrading.
- SOPS + age is the standard encryption tool across all projects. `.sops.yaml` at the repo root defines path-based encryption rules. Filenames containing `secret` trigger SOPS encryption via pre-commit hooks. Non-secret files must NOT contain `secret` in their name. Keep unencrypted secrets in `local_secrets/` (gitignored).
```bash
~/.claude/status/set-topic.sh "$(pwd)" "project-name: brief task description"
```
## Detail files — when to read
**Examples:**
- `~/.claude/status/set-topic.sh "$(pwd)" "brainiac-app: M2 web frontend"`
- `~/.claude/status/set-topic.sh "$(pwd)" "cluster-bootstrap: Cilium upgrade"`
- `~/.claude/status/set-topic.sh "$(pwd)" "General chat"`
### When to Update
- **Session start**: Set the topic once the user picks a project or task
- **Focus change**: Update if the user shifts to a different project or task mid-session
- **Keep it short**: Aim for `project: task` format, under ~40 characters
### How It Works
The status line script (`scripts/statusline.sh`) runs after each assistant message. It writes the session ID to `/tmp/claude-session-id-<md5 of cwd>`, which `set-topic.sh` reads to find the correct per-session topic file at `~/.claude/status/<session-id>/claude-topic.txt`.
**Early calls are safe:** If `set-topic.sh` is called before the status line has run (i.e., on the first message), the topic is queued to a pending file and automatically applied when the status line first runs after the next response.
Pull one of these in only when the situation calls for it. Listed in roughly descending frequency.
| File | Read when |
|---|---|
| [`claude/source-control.md`](./claude/source-control.md) | Touching git, creating repos, setting up remotes |
| [`claude/agent-runtimes-cp.md`](./claude/agent-runtimes-cp.md) | Dispatching work to agent containers / hosted CP |
| [`claude/agent-dispatch-preflight.md`](./claude/agent-dispatch-preflight.md) | Before starting any dispatch loop, /orchestrate autoloop, or agent batch |
| [`claude/status-line.md`](./claude/status-line.md) | Setting or updating the status line topic |
| [`claude/plan-mode.md`](./claude/plan-mode.md) | In plan mode — before calling `ExitPlanMode` |
| [`claude/spec-driven-development.md`](./claude/spec-driven-development.md) | Starting a milestone / writing or editing specs |
| [`claude/milestones.md`](./claude/milestones.md) | Completing a milestone (verification, reflection) |
| [`claude/documentation-standards.md`](./claude/documentation-standards.md) | Creating or restructuring project doc files (ABOUT/CLAUDE/MEMORY/CONTEXT/FUTURE/README) |
| [`claude/new-projects.md`](./claude/new-projects.md) | Creating a new project from scratch |
| [`claude/scripting-conventions.md`](./claude/scripting-conventions.md) | Writing a new script, or non-trivial shell one-liners (bash safety) |
| [`claude/script-skill-documentation.md`](./claude/script-skill-documentation.md) | Authoring or editing a script in claude-foundations or a skill in custom-claude-skills |
| [`claude/knowledge-distillation.md`](./claude/knowledge-distillation.md) | Running `/log`, `/reflect-logs`, or `/distill-best-practices` |
| [`claude/ask-minimax-usage.md`](./claude/ask-minimax-usage.md) | Considering delegating a file-heavy task to MiniMax |
| [`claude/impeccable.md`](./claude/impeccable.md) | Skill maintenance/install for Impeccable (design skill): updating the version, re-applying the no-auto-invoke flag, hint-shim sync, detector usage |

View File

@@ -5,3 +5,4 @@ Current work focus and active tasks. Used by `context-load` to orient agents at
## Active
- [Context loading improvements](context/context-loading.md) — Testing and refining the `context-load` and `start-claude` scripts
- [Transcript tracking — first-run verification](context/transcript-tracking.md) — System built and committed; verify Sonnet subagent writes to correct absolute path on first `/log` run with real backups

48
FUTURE.md Normal file
View File

@@ -0,0 +1,48 @@
# Future Ideas — claude-foundations
Backlog of improvement ideas not on the active roadmap.
---
## Standing Deployment Authorization in CLAUDE.md
- **Problem:** Claude has all the tooling needed to run deployment steps (sops, ansible-playbook, kubectl, docker, git push) but must ask for confirmation every time because these are irreversible, shared-infrastructure operations. This makes multi-step milestone deployments slow and interactive.
- **Idea:** Add explicit standing authorization in CLAUDE.md for deployment operations — scoped to verified milestone changes (e.g., "Claude may run Ansible playbooks against VPS and bootstrap VM when deploying verified milestone changes"). Define the scope clearly so it doesn't become a blank cheque.
- **Open questions:** Should authorization be per-project or global? Should it require a passing verification script first? Should it be limited to specific Ansible tags or playbooks?
- **Depends on:** Pre-deployment checklist script (see below)
---
## Pre-Deployment Checklist Script
- **Problem:** Before deploying infrastructure changes, there's no automated way to validate that the repo is in a clean, deployable state. Claude needs a safety gate to run before taking irreversible deployment actions.
- **Idea:** A `scripts/pre-deploy-check.sh` in cluster-bootstrap that validates: clean git status, all secrets SOPS-encrypted (no plaintext in tracked files), kustomize builds cleanly for all overlays, Ansible syntax checks pass, no placeholder values in inventory. Returns non-zero on any failure.
- **Open questions:** Should it also check that the target services are healthy before deploying (kubectl health checks)? Should it produce a deployment plan summary (like terraform plan)?
- **Depends on:** Nothing — can be built anytime
---
## Milestone Deployment Orchestration Script
- **Problem:** Deploying a milestone involves 6-8 manual steps in a specific order (SOPS encrypt, git push, Ansible playbooks, kubectl operations, container builds). Each step is straightforward but the sequencing is error-prone and tedious.
- **Idea:** A `scripts/deploy-milestone.sh` in cluster-bootstrap that sequences all deployment steps: pre-deploy checks → SOPS encrypt → git commit + push → Ansible playbooks (with --diff) → wait for ArgoCD sync → S3/kubectl operations → container builds → post-deploy verification. Support `--dry-run` mode that prints what it would do without executing.
- **Open questions:** Should it be a single script or a Makefile with targets? Should it poll ArgoCD sync status or just wait a fixed time? How to handle partial failures (resume from last successful step)?
- **Depends on:** Pre-deployment checklist script, standing deployment authorization
---
## Rollback Documentation Per Deployment Target
- **Problem:** When Claude takes deployment actions, it needs to understand how to revert each one if something goes wrong. Currently rollback procedures are tribal knowledge.
- **Idea:** Document rollback procedures for each deployment target in cluster-bootstrap (e.g., "VPS: re-run Ansible with previous commit", "ArgoCD: kubectl rollout undo or git revert + push", "S3: delete bucket via garage CLI"). Could live in `docs/rollback.md` or per-service README files.
- **Open questions:** Should rollback be automated (rollback script) or just documented? Should the deploy script automatically create git tags as rollback points?
- **Depends on:** Nothing — can be written anytime
---
## CacheAligner pattern for API-metered agent pipelines
- **Problem:** When dispatching many sub-agent API calls (e.g. agent-runtimes), dynamic content in system prompts (dates, session IDs, UUIDs) defeats Anthropic's prompt cache. Cached prefixes get a 90% token cost discount, but any prefix change invalidates the cache.
- **Idea:** Extract volatile values (timestamps, session tokens, request IDs) from system prompts and relocate them to message endings. Keep the system prompt prefix stable across calls so the provider KV cache hits consistently. This is a design pattern, not a dependency — inspired by Headroom's CacheAligner (chopratejas/headroom).
- **Open questions:** How much of our agent prompt content is actually stable vs dynamic? What's the realistic cache hit rate improvement? Does Anthropic's cache window (5 min TTL) align with our dispatch cadence?
- **Depends on:** agent-runtimes reaching API-metered multi-agent workloads (M5+)

View File

@@ -13,9 +13,12 @@
- [git-status-report](memory/script-git-status-report.md) — Scans directories for git repos, reports uncommitted changes and remote sync (lives in small-scripts)
- [statusline](memory/script-statusline.md) — Status bar scripts: statusline.sh (renderer) and set-topic.sh (topic setter) for per-session topic display
- [require-plan-file](memory/script-require-plan-file.md) — PreToolUse hook: blocks ExitPlanMode unless a *-PLAN.md file exists in the project root
- [extract-transcripts](memory/script-extract-transcripts.md) — Manages transcript backup tracking: register, list, extract, mark-processed. Used by /log and pre-compact hook
- [list-transcripts-here](memory/script-list-transcripts-here.md) — Wrapper calling extract-transcripts.py --list $(pwd); exists to avoid $() in SKILL.md bang commands
## Skills
- [/end-session](memory/skill-end-session.md) — End-of-session wrap-up: session log + CONTEXT.md update for seamless resumption
- [/log](memory/skill-log.md) — End-of-session logging to memory/log/ for later reflection
- [/reflect-logs](memory/skill-reflect-logs.md) — Processes session logs into topic memory files (gotchas, decisions, process lessons)
- [/reflect](memory/skill-reflect.md) — Milestone reflection: reviews conversation + git log, produces structured artifacts
@@ -25,6 +28,9 @@
- [/housekeeping](memory/skill-housekeeping.md) — Cross-project health check: git status, unreflected logs, skill validation, pipeline recommendations
- [/decompose](memory/skill-decompose.md) — Break tasks into subtasks with dependency graph, write .agent-tasks.json for container agent orchestration
- [/orchestrate](memory/skill-orchestrate.md) — Check task state, launch container agents in git worktrees. Use `/loop 2m /orchestrate` for auto-polling
- [/review-plan](memory/skill-review-plan.md) — Review plan against best practices (API, security, specs, TDD) before implementation
- [/review-spec](memory/skill-review-spec.md) — Review spec for structure, requirement quality, security coverage, and testability
- [/spawn-session](memory/skill-spawn-session.md) — Spawn a new detached tmux Claude Code session with Remote Control, named after a project (wraps claude-tmux)
## References
@@ -35,4 +41,7 @@
- [Decisions](memory/decisions.md) — Architecture and design decisions: pipeline design, linting system, context-load, CLAUDE.md structure
- [Process Lessons](memory/process-lessons.md) — Working rules: git-status at session start, plan mode for architecture, hook exit codes, ssh-agent
- [Gitea Gotchas](memory/gotchas-gitea.md) — Push permission denied when repo created by different API user than SSH alias
- [Skills Gotchas](memory/gotchas-skills.md) — Broken symlinks under set -e, mid-session skill discovery limitation
- [Skills Gotchas](memory/gotchas-skills.md) — Broken symlinks under set -e, mid-session skill discovery limitation, `$()` and `$VAR` bang-command rejection
- [Statusline Gotchas](memory/gotchas-statusline.md) — set-topic.sh bootstrap race condition, md5sum -n flag consistency
- [Hooks Gotchas](memory/gotchas-hooks.md) — PreCompact `transcript_path` removed from input; derive from session_id+cwd
- [Python Gotchas](memory/gotchas-python.md) — `sys.exit()` swallowed by bare `except:` inside loops

View File

@@ -1,6 +1,6 @@
# claude-foundations
Shared infrastructure for Claude Code sessions: hooks, formatters, best practices, and the knowledge distillation pipeline.
Shared infrastructure for Claude Code sessions: hooks, formatters, and the knowledge distillation pipeline.
## Repository Structure
@@ -21,15 +21,16 @@ claude-foundations/
pre-compact-backup.sh # PreCompact: backup transcript before compaction
require-plan-file.sh # PreToolUse/ExitPlanMode: enforce *-PLAN.md exists before leaving plan mode
scripts/
install-hooks.sh # Symlink hooks into ~/.claude/hooks/
setup-formatters.sh # Set up formatters for a project
statusline.sh # Status line renderer (symlinked from ~/.claude/status/)
set-topic.sh # Set per-session topic for the status line
best-practices/ # Generalised best practices (one file per topic) — also maintained in skynet/best-practices
install-hooks.sh # Symlink hooks into ~/.claude/hooks/ and skill-helper scripts into ~/.claude/scripts/
setup-formatters.sh # Set up formatters for a project
statusline.sh # Status line renderer (symlinked from ~/.claude/status/)
set-topic.sh # Set per-session topic for the status line
extract-transcripts.py # Transcript backup tracking: register, list, extract, mark-processed
list-transcripts-here.sh # Wrapper: extract-transcripts --list $(pwd) (skill bang-command safe)
context/ # Active work focus detail files
memory/ # Session logs and reflections
settings.yaml # Knowledge pipeline configuration
BESTPRACTICES.md # Best practices index (loaded by context-load)
BESTPRACTICES.md # Redirect to skynet/best-practices repo
CLAUDE.md # Global project guidelines (symlinked to ~/dev/claude/)
CONTEXT.md # Active work focus index (loaded by context-load)
HOOKS.md # Hook documentation
@@ -45,7 +46,7 @@ cd ~/dev/claude/projects/claude-foundations
scripts/install-hooks.sh
```
This symlinks all hooks into `~/.claude/hooks/` and prints the `settings.json` config to add. The PostToolUse hook is what triggers auto-formatting on every Edit/Write.
This symlinks all hooks into `~/.claude/hooks/`, installs skill-helper scripts into `~/.claude/scripts/`, and prints the `settings.json` config to add. The PostToolUse hook is what triggers auto-formatting on every Edit/Write.
### 2. Opt a project into auto-formatting
@@ -141,7 +142,9 @@ rm <file>.pre-lint
| Script | Purpose |
|--------|---------|
| `scripts/install-hooks.sh` | Symlink all hooks to `~/.claude/hooks/` and print settings.json config |
| `scripts/install-hooks.sh` | Symlink hooks to `~/.claude/hooks/` and skill-helper scripts to `~/.claude/scripts/`; print settings.json config |
| `scripts/setup-formatters.sh` | Create formatter symlinks in a target project |
| `scripts/statusline.sh` | Status line renderer — shows topic, model, context %. Symlinked from `~/.claude/status/` |
| `scripts/set-topic.sh` | Set the session topic: `set-topic.sh <cwd> "topic text"` |
| `scripts/extract-transcripts.py` | Transcript backup tracking — register backups, list unprocessed, extract conversation text, mark processed |
| `scripts/list-transcripts-here.sh` | Wrapper calling `extract-transcripts.py --list $(pwd)` — used in `/log` skill pre-gathered context |

View File

@@ -1,41 +0,0 @@
{
"version": 1,
"last_run": "2026-03-25T12:00:00Z",
"projects": {
"agent-runtimes": {
"path": "/home/paul/dev/claude/projects/agent-runtimes",
"last_sha": "d40ea0b39140077a482ce9ae304fad1cf51c832e",
"last_run": "2026-03-25T12:00:00Z"
},
"claude-foundations": {
"path": "/home/paul/dev/claude/projects/claude-foundations",
"last_sha": "6c0f2db169b588afa9c92587eef3b64311acc025",
"last_run": "2026-03-25T12:00:00Z"
},
"cluster-apps/octopus-deploy": {
"path": "/home/paul/dev/claude/projects/cluster-apps/octopus-deploy",
"last_sha": "d6aee319f33853ad58ea304d9ea1deedff8444d0",
"last_run": "2026-03-25T12:00:00Z"
},
"cluster-bootstrap": {
"path": "/home/paul/dev/claude/projects/cluster-bootstrap",
"last_sha": "edc48d7a3113c049786cf6de74b2f33f101f39ef",
"last_run": "2026-03-25T12:00:00Z"
},
"custom-claude-skills": {
"path": "/home/paul/dev/claude/projects/custom-claude-skills",
"last_sha": "e8e572e0afdcf3895f4a500ec73ec6d95d1cc19c",
"last_run": "2026-03-25T12:00:00Z"
},
"hugo-accelerator": {
"path": "/home/paul/dev/claude/projects/hugo-accelerator",
"last_sha": "bf0b1a8bc7ed7964a724a2b8ee6c15b6dc1c926c",
"last_run": "2026-03-25T12:00:00Z"
},
"small-scripts": {
"path": "/home/paul/dev/claude/small-scripts",
"last_sha": "161c633b42c5535b1c649487a330b23a9fa11626",
"last_run": "2026-03-25T12:00:00Z"
}
}
}

View File

@@ -1,36 +0,0 @@
# Ansible
## Inventory and Execution
- Always pass `-i inventory.yml` explicitly or run from the directory containing `ansible.cfg`
- Playbooks that can't find inventory skip silently with no error — a common source of "it ran but nothing happened" confusion
- Variables that need customisation go in `inventory.yml` files, not scattered across role defaults
## Role Structure
- Roles follow standard structure: `tasks/main.yml`, `templates/*.j2`, `handlers/main.yml`
- Jinja2 templates have `.j2` extension and include a "managed by Ansible" header comment
## Template Safety
- **Never use placeholder values with `-e` for vars that template config files.** Using `-e "var=dummy"` will overwrite live configs with garbage. Either read real values, use `--skip-tags` to skip templating tasks, or restructure roles so sensitive templates are in a separate tag.
## Credential Safety
- Pass secrets via `@file` not `-e` on the command line — `-e "key=value"` exposes secrets in `ps` output
- Use temp files with `trap rm` cleanup: `-e "@${tmpfile}"`
## Module Gotchas
- `docker_compose_v2` doesn't support `state: restarted` — use `recreate: always` instead
- `ansible.builtin.unarchive` with `remote_src` and `extra_opts: --strip-components` is unreliable — use `get_url` + `command: tar` separately
- `get_url` won't re-download when the URL changes but the destination filename stays the same — use a version marker file to detect changes
## Service Restarts
- Some services (dnsmasq, etc.) need container restarts for config changes to take effect
- Ansible handlers handle this, but always verify the change took effect (e.g., `dig @<ip> <record> +short`)
## Docker Compose
- `network_mode: host` ignores `ports:` mappings — remove `ports:` to avoid warnings

View File

@@ -1,80 +0,0 @@
# Debugging Methodology
## Check Before You Act
- Before writing firewall/network rules, check actual routing (`ip route get <dest>`)
- Before running config management with variables, ensure values are real, not placeholders
- Before assuming a container has a shell, `docker inspect` it
- Before creating API tokens, research all required scopes upfront — iterating one scope at a time costs a push-debug cycle each
## Routing and Networking
- Always run `ip route get <dest>` on the forwarding host first
- macvlan, Docker bridge, and other virtual interfaces mean the "obvious" physical interface is often wrong
- Test from both in-cluster and external perspectives
## Full-Chain Testing
After wiring up any new service:
1. Test direct to backend (bypass all proxies)
2. Test through reverse proxy (bypass DNS)
3. Test end-to-end as a user would
Use `curl --resolve` to test specific paths without depending on DNS propagation.
## When Something Doesn't Sync/Apply
- Check resource exclusions in the GitOps controller immediately
- Check if the resource type requires special permissions or labels
- Check if ServerSideApply conflicts are preventing field changes
- Don't try workarounds before understanding the root cause
## OIDC Integration Checklist
Before starting any OIDC integration, research:
1. What format is the `sub` claim (UUID? username?)
2. Which claims are in the ID token vs userinfo endpoint
3. How the consumer matches RBAC identities (groups? email? username?)
## Log-First Diagnosis
- **CrashLoopBackOff: check logs first.** Error messages in pod logs usually point directly to the fix. Don't tweak configuration or security contexts blindly — `kubectl logs <pod>` first.
- **Discriminate transient from persistent errors.** CSI lock contention, etcd timeouts during first install, and brief connectivity blips are self-healing. Don't spend time debugging errors that resolve on retry. If you see retry/backoff patterns in logs, wait before intervening.
- **Trust controller retry logic.** CSI controllers, operators, and reconciliation loops have built-in retry. Transient failures during rapid provisioning are expected, not bugs.
## Reproduce Before Fixing
When a bug is discovered or reported, **do not start by trying to fix it.** The first step is always to write a test that reproduces the failure:
1. **Write a failing test.** Capture the bug as a test case that demonstrates the broken behaviour. This forces you to understand the bug precisely — what input triggers it, what the wrong output is, and what the correct output should be.
2. **Fix the bug in isolation.** Use a subagent or a separate session to write the fix. The fixing agent gets the failing test as its success criterion — it's done when the test passes. This separation prevents the fixer from unconsciously weakening the test to match a broken implementation.
3. **The test stays forever.** The reproduction test becomes a permanent regression test. It proves the fix works and prevents the bug from returning.
This workflow has several advantages:
- **Forces precise understanding.** Writing a test means you know exactly what's broken, not just "it doesn't work."
- **Prevents partial fixes.** The test defines "done" objectively — the fix either passes or it doesn't.
- **Parallelises work.** While one agent fixes the bug, you can continue other work.
- **Catches regressions.** The test remains in the suite, guarding against the same class of failure.
```python
# Step 1: Write the failing test FIRST
def test_regression_issue_427_empty_payload_crashes():
"""Bug #427: Empty payload causes unhandled TypeError in dispatcher.
Should return a 400 validation error, not crash."""
response = client.post("/dispatch", json={})
assert response.status_code == 400 # Currently crashes with 500
# Step 2: Hand to a subagent/session: "Make this test pass without breaking others"
```
## `GIT_SSH_COMMAND` Only Affects Git-Invoked SSH
`GIT_SSH_COMMAND` (e.g., `ssh -o StrictHostKeyChecking=no`) only applies when `git` invokes SSH internally (clone, push, fetch). Direct `ssh` calls — such as `ssh -T git@host` for connectivity testing — ignore it entirely. When working in containers or CI environments where host keys aren't pre-trusted, direct SSH commands need explicit flags: `ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`.
## Pattern Mining Before Authoring
Before building a new service, component, or script, read existing patterns in the codebase first. This matches conventions on the first attempt and avoids rework on naming, structure, and integration points. Applies to K8s manifests, CI pipelines, skill authoring, and script structure.
## Grep Your Own Docs
Known issues documented in CLAUDE.md or MEMORY.md but not applied to new scripts/configs waste debugging time. Search your own documentation before writing automation that touches areas with known gotchas.

View File

@@ -1,113 +0,0 @@
# Docker UID Matching for Mounted Volumes
When a host directory is mounted into a Docker container, files created by the container process are owned by the container user's UID/GID. If this doesn't match the host user, you get one of two problems:
1. **Permission denied** — the container can't write to the mounted directory
2. **Wrong ownership** — files created inside the container are owned by a different user on the host (e.g., `root` or UID `1001`)
Both are common sources of friction in developer-facing container tools and CI/CD pipelines.
## The Pattern: UID Wrapper Entrypoint
The solution is a small entrypoint wrapper script that:
1. Starts as root
2. Detects the UID/GID of the mounted directory via `stat`
3. Adjusts the container user's UID/GID to match using `usermod`/`groupmod`
4. Drops privileges via `gosu` and execs the real command
This is transparent to the user — they don't need to pass `--user` flags or know their UID.
### Implementation
**Dockerfile:**
```dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -s /bin/bash agent
RUN mkdir -p /project && chown agent:agent /project
COPY uid-wrapper.sh /usr/local/bin/uid-wrapper.sh
# Start as root — the wrapper drops privileges after UID adjustment
ENTRYPOINT ["uid-wrapper.sh", "your-actual-command"]
```
**uid-wrapper.sh:**
```bash
#!/bin/bash
set -e
TARGET_DIR="/project" # The mount point to match
AGENT_USER="agent" # The non-root user to adjust
# If not root, skip adjustment (e.g., K8s securityContext sets UID)
if [ "$(id -u)" != "0" ]; then
exec "$@"
fi
if [ -d "$TARGET_DIR" ]; then
HOST_UID=$(stat -c '%u' "$TARGET_DIR")
HOST_GID=$(stat -c '%g' "$TARGET_DIR")
if [ "$HOST_UID" != "0" ]; then
# Adjust GID if different
if [ "$HOST_GID" != "$(id -g $AGENT_USER)" ]; then
groupmod -g "$HOST_GID" "$AGENT_USER" 2>/dev/null || true
fi
# Adjust UID if different
if [ "$HOST_UID" != "$(id -u $AGENT_USER)" ]; then
usermod -u "$HOST_UID" "$AGENT_USER" 2>/dev/null || true
fi
# Fix home directory ownership
chown -R "$AGENT_USER:$(id -g $AGENT_USER)" /home/"$AGENT_USER" 2>/dev/null || true
fi
fi
exec gosu "$AGENT_USER" "$@"
```
### Key Details
- **`gosu` over `su`/`sudo`.** `gosu` execs directly (PID 1 becomes the real process), while `su` creates a child process that breaks signal handling. `gosu` is the standard tool for this pattern.
- **Handle existing UIDs/GIDs.** The host UID may already be taken by another user in the container. Ubuntu 24.04 images ship with a `ubuntu` user at UID 1000 — the most common host UID. The wrapper must evict the conflicting user to a high unused UID (e.g., 59999 and search downward) before assigning the target UID to the agent user. `usermod -u <new> <conflicting_user>` followed by `usermod -u <target> agent`. Same applies to GIDs — use `getent group` to check before `groupmod`. **Simpler alternative:** Delete the conflicting user at build time (`RUN userdel ubuntu` in the Dockerfile). This avoids runtime conflict handling entirely and is preferred when you control the image.
- **SSH agent socket forwarding requires UID match.** When mounting `$SSH_AUTH_SOCK` into a container, the socket is mode 0600 owned by the host UID. The container process must run as the same UID to use it — which the UID wrapper handles naturally. Set `SSH_AUTH_SOCK` in the container env to the mounted path. Note: private keys cannot be extracted via the agent protocol — it only supports signing operations.
- **Skip when not root.** In Kubernetes, `securityContext.runAsUser` sets the UID before the container starts. The wrapper detects this (`id -u != 0`) and skips adjustment — the platform is handling it.
- **Mount point owned by root.** If the mounted directory is owned by root (UID 0), don't adjust — running as root defeats the purpose. The wrapper only adjusts for non-root UIDs.
- **Fix home directory.** After `usermod`, the user's home directory still has the old UID. `chown -R` fixes this. Skip other directories — only fix what the user needs.
## When to Use
- **Developer-facing container tools** where users mount local project directories (e.g., running a CLI tool inside a container)
- **CI/CD containers** that write build artifacts back to a mounted workspace
- **Any container that writes to a host-mounted volume** and the output needs correct ownership
## When NOT to Use
- **Kubernetes with securityContext** — the platform handles UID assignment; the wrapper correctly skips adjustment in this case
- **Containers that don't write to mounted volumes** — unnecessary overhead
- **Images that must run as root** — the wrapper's purpose is to run as non-root; if you need root, you don't need the wrapper
## Alternatives
| Approach | Pros | Cons |
|----------|------|------|
| `docker run --user $(id -u):$(id -g)` | Simple, no image changes | No home directory, no `/etc/passwd` entry, breaks tools that need a user identity |
| `fixuid` | Purpose-built tool | Another binary to install and maintain |
| UID wrapper (this pattern) | Transparent, handles edge cases, works in Docker and K8s | Requires `gosu` in image, starts as root |
| Named volumes only | Docker manages ownership | Can't mount host directories |
## Source
Learned from the agent-runtimes project (M1) where Claude Code refuses `--dangerously-skip-permissions` as root for security reasons. The container needed to run as non-root, but files created in mounted project directories needed to be owned by the host user. The UID wrapper solved both problems transparently.

View File

@@ -1,54 +0,0 @@
# Documentation Standards
Every project maintains four core markdown files.
## CLAUDE.md
The primary reference for Claude sessions. Should contain:
- Project overview and architecture
- Repository structure (keep updated as the project evolves)
- Key design decisions with rationale
- Conventions and coding standards
- Environment details (IPs, URLs, credential references — never actual values)
- Common operations / how-to recipes
- Put critical rules at the top — Claude reads sequentially and earlier content has more influence
## MEMORY.md (Tiered Memory System)
Long-running projects accumulate significant context. Use a **tiered memory** structure:
**MEMORY.md** is a **thin index only** — one-line descriptions with links to topic files in `memory/`. No content lives in MEMORY.md itself. Keep it under ~50 lines.
**memory/** contains the actual content, split by topic:
- `memory/project-status.md` — Current milestone, what's next, blockers
- `memory/gotchas-<topic>.md` — Gotchas grouped by technology
- `memory/process-lessons.md` — How-to-work-with-this-repo lessons
- `memory/m<N>-reflection.md` — One file per milestone reflection
- `memory/decisions.md` — Architecture and design decisions
**Principles:**
- Split by topic, not by time
- Index descriptions matter — they're used to decide what to read
- Prune aggressively — stale memory is worse than no memory
- Each file should be self-contained and greppable
- Deduplicate with CLAUDE.md — stable conventions go in CLAUDE.md, learnings and gotchas go in memory
## FUTURE.md
Backlog of improvement ideas, each with:
- **Problem:** What's painful or manual today
- **Idea:** What the improvement looks like
- **Open questions:** Unknowns to research before starting
- **Depends on:** Other items or milestones that should come first
## README.md
Human-readable project documentation:
- Architecture summary
- Quick start / setup instructions
- Milestone table with status
- Scripts section listing every script with purpose and usage
## Extract Reusable Patterns Early
When a reusable pattern emerges during project work (a gotcha that applies to any project using the same tool, a process lesson that generalises), extract it into a best-practices guide immediately rather than waiting for a dedicated distillation pass. The guide pays for itself when used to audit and improve the current project in the same session, and benefits all future projects.

View File

@@ -1,39 +0,0 @@
# Git & Source Control
## Commit Practices
- Use meaningful commit messages; prefer small, focused commits over large batches
- Never commit secrets in plaintext — use SOPS + age or equivalent encryption
- Enable pre-commit hooks where appropriate (secret detection, linting, formatting)
## Pre-Commit Hooks
- Block `local_secrets/` and similar directories from being committed
- Auto-encrypt files matching `.sops.yaml` rules that aren't yet encrypted
- Enable with `git config core.hooksPath .githooks`
- Consider secret detection, linting, and formatting hooks
## GitOps Workflow
- All infrastructure changes should be tracked in Git
- No manual changes without corresponding GitOps manifests — anything applied manually (e.g., `kubectl apply`, `helm install`) should immediately get a corresponding tracked manifest
- For ArgoCD-managed clusters: edit in Git, push, sync — never edit live resources directly
## Remote Conventions
- SSH workflows preferred over HTTPS for Git remotes
- Use SSH config host aliases for multi-user setups (e.g., `gitea.example.com-<user>`)
- Remote URL format: `git@<host-alias>:<org>/<repo>.git`
- Optionally push-mirror to GitHub for public visibility
## Access and Clone Gotchas
- **Org repos require explicit collaborator grants.** Don't assume organizational membership implies write access — verify permissions before setting up automation or CI/CD.
- **Shallow clones break push operations.** `git clone --depth 1` is fine for read-only CI jobs, but pipelines that push artifacts, tags, or mirror to other remotes need full clones.
## Version Management
- Use the latest stable version of dependencies unless pinned for a reason
- Verify versions from live sources (`helm search repo`, upstream docs, package registries) — don't rely on memory
- Document the reason in a comment if a version is intentionally pinned below latest
- Check compatibility matrices before upgrading (e.g., Talos ↔ Kubernetes, framework ↔ runtime)

View File

@@ -1,25 +0,0 @@
# Helm Charts
## Schema Validation
- **Always validate values against the chart schema before committing.** Run `helm show values <repo>/<chart> --version <ver>` to check the actual structure.
- Helm chart schemas change between versions — field names and nesting can differ from documentation or online examples.
- A quick `helm template` test locally catches schema errors before deployment.
- `additionalProperties: false` in chart schemas means any extra keys at the wrong nesting level cause a hard failure.
## Version Verification
- Run `helm search repo` or check upstream docs to confirm latest stable versions
- Don't rely on memory for chart versions — they go stale quickly
- Check compatibility matrices between chart version, app version, and other cluster components
## Multi-Source Applications
- ArgoCD multi-source Applications use `$ref` syntax to combine external Helm charts with Git-stored values files
- Keep values files in Git alongside the ArgoCD Application manifest
## Timeout Handling
- Under cluster pressure (many events, etcd busy), default Helm timeouts may not be enough
- Increase timeout for initial installs (e.g., 10m instead of 5m)
- `helm upgrade --install` is idempotent — retries are safe

View File

@@ -1,68 +0,0 @@
# Kubernetes Patterns
## Volume Mounts
- **Avoid `subPath` volume mounts** for Secrets and ConfigMaps. The kubelet does not auto-update `subPath` mounts when the source changes — the pod must be restarted. Use directory mounts instead and adjust the application's config path.
- **Secret volume propagation is async.** After updating a Secret, the kubelet takes seconds to sync mounted volumes. A `rollout restart` issued immediately after may start pods with stale data. Add a short delay (5s) before restarting.
## Deployment Strategies
- **RWO PVC + RollingUpdate = Deadlock.** New pod can't attach the volume while the old pod holds it. Use `strategy: Recreate` for single-replica deployments with RWO PVCs.
- **SSA + strategy change conflict.** Switching from RollingUpdate to Recreate via ServerSideApply fails because SSA won't remove the old `rollingUpdate` field. Must patch the live resource first.
## Naming
- `metadata.name` must be DNS-1035 compliant — no dots allowed. Replace dots with dashes (e.g., `oreillyit-nz` not `oreillyit.nz`). Label values CAN contain dots.
## Bootstrap Ordering
Some components have chicken-and-egg dependencies:
1. CNI (e.g., Cilium) must be installed before anything else — nodes are NotReady without it
2. GitOps controller (e.g., ArgoCD) installed second
3. Root app applied last — the GitOps controller then "adopts" CLI-installed releases
Manual bootstrap secrets (encryption keys, OIDC client secrets) must be documented as explicit steps.
## Network Policies
- DNS egress for `toFQDNs` rules must use `toEndpoints` targeting kube-dns pods with `rules.dns` — this triggers the DNS proxy. Using `toCIDRSet` for DNS bypasses the proxy and FQDN rules never populate.
- Cross-namespace policies need explicit namespace matching (e.g., `matchExpressions` on namespace label).
- Always test from the actual consumer namespace, not same-namespace test pods.
## Probe Strategy
- **Liveness vs readiness probes serve different purposes.** TCP checks confirm the process is listening (liveness). Exec/command checks confirm the application is ready to serve (readiness). Don't conflate them.
- **Probes must match application host validation.** Applications that validate Host headers (e.g., Next.js `ALLOWED_HOSTS`) will reject probes sent to the pod IP. Set `httpGet.httpHeaders` with the expected Host value.
- **Don't load credentials into liveness probes.** If readiness requires an authenticated check (e.g., `sqlcmd`), use a simple TCP check for liveness and reserve the authenticated check for readiness only.
## Init Container Patterns
- **Writable config via init container + emptyDir.** When apps require writable directories but ConfigMaps are read-only, use an init container to copy config into an emptyDir volume that the main container mounts read-write.
- **Privilege separation.** Init containers can run as root to create directories or set ownership, while the main container runs as a non-root UID. Prefer this over running the entire workload as root.
- **Non-root images have hidden filesystem requirements.** Many modern images (e.g., MSSQL 2022, UID 10001) need writable directories beyond the obvious ones. Always check image documentation or `docker inspect` before writing manifests.
## StatefulSet Edge Cases
- **CrashLoopBackOff pods won't auto-replace on spec update.** The StatefulSet controller won't delete and recreate a crashing pod when you update the spec — manual `kubectl delete pod` is required to force recreation.
- **Immutable field diffs can deadlock auto-sync.** StatefulSet fields like `volumeClaimTemplates` are immutable after creation. GitOps controllers (ArgoCD) will show permanent OutOfSync if the desired state differs from the live immutable fields. Force sync or recreate the StatefulSet.
## GitOps: Imperative vs Declarative
- **Never use imperative operations on GitOps-managed resources.** `kubectl rollout restart` adds annotations that conflict with the GitOps controller's desired state, causing permanent OutOfSync. Use declarative paths instead — update a configmap hash annotation in Git, or change a pod template label.
- **ArgoCD reconciliation has latency.** New Application manifests don't appear immediately due to polling intervals. Use manual refresh annotations when automation needs immediate reconciliation.
## PodSecurity Alignment
- **Namespace PodSecurity labels must match container security contexts.** DinD, CSI drivers, and other privileged workloads need `pod-security.kubernetes.io/enforce: privileged` on their namespace. A `baseline` or `restricted` namespace silently blocks privileged pods.
- **Document privileged namespace requirements.** When a workload needs elevated privileges, document the specific requirement (e.g., "Docker-in-Docker for CI builds") alongside the namespace label.
## ArgoCD Source Type Detection
- **ArgoCD auto-detects Kustomize.** When a source directory contains `kustomization.yaml`, ArgoCD runs Kustomize automatically. Adding an explicit `directory:` source type overrides this detection and causes ArgoCD to try applying `kustomization.yaml` as a raw K8s resource, which fails with schema errors. Remove explicit directory source types from Kustomize sources.
- **Credential template URL-prefix must match exactly.** ArgoCD repo-creds secrets use URL prefix matching. When migrating Git server URLs (hostname, protocol, or port changes), update the credential template to match the new prefix. Stale credentials cause "authentication required" errors on all apps using that prefix.
## Miscellaneous
- `enableServiceLinks: false` may be needed when K8s-injected service env vars conflict with app config (e.g., Authelia interprets `AUTHELIA_*` service vars as configuration).
- Proxmox VM names must match K8s node hostnames for cloud controller manager integration.
- Metrics-server on Talos needs `--kubelet-insecure-tls` (self-signed kubelet certs).

View File

@@ -1,94 +0,0 @@
# Linting & Formatting
Automated code formatting and linting integrated into the Claude Code workflow via PostToolUse hooks and git pre-commit hooks.
## Architecture
```
claude-foundations/formatters/ ← canonical formatter scripts (one per extension)
project/formatters/ ← symlinks to the formatters this project uses
~/.claude/hooks/post-edit-lint.sh ← dispatches to project formatters on Edit/Write
.git/hooks/pre-commit ← symlink to pre-commit-lint.sh
```
Projects opt in by symlinking only the formatters they need. No formatter = no-op.
## Tool Choices by Language
| Language | Formatter | Linter | Config File |
|----------|-----------|--------|-------------|
| Python | `ruff format` | `ruff check` | `pyproject.toml` |
| Shell | `shfmt` | `shellcheck` | `.editorconfig` |
| TypeScript/JavaScript | `biome format` | `biome check` | `biome.json` |
| SQL | `sqlfluff fix` | `sqlfluff lint` | `.sqlfluff` |
| JSON/YAML/Markdown | `prettier` | — | `.prettierrc` |
### Why these tools?
- **ruff**: Rust-based, extremely fast, replaces black + isort + flake8 + pyflakes in one tool
- **biome**: Rust-based, replaces prettier + eslint for JS/TS in one tool
- **shfmt + shellcheck**: The standard combo for shell scripts; shfmt formats, shellcheck catches bugs
- **prettier**: Handles JSON/YAML/MD well; biome doesn't cover these yet
## Formatter Script Contract
Every script in `formatters/` follows the same interface:
- **Input:** `$1` = absolute file path
- **Behaviour:** format the file in place, then lint it
- **Stdout:** suppressed
- **Stderr:** lint warnings/errors that couldn't be auto-fixed
- **Exit code:** `0` if clean, `1` if lint errors remain
- **Missing tools:** exit `0` silently (`command -v` check)
- **No `set -e`:** individual commands may fail; execution must continue
## PostToolUse Integration
The `post-edit-lint.sh` hook fires on Edit/Write/MultiEdit:
1. Extracts `file_path` and `cwd` from stdin JSON
2. Walks up from `cwd` to find `formatters/` directory
3. Creates a checkpoint using `git hash-object` (fast, no commits)
4. Runs the matching formatter
5. On clean pass: removes checkpoint, exits 0 (silent)
6. On lint errors: keeps checkpoint, exits 2 (feeds errors to Claude)
Exit code 2 is special for PostToolUse — it feeds stderr back to Claude as feedback without blocking the edit.
## Checkpoint Mechanism
Uses `git hash-object -w` to store pre-format content as an orphan blob (~1ms, no commits, no stash). The `.pre-lint` file stores only a 40-char SHA. Falls back to `cp` outside git repos.
```bash
# Revert after lint errors:
git cat-file blob "$(cat file.py.pre-lint)" > file.py
rm file.py.pre-lint
```
## Subagent Fix Pattern
When lint errors occur, Claude sees the errors via stderr. The recommended workflow:
1. Claude reports the errors to the user
2. If the user says "fix", Claude spawns a subagent via the Agent tool
3. The subagent reads the file and errors, makes Edit calls to fix them
4. Each Edit triggers the hook again (re-format, re-lint) in the subagent
5. Fix iterations stay in the subagent's context, not the main conversation
## Pre-commit Integration
`pre-commit-lint.sh` reuses the same formatter scripts:
1. Iterates staged files (Added/Modified only)
2. Runs matching formatters
3. Re-stages formatted files
4. Exits non-zero if lint errors remain (blocks commit)
Install: symlink `.git/hooks/pre-commit``pre-commit-lint.sh`, or use `setup-formatters.sh` which does this automatically.
## Setup Checklist
1. Run `scripts/setup-formatters.sh <project> <ext> [<ext> ...]` to symlink formatters
2. Install the required tools (`ruff`, `shfmt`, `shellcheck`, `biome`, `prettier`, `sqlfluff`)
3. Create per-project config files as needed (`pyproject.toml`, `.editorconfig`, etc.)
4. Run `scripts/install-hooks.sh` to install the PostToolUse hook (one-time global setup)
5. Use `/linter scan` to verify everything is connected

View File

@@ -1,42 +0,0 @@
# Milestones & Reflections
## Milestone Structure
Break projects into numbered milestones (M1, M2, ...). This provides clear checkpoints, measurable progress, and natural reflection points.
## Milestone Completion Checklist
Every milestone MUST include:
### 1. Verification Script
`scripts/verify-m<N>.sh` — automated checks confirming all milestone outcomes.
- Idempotent, non-destructive, returns non-zero on failure
- Colour output (green/red) for pass/fail
- Environment-resilient (no sudo, test from accessible side)
- Check for default/insecure credentials
### 2. Milestone Reflection
Write `memory/m<N>-reflection.md` by reviewing the **entire conversation** from milestone start. Cover:
- **Process improvements:** What slowed us down? Wrong assumptions? Where did we go in circles? What would make this faster if redone from scratch?
- **Key knowledge for reproduction:** Critical facts, gotchas, non-obvious config details, version-specific quirks, debugging detours
- **Scripts and automation:** Existing tools that proved valuable, new scripts to build, patterns to extract into reusable automation
- **Future improvements:** Ideas that surfaced but don't belong in current scope — add to FUTURE.md
### 3. Updated README.md
Ensure the scripts section, milestone table, and setup steps are current.
### 4. Updated CLAUDE.md
Reflect new repo structure, conventions, and patterns discovered during the milestone.
## Reflection Quality
Good reflections capture:
- Commit stats (total commits, fix percentage) to measure validation discipline
- Longest detour and root cause
- Most avoidable waste and what would have prevented it
- Concrete checklist items for future similar work

View File

@@ -1,33 +0,0 @@
# Networking & Infrastructure
## nftables Flush Ruleset on Remote Hosts
On remote hosts, `nftables flush ruleset` followed by a failed rule load leaves the host with NO firewall. SSH survives only on existing connections — new connections are blocked or allowed depending on the default policy.
**Always validate rules before applying:** `nft -c -f <rulefile>` does a dry-run parse. For extra safety, deploy a cron-based auto-rollback timer that reverts rules unless explicitly confirmed (similar to `shutdown -c` pattern).
## systemd Socket Activation Overrides Config File Ports
On modern Linux systems (Ubuntu 24.04+), systemd socket activation controls the listening port for services like SSH. Editing the service config file alone (e.g., `sshd_config Port 2222`) has no effect — the socket unit still binds the original port.
**Check socket activation first:** `systemctl cat <service>.socket` shows whether socket activation is in play. If so, override the socket unit's `ListenStream` directive, not the service config.
## Docker Sets iptables FORWARD Policy to DROP
Docker sets the iptables FORWARD chain default policy to DROP. This affects ALL forwarding on the host, not just Docker traffic. Non-Docker forwarding (VPN, VM bridges, custom NAT) silently breaks.
**Fix:** Add explicit ACCEPT rules in the `DOCKER-USER` chain for non-Docker forwarding needs. This chain is processed before Docker's own rules and persists across Docker restarts.
## HTTP Host Header vs TLS SNI Are Different Layers
When proxying to a backend over HTTPS, two independent identifiers must be set correctly:
- **TLS SNI** — sent during the TLS handshake, used for certificate selection. Missing SNI causes `x509: cannot validate certificate for <IP>`.
- **HTTP Host header** — sent after TLS is established, used for virtual host routing. Missing or wrong Host header causes 404 from the backend.
A reverse proxy must set both. They often need to be the same value, but they're configured independently.
## Wildcard Certs in Auto-Renewing Proxies
Auto-renewing proxies (Caddy, Traefik with Let's Encrypt, etc.) that also support file-loaded certificates treat file-loaded certs as globally available. A wildcard cert loaded for one site block will match ALL matching subdomains, silently preventing automatic certificate issuance for other sites.
**Rule:** Use automatic certificate management for all sites. Don't mix file-loaded and automatic certs unless you understand the matching priority.

View File

@@ -1,221 +0,0 @@
# Octopus Deploy Process Templates
Best practices for creating and managing Octopus Deploy process templates using OCL (Octopus Configuration Language) in Platform Hub.
## Key Concepts
- **Step templates** (action templates) are reusable individual steps, created via API or UI, stored in a space's library
- **Process templates** are reusable multi-step deployment processes, stored as OCL files in Platform Hub's Git repo
- **Project templates** compose process templates into full project configurations (feature coming soon)
- Process templates live in `.octopus/process-templates/<slug>.ocl` in the Platform Hub Git repo
- Projects consume process templates via `process_template` blocks in their `deployment_process.ocl`
## OCL File Structure
### Process Template File
```hcl
name = "Deploy to Kubernetes - Helm"
description = "Standard Helm-based Kubernetes deployment with pre-validation and smoke tests"
# Parameters — values supplied by consuming projects
parameter "target_tags" {
display_settings = {
Octopus.ControlType = "TargetTags"
}
help_text = "Kubernetes target tags"
label = "Target Tags"
}
parameter "cloud_target" {
display_settings = {
Octopus.ControlType = "SingleLineText"
}
help_text = "Cloud provider (gcp, aws, azure)"
label = "Cloud Target"
value "gcp" {} # default value
}
# Steps — ordered deployment steps
step "deploy-helm" {
name = "Deploy via Helm"
properties = {
Octopus.Action.TargetRoles = "#{target_tags}"
}
action {
action_type = "Octopus.Script"
properties = {
Octopus.Action.Script.ScriptSource = "Inline"
Octopus.Action.Script.Syntax = "PowerShell"
Octopus.Action.Script.ScriptBody = "Write-Host 'Deploying...'"
}
worker_pool_variable = ""
}
}
```
### How Projects Consume Process Templates
In a project's `deployment_process.ocl`:
```hcl
process_template "deploy-app" {
name = "Deploy Application"
process_template_slug = "deploy-to-kubernetes-helm"
version_mask = "1.X" # auto-update minor/patch
parameter "target_tags" {
value = "kubernetes,production"
}
parameter "cloud_target" {
value = "aws"
}
}
```
## OCL Syntax Rules
From the EBNF grammar (https://github.com/OctopusDeploy/Ocl):
- **Name, `=`, and value** must be on the same line
- **Block name, labels, and `{`** must be on the same line
- **Closing `}`** must be on its own line (except empty blocks like `value "default" {}`)
- **Strings** use double quotes, cannot contain unescaped `"`
- **Multi-line strings** use heredoc: `<<-EOF` / `EOF` (indented variant)
- **Arrays** use `["item1", "item2"]`
- **Dictionaries** use `{ key = value }` (one entry per line inside braces)
## Referencing Step Templates
Step templates are referenced by **ID and version** in the action's properties, NOT by name:
```hcl
step "run-tests" {
name = "Run Unit Tests"
action {
# Reference a step template instead of action_type
properties = {
Octopus.Action.Template.Id = "ActionTemplates-62"
Octopus.Action.Template.Version = "1"
# Step template parameter values
Language = "go"
CloudTarget = "gcp"
}
worker_pool_variable = ""
}
}
```
When NOT using a step template, define `action_type` directly:
```hcl
action {
action_type = "Octopus.Script"
properties = { ... }
}
```
## Channel Scoping
Scope steps to specific channels using the `channels` attribute on the action block. Uses **channel slugs** (auto-generated from names):
```hcl
action {
action_type = "Octopus.Script"
channels = ["non-prod"] # only runs in non-prod channel
properties = { ... }
}
```
## Package References
```hcl
# Container image for worker execution
container {
feed = "registered-dockerhub" # feed slug, not ID
image = "octopusdeploy/worker-tools:ubuntu.22.04"
}
# Package reference in a step
packages "MyPackage" {
acquisition_location = "NotAcquired" # Server | ExecutionTarget | NotAcquired
feed = "platformhub-non-prod" # feed slug
package_id = "paul-oreilly-octopus/nonprod/listing-service"
properties = {
SelectionMode = "immediate"
}
}
```
## Parameter Types
| Control Type | `Octopus.ControlType` Value | Can Have Default |
|---|---|---|
| Single-line text | `SingleLineText` | Yes |
| Multi-line text | `MultiLineText` | Yes |
| Sensitive/password | `Sensitive` | Yes (encrypted) |
| Checkbox | `Checkbox` | Yes |
| Dropdown | `Select` | Yes |
| AWS/Azure/GCP Account | `AWSAccount` etc. | Yes |
| Worker Pool | (worker pool) | No |
| Package | (package) | No |
| Target Tags | `TargetTags` | No |
| Environments | (environments) | No |
| Channels | (channels) | No |
## Step Properties Reference
| Property | Type | Values | Default |
|---|---|---|---|
| `step.condition` | enum | `Success`, `Failure`, `Always`, `Variable` | `Success` |
| `step.start_trigger` | enum | `StartAfterPrevious`, `StartWithPrevious` | `StartAfterPrevious` |
| `step.package_requirement` | enum | `LetOctopusDecide`, `BeforePackageAcquisition`, `AfterPackageAcquisition` | `LetOctopusDecide` |
| `action.channels` | string[] | channel slugs | all |
| `action.environments` | string[] | environment slugs | all |
| `action.excluded_environments` | string[] | environment slugs | none |
| `action.is_disabled` | bool | | `False` |
| `action.is_required` | bool | | `False` |
| `action.notes` | string | step description | |
| `action.worker_pool` | string | worker pool slug | |
| `action.worker_pool_variable` | string | variable name | |
## Versioning
- Process templates use **semantic versioning** (major.minor.patch)
- `version_mask = "1.X"` in consuming projects auto-updates on minor/patch changes
- **Major version bumps** require explicit upgrade by consuming projects
- Keep major bumps for breaking changes (parameter renames, step removals)
- Minor/patch for new optional parameters, script improvements, bug fixes
## Gotchas
1. **Step templates are space-scoped.** Process templates in Platform Hub cannot reference `ActionTemplates-*` IDs from other spaces. If you need reusable steps, use inline `action_type = "Octopus.Script"` in the process template OCL. Step templates are useful within a single space's projects, but not for cross-space process templates.
2. **Process template names** cannot contain parentheses, slashes, or ampersands — only letters, numbers, periods, commas, dashes, underscores, and hashes.
3. **Heredoc for multi-line scripts** — use `<<-EOT` / `EOT` for PowerShell scripts that contain double quotes. The `-` prefix allows indented closing tags.
4. **Every step needs a worker pool.** Process templates must have a `worker_pool` parameter (type `WorkerPool`), and every action must set `worker_pool_variable = "worker_pool"` referencing it. Without this, the template will fail to parse with "A step must specify a worker pool parameter".
5. **Publishing and sharing is UI-only.** Process template sharing (which spaces can see/use a template) is stored in the Octopus database, not in Git/OCL. You must publish and share each template through the UI. There is no API or CLI for this currently.
## Best Practices
1. **One template per deployment pattern**, not per cloud. Use parameters to vary cloud-specific behaviour.
2. **Use step templates for reusable individual steps**, process templates for reusable multi-step workflows.
3. **Parameters should have sensible defaults** where possible — reduces friction for consuming projects.
4. **Use `TargetTags` parameter type** for Kubernetes target selection rather than hardcoding roles.
5. **Name templates with the action, not the technology**: "Deploy to Kubernetes" not "Helm Chart Deployer".
6. **Keep descriptions updated** — they appear in the UI when browsing templates.
7. **Process templates cannot reference the project's own Git repo** for scripts — use inline scripts or external URLs.
8. **Test templates** by creating a test project that consumes them before sharing widely.
## References
- OCL Syntax: https://octopus.com/docs/projects/version-control/ocl-file-format
- Config as Code Reference: https://octopus.com/docs/projects/version-control/config-as-code-reference
- Process Templates: https://octopus.com/docs/platform-hub/templates/process-templates
- Template Parameters: https://octopus.com/docs/platform-hub/templates/parameters
- Publishing & Sharing: https://octopus.com/docs/platform-hub/templates/publishing-and-sharing
- Best Practices: https://octopus.com/docs/platform-hub/templates/process-templates/best-practices
- Troubleshooting: https://octopus.com/docs/platform-hub/templates/process-templates/troubleshooting
- OCL Grammar (EBNF): https://github.com/OctopusDeploy/Ocl

View File

@@ -1,48 +0,0 @@
# Scripting Conventions
## Structure
- All scripts live in `scripts/` and run from the repository root
- Scripts should be idempotent and safe to re-run
- Exit non-zero on failure so `&&` chains work naturally
## Verification Scripts
- Automated checks confirming milestone or feature outcomes
- Use colour output (green/red) for pass/fail indicators
- Should be non-destructive and environment-resilient
- Avoid needing sudo — test from the accessible side of a connection instead
- Check for default/insecure credentials and print remediation instructions on failure
- Use `curl --resolve` to bypass DNS/proxy layers when testing direct connectivity
## Automation Triggers
If you run the same 3+ commands in sequence more than once, it should become a script. Look for:
- Repeated command sequences in conversation history
- Steps requiring careful ordering
- Multi-step manual processes that are error-prone
## Error Handling by Tool Purpose
Not all scripts need the same error handling strategy:
- **Destructive scripts** (deploy, configure, delete) should use `set -euo pipefail` — fail fast on any error.
- **Reporting/read-only scripts** (status dashboards, aggregation, monitoring) should start without `set -e` — complex data collection from multiple sources is hard to debug under errexit. Use explicit conditional checks instead.
- **The choice depends on the tool's purpose.** A script that writes to production needs strict error handling. A script that reads from 10 sources and aggregates results needs resilience.
## Dryrun Mode
Every script that modifies state should support `--dryrun` / `-n`:
- Makes the script self-documenting about its side effects
- Enables safe testing and review before execution
- Enables test harnesses that verify output without executing changes
- Dryrun output should show exactly what would happen, not a summary
## Shell Gotchas
- `((PASS++))` fails under `set -e` when PASS=0 — the expression evaluates to 0 (false), triggering errexit. Use `PASS=$((PASS + 1))` instead.
- `set -e` silently terminates complex pipelines and subshells with no output — makes debugging extremely difficult. Also kills command substitutions that capture non-zero exit codes (e.g., `result=$(grep "pattern" file)` exits if grep finds nothing).
- `grep` interprets option-like strings (starting with `-`) as flags — use `--` terminator before patterns or input that may start with dashes.
- Always quote variables in conditionals and file paths
- Use `trap` for cleanup of temp files and credentials
- **Use `git diff --numstat` for binary file detection** instead of `file`. The `file` command is unreliable (marks shell scripts as "executable"), while `git diff --numstat` shows `-` for binary files using git's robust binary detection heuristics.
- **Use `cat -A` to diagnose invisible character issues.** Reveals non-printing characters like em dashes, zero-width spaces, and smart quotes that look identical to correct characters but break YAML parsers, config files, and frontmatter. Essential when a file looks correct but tooling rejects it.

View File

@@ -1,53 +0,0 @@
# Secrets Management
## SOPS + age
SOPS with age encryption is the standard across all projects. A single `.sops.yaml` at the repo root defines path-based encryption rules.
### File Naming
- `.sops.yaml` path-based rules match specific filename patterns (e.g., `**/*secret*.yaml`)
- Non-secret files must NOT contain `secret` in their name, or the pre-commit hook will encrypt them
- KSOPS generator files should be named `ksops-generator.yaml`, not `secret-generator.yaml`
### encrypted_regex Gotcha
When using `encrypted_regex` for selective field encryption (e.g., Ansible group_vars), variable names must contain a keyword that matches the regex (e.g., `password|private_key|api_key|secret|token`). Arbitrary key names are silently left unencrypted.
### SOPS Vars Plugin
Each Ansible project needs `vars_plugins_enabled = host_group_vars,community.sops.sops` in `ansible.cfg`. Files in `group_vars/` must be named after a group (e.g., `all.sops.yaml`), not arbitrary names.
### Interactive Editor Pitfalls
- `sops <file>` opens an interactive editor — fails in non-interactive sessions
- `sops -e /tmp/file` fails when the temp path doesn't match `.sops.yaml` rules
- Multiple `sops --set` calls can corrupt files — use the interactive editor for multi-field edits
## Credential Handling
- **Never pass secrets via command-line arguments** — visible in `ps` output
- Use `@file` references, environment variables sourced at runtime, or stdin
- For Ansible, use temp files with `trap rm` cleanup: `-e "@${tmpfile}"`
- Read secrets at execution time and use them ephemerally — never cache or persist values
- Reference the **existence** of a secret file in docs, never its contents
## Bootstrap Secrets
Some secrets are chicken-and-egg (e.g., the age decryption key for ArgoCD's KSOPS). These must be created manually as a bootstrap step and documented clearly.
## Credential Lifecycle Management
- **Track credential expiry dates.** OAuth client secrets, API tokens, and certificates have expiry dates that can cause silent failures. Document expiry dates when creating credentials.
- **Set alerts before expiry.** For long-lived credentials (e.g., 720-day OAuth client secrets), set calendar reminders or automated monitoring alerts well before they expire.
- **Rotation plan.** Know the rotation procedure before you need it — some credential types (e.g., Azure app registrations) require coordinated updates across multiple systems.
## Multi-Field Secret Files
Secret files that contain multiple fields (e.g., repo URL, token, username) cannot be used as bare values. Consumers must parse individual fields (e.g., `grep + awk` or structured YAML/JSON parsing).
The multi-field format is preferable because it's self-documenting — all related credentials live together. But any automation reading the file needs extraction logic, not just `cat`.
## Backup Considerations
Backup plans must include encryption keys (age private keys, etc.) so that encrypted data in Git repos remains recoverable.

View File

@@ -1,46 +0,0 @@
# Claude Code Skills
## Skill Structure
- Each skill lives in `skills/<skill-name>/SKILL.md`
- Skills should be project-agnostic where possible — use dynamic context injection to adapt
- After adding a new skill, run the install script to register it
- Skills only useful for one project should live in that project's `.claude/skills/` instead
## Authoring Guidelines
- **Inline by default** — only use `context: fork` if the skill genuinely doesn't need conversation history
- **Pre-fetch context** with `!`command`` injection to reduce tool calls during execution
- **Restrict tools** with `allowed-tools` to the minimum needed — reduces permission prompts
- **Use $ARGUMENTS** for user input, `$0`, `$1` etc. for positional args
- Dynamic commands in `!`command`` run at skill load time, not during Claude's execution
## Portable Path Resolution
- **Use `CLAUDE_PROJECT_ROOT` env var** for cross-project path references in `!`command`` blocks. Hardcoded absolute paths (e.g., `~/dev/claude/...`) are user-specific. Relative paths (`../`) break depending on CWD and can trigger sandbox violations when they resolve outside allowed directories.
- **Add a detection fallback.** Include a "Step 0" in skill instructions that detects the project root by walking up the directory tree to find the highest `CLAUDE.md` if the env var isn't set. This makes skills work even without prior setup.
- **Keep config paths relative to the root.** Settings files should use paths relative to `CLAUDE_PROJECT_ROOT` (e.g., `projects_dir: projects`) rather than absolute paths, so they're portable across machines.
## Skill Discovery Timing
- **Skills are discovered at session start, not dynamically.** Creating or symlink a new skill mid-session requires restarting Claude Code to use it as a slash command.
- **Broken symlinks cause silent failures under `set -e`.** `readlink -f` on a broken symlink returns empty string. The install script should validate symlinks and remove stale ones.
## `!`command`` Gotchas
- **No `$()` command substitution** — the permission checker rejects commands containing `$()`
- **No complex shell pipelines relying on subshells** — keep commands simple and self-contained
- **`allowed-tools` patterns must match the command binary** — each binary used in `!`command`` blocks needs its own pattern
- **Prefer specific tool patterns over broad ones** — `Bash(git log *)` is safer than `Bash(git *)`
- **Fallback to tool instructions for dynamic paths** — if a command needs `$ARGUMENTS` to compute a path, use a plain-text instruction telling Claude to use the Read tool instead
- **Env var expansion works** — `${CLAUDE_PROJECT_ROOT}` expands in `!`command`` blocks because they run as shell commands. This is the recommended pattern for portable cross-project paths.
## Non-ASCII in YAML Frontmatter
Skills with em dashes (`—`), smart quotes (`"`), or other non-ASCII characters in the YAML frontmatter `description` field fail to load silently — the skill appears as "Unknown skill" with no error message. The markdown body below the frontmatter can contain any characters.
AI models commonly generate em dashes instead of regular dashes. Always validate skill files (e.g., with `cat -A` or a dedicated validator) before committing.
## Profile-Independent Skills Directories
Each Claude Code profile maintains a completely independent skills directory. Skills installed in one profile (e.g., default) are unavailable in other profiles (e.g., `.claude-octopus`). Install scripts must use the profile-aware config directory path rather than hardcoded paths like `~/.claude/skills/`.

View File

@@ -1,227 +0,0 @@
# Spec-Driven Development with AI Agents
Best practices for using structured specifications to coordinate AI agent implementation work. Extracted from real project experience (agent-runtimes) and industry research (OpenSpec, Codified Context paper, Addy Osmani's workflow guides).
## Why Specs Matter for AI Agents
AI agents trust documentation absolutely. A well-written spec gives an agent everything it needs to implement a subsystem without reading the entire codebase. A stale or vague spec causes silent failures where agents generate code that is structurally valid but architecturally wrong.
Specs serve three functions that CLAUDE.md alone cannot:
1. **Compressed context** — an agent reads one spec, not 300 lines of mixed concerns
2. **Testable contracts** — numbered requirements and scenarios translate directly to pytest
3. **Handoff boundaries** — an agent working on the dispatcher doesn't need to understand the entrypoint internals, just the interface between them
## Spec Structure
Each spec follows a consistent template. Sections are ordered so an agent can read top-down and build understanding progressively.
### Required Sections
1. **Overview** — What this subsystem does. 2-3 sentences. An agent should know if this spec is relevant after reading this.
2. **Responsibilities** — What this subsystem owns and what it delegates. Prevents scope creep during implementation.
3. **Dependencies** — Which other specs to read first. Keeps the reading list minimal.
4. **Data Model** — Types, schemas, state machines, interfaces. The concrete contract.
5. **Requirements** — Numbered functional requirements (e.g., E-1, E-2). Each must be independently testable.
6. **Scenarios** — Concrete given/when/then examples. These become test functions.
### Optional Sections
7. **Interface** — API surface, function signatures, HTTP endpoints. Include when the subsystem has an external-facing API.
8. **Extension Points** — How to add new capabilities without modifying existing code. Step-by-step instructions.
9. **Error Handling** — Failure modes and expected behaviour. Prevents agents from inventing their own error strategies.
### Writing Guidelines
- **Be specific, not comprehensive.** A spec that says "handle errors appropriately" is useless. A spec that says "return exit code 124 on timeout" is testable.
- **Include the why.** Design intent and constraints prevent agents from making structurally valid but architecturally wrong changes. Requirements without rationale are followed mechanically — agents can't judge edge cases or make trade-offs. Every constraint needs a "Why:" line. Example: `"Secrets never in payload"` needs `"because payloads may be logged and stored in task history"`.
- **Use concrete examples.** Every data model section should include a realistic JSON/code example, not just a schema.
- **Cross-reference, don't duplicate.** If two specs share a concept (e.g., the payload schema), one spec owns it and the other links to it.
- **Keep each spec self-contained.** An agent should be able to implement a subsystem by reading the target spec plus its listed dependencies. If it needs to read CLAUDE.md, the spec is incomplete.
## Requirement Numbering
Each spec uses a short prefix derived from its name, followed by a sequential number:
| Spec | Prefix | Example |
|---|---|---|
| payload.md | P | P-1, P-2 |
| entrypoint.md | E | E-1, E-2 |
| actions.md | A | A-1, A-2 |
| runners.md | R | R-1, R-2 |
| dispatcher.md | D | D-1, D-2 |
| control-plane.md | CP | CP-1, CP-2 |
Requirements must be:
- **Independently testable** — each maps to one or more test functions
- **Unambiguous** — an agent can determine pass/fail without human judgement
- **Stable** — changing a requirement number invalidates tests, so avoid renumbering
## Scenarios as Test Blueprints
Every scenario in a spec should be directly translatable to a test function. Use this format:
```markdown
### Scenario: Pre-action failure
**Given:** Payload with clone pre-action (invalid repo URL)
**When:** Clone fails (git returns non-zero)
**Then:** on_error actions run, container exits 1. Runner never executes.
```
This becomes:
```python
def test_scenario_preaction_failure_runs_on_error_and_exits_1(...):
"""Given clone fails, on_error runs and exits 1. Runner never executes."""
```
Guidelines:
- Each scenario tests one behaviour, not a combination
- Include both happy paths and error paths
- Name the scenario descriptively — it becomes the test function's docstring
- Include enough setup detail that an agent can write the test without guessing
## The Spec → Test → Code Workflow
This is the core development loop. Tests are written from the spec before code exists.
### 1. Write or Update the Spec
Define requirements and scenarios. Get them reviewed. The spec is the source of truth for what the system should do.
### 2. Write Tests from the Spec
Translate requirements and scenarios into pytest functions. Tests should:
- Map to requirement IDs in their names: `test_e4_preaction_failure_skips_remaining`
- Use the scenario's given/when/then as the test body structure
- Mock external dependencies (subprocess, HTTP, filesystem)
- Run fast (no Docker, no network, no real APIs)
### 3. Run the Tests — They Should All Fail
This confirms the tests are actually testing something. If a test passes before implementation, it's either testing the wrong thing or the feature already exists.
### 4. Implement Until Tests Pass
Write the minimum code to make tests pass. The spec defines what, the tests verify it, the code implements it.
### 5. Update Spec if Implementation Reveals Issues
Sometimes implementation reveals that a requirement is unworkable or incomplete. Update the spec, update the test, then update the code. The spec stays authoritative.
## Spec Maintenance
### Preventing Drift
Specs drift from code when they're treated as planning documents that are "done" after implementation. They must be treated as living contracts.
**Rules:**
- **Spec changes require test changes.** If a requirement changes, its test must change in the same commit.
- **Code changes that affect interfaces require spec changes.** If a function signature, API endpoint, or data schema changes, the relevant spec must be updated in the same commit.
- **New features require spec-first.** Add the requirement and scenario to the spec, write the test, then implement.
### CI Enforcement
Enforce spec hygiene with automated checks:
1. **Pre-commit hook** — run pytest, block commit on failure (already implemented)
2. **Spec coverage check** — a script that verifies every numbered requirement has at least one test function referencing it
3. **Orphan test detection** — tests referencing requirement IDs that no longer exist in specs
### Review Checklist
When reviewing a PR that touches a spec subsystem:
- [ ] Spec updated if interface or behaviour changed
- [ ] Test added/updated for new/changed requirements
- [ ] Cross-references still valid
- [ ] No requirements removed without deprecation note
## Context Architecture for Agents
Based on the Codified Context paper (108k-line system, 283 sessions), structure project knowledge in three tiers:
### Tier 1: Hot Context (Always Loaded)
CLAUDE.md — conventions, env vars, repo structure, scripts. Loaded every session. Keep under ~300 lines by linking to details elsewhere.
### Tier 2: Spec Context (Per-Task)
`spec/` files — loaded based on what the agent is working on. An agent implementing a new action reads `spec/actions.md` + `spec/payload.md`. An agent working on the dispatcher reads `spec/dispatcher.md` + `spec/container-backends.md`.
The spec index (SPEC.md) has a "read this when..." column to guide selection.
### Tier 3: Cold Context (On-Demand)
`memory/` files — gotchas, reflections, decisions. Loaded only when relevant. An agent hitting a weird Cilium issue checks `memory/gotchas-cilium.md`.
### Routing Context to Agents
When launching an agent to work on a subsystem:
1. Point it at the relevant spec(s) via its prompt
2. Include CLAUDE.md for conventions
3. Let it pull from memory/ on-demand if it hits issues
Don't load everything — agents perform better with focused context than with a 50-page dump.
## Testing Depth
The spec→test→code workflow defines *when* to write tests. For *how* to write comprehensive tests — edge case discovery, property-based testing, mutation testing, AI agent testing patterns — see [Test-Driven Development](test-driven-development.md).
## Post-Write Spec Audit
After writing specs, audit them against best practices before implementation. Common gap categories:
1. **Missing rationale** — Constraints without "Why:" lines. Agents follow them mechanically but can't judge edge cases.
2. **Missing error/failure scenarios** — Happy paths are covered but failure modes aren't specified.
3. **Cross-spec interface misalignment** — Two specs describe the same interface differently.
4. **Vague requirements** — "Handle errors appropriately" instead of specific error codes and behaviours.
5. **Missing specs for discovered subsystems** — Implementation reveals components that weren't planned for.
Write-then-audit is more productive than trying to get specs perfect on the first pass. The audit step catches systematic gaps across all specs at once.
## Planning Session Limits
Architecture decisions, infrastructure research, and spec refinement each get one planning session. After three sessions of planning, start implementation. Specs are hypotheses that need code to validate them — extended planning without implementation produces diminishing returns and theoretical designs that don't survive contact with reality.
## Categorize Findings Before Acting
When a spec review or audit produces many findings, categorize them by priority (high/medium/low) before making changes. Present the categorized list for alignment before editing. Starting edits without prioritization leads to scope creep — low-priority cosmetic fixes consume time that should go to high-priority structural gaps.
## Multi-Agent Orchestration Practices
### Commit WIP Before Decomposing Tasks
Untracked and uncommitted files are NOT available in git worktrees. If agents work in worktrees (or container-mounted worktrees), they won't see specs, plans, or dependency outputs that haven't been committed. Commit to a staging branch before decomposition — this eliminates the dominant overhead of manually copying files into each worktree.
### Agents Must Self-Verify with Tests
Add "Run tests and fix any failures" to every implementation agent prompt. Agents that write code without running tests produce bugs that only surface during assembly. Self-verification catches issues while the agent still has full context of what it wrote.
### State Import and Style Conventions Explicitly
Agents default to standard language conventions (e.g., relative Python imports, standard packaging). If the project uses non-standard patterns (bare imports, specific naming conventions, module-level structure), state them explicitly in the prompt. A single line like "Use `from harness import X`, not `from .harness import X`" prevents import mismatches during assembly.
### Budget for Assembly Fixups
Parallel agent work produces ~3 fixups per orchestration run, each under 5 minutes. Common fixup categories: import conventions, module-level side effects, SDK exception constructor signatures, validator patterns. This is the expected cost of parallel work, not a failure. Budget 15-20 minutes for assembly and fixup after each orchestration run.
## Anti-Patterns
### Specs as documentation, not contracts
**Symptom:** Specs describe what was built, updated after the fact. Tests don't reference spec IDs.
**Fix:** Write specs before code. Tests reference requirement IDs. Specs are the input, not the output.
### Mega-spec
**Symptom:** One large spec covering the entire system. Agents must read thousands of lines to find what they need.
**Fix:** Split by subsystem. Each spec should be readable in under 5 minutes.
### Spec without scenarios
**Symptom:** Requirements are abstract ("handle errors gracefully"). No concrete examples.
**Fix:** Every requirement needs at least one given/when/then scenario with specific inputs and outputs.
### Implementation details in specs
**Symptom:** Spec dictates variable names, algorithm choices, internal data structures.
**Fix:** Specs define what and why, not how. The interface is specified; the implementation is free.
### Untested requirements
**Symptom:** Requirements exist in the spec but no test references them. They drift without anyone noticing.
**Fix:** Spec coverage check in CI. Every requirement ID must appear in at least one test function name.

View File

@@ -1,474 +0,0 @@
# Test-Driven Development for Spec-Based Projects
Best practices for writing comprehensive, regression-catching tests in projects that use structured specifications. Focuses on maximising test value (catching real bugs) rather than test volume (inflating coverage numbers). Extracted from industry research, academic papers (TDAD, Codified Context), and practitioner experience.
## Core Principle: Tests Are the Spec's Enforcement Layer
In a spec-driven project, the spec defines *what* and the tests *prove it*. A requirement without a test is an aspiration. A test without a requirement is undocumented behaviour. Keep them tightly coupled:
- Every numbered requirement (P-1, E-3) has at least one test
- Every test function name includes its requirement ID: `test_e3_preaction_failure_exits_1`
- Spec changes and test changes ship in the same commit
## Deriving Tests from Specs
### Requirements to Tests
Each spec requirement becomes one or more test functions. The mapping isn't always 1:1 — a requirement like "must respect timeout" needs tests for: default timeout, explicit timeout, timeout=0 (no limit), timeout exceeded.
```python
# From spec: R-4: Runners must respect runtime.timeout.
# Default 3600s. Value of 0 means no timeout.
def test_r4_default_timeout_is_3600():
"""R-4: When timeout not specified, default is 3600s."""
def test_r4_explicit_timeout_is_honoured():
"""R-4: When timeout=60, process killed after 60s."""
def test_r4_zero_timeout_means_no_limit():
"""R-4: When timeout=0, no timeout is applied."""
def test_r4_timeout_returns_exit_code_124():
"""R-4 + R-5: Timeout produces exit code 124."""
```
### Scenarios to Tests
GIVEN/WHEN/THEN scenarios translate directly to Arrange/Act/Assert:
```python
def test_scenario_preaction_failure_runs_on_error():
"""Given clone fails, on_error runs and exits 1. Runner never executes."""
# GIVEN — arrange
payload = make_payload(pre_actions=[{"action": "clone", "repo": "bad-url"}])
mock_clone = Mock(side_effect=subprocess.CalledProcessError(128, "git"))
# WHEN — act
exit_code = run_entrypoint(payload, clone_handler=mock_clone)
# THEN — assert
assert exit_code == 1
mock_runner.assert_not_called()
mock_on_error.assert_called_once()
```
### Parameterised Tests from Spec Enumerations
When a spec lists multiple valid values, use `@pytest.mark.parametrize`:
```python
# From spec: task states are pending, assigned, running, succeeded, failed, timed_out, cancelled
@pytest.mark.parametrize("terminal_state", ["succeeded", "failed", "timed_out", "cancelled"])
def test_cp_terminal_state_cannot_be_overwritten(terminal_state):
"""CP: Terminal states reject further transitions with 409."""
```
## Systematic Edge Case Discovery
~80% of bugs cluster at boundaries. Use these techniques to find edge cases systematically rather than by intuition.
### Boundary Value Analysis
For every input parameter, test at the edges of its valid range:
| Input type | Test values |
|---|---|
| Integer (range 1-100) | 0, 1, 2, 99, 100, 101, -1, MAX_INT |
| String | `""`, `"a"`, max-length string, max+1, unicode (`"\u0000"`, emoji), whitespace-only |
| List/Array | `[]`, `[single]`, many items, duplicates, `None` |
| Dict/Map | `{}`, missing required keys, extra unknown keys, `None` values |
| Timeout (seconds) | 0, 1, -1, very large (999999), `None`/missing |
| Base64 | valid, invalid chars, empty, padding variants (`=`, `==`, none) |
### Equivalence Partitioning
Group inputs into classes where all members should behave identically. Test one from each class:
```python
# Payload validation: prompt field
# Class 1: valid string → accepted
# Class 2: empty string → rejected (spec says prompt is required)
# Class 3: missing key → rejected
# Class 4: wrong type (int, list, None) → rejected
# Class 5: very long string → accepted (no length limit in spec)
@pytest.mark.parametrize("prompt,should_pass", [
("Fix the bug", True), # Class 1: valid
("", False), # Class 2: empty
(None, False), # Class 3: missing/None
(42, False), # Class 4: wrong type
("x" * 100_000, True), # Class 5: long string
])
def test_p_prompt_validation(prompt, should_pass):
...
```
### State Transition Coverage
For state machines (task states, dispatcher states), test:
1. **Every valid transition:** `pending → assigned → running → succeeded`
2. **Every invalid transition:** `succeeded → running` (should be rejected)
3. **Initial state:** newly created tasks start in `pending`
4. **Terminal states:** `succeeded`, `failed`, `timed_out`, `cancelled` cannot transition further
5. **Re-entrant transitions:** same state → same state (should be idempotent or rejected, per spec)
```python
VALID_TRANSITIONS = [
("pending", "assigned"),
("assigned", "running"),
("running", "succeeded"),
("running", "failed"),
("running", "timed_out"),
("assigned", "cancelled"),
("running", "cancelled"),
]
INVALID_TRANSITIONS = [
("succeeded", "failed"),
("failed", "running"),
("cancelled", "pending"),
("timed_out", "running"),
]
@pytest.mark.parametrize("from_state,to_state", VALID_TRANSITIONS)
def test_valid_state_transition(from_state, to_state):
...
@pytest.mark.parametrize("from_state,to_state", INVALID_TRANSITIONS)
def test_invalid_state_transition_rejected(from_state, to_state):
...
```
### The Edge Case Checklist
Walk through this for every function under test:
1. **Empty/null inputs** — what happens when required fields are missing?
2. **Boundary values** — min, max, zero, negative, off-by-one
3. **Type mismatches** — string where int expected, list where dict expected
4. **Malformed input** — invalid JSON, bad base64, truncated data
5. **Concurrent operations** — two tasks claiming the same resource
6. **Ordering** — actions that depend on sequence (pre-action before runner)
7. **Idempotency** — calling the same operation twice (kill an already-killed container)
8. **Resource exhaustion** — at capacity, disk full, timeout expired
9. **Partial failure** — first action succeeds, second fails (cleanup?)
## Property-Based Testing with Hypothesis
Instead of specifying individual test cases, define *properties* that must hold for all inputs. Hypothesis generates hundreds of inputs including edge cases you'd never think of.
### When to Use Property-Based Testing
- **Serialisation roundtrips:** encode → decode returns original
- **Parsers:** should never crash on any input
- **Data transformations:** invariants that hold regardless of input
- **Validators:** valid inputs accepted, invalid inputs rejected (never crash)
### When NOT to Use It
- Tests where generating valid inputs is harder than the code itself
- Tests where the "property" just restates the implementation
- UI or integration tests
### Patterns
```python
from hypothesis import given, strategies as st, assume, settings
from hypothesis import example
# Roundtrip: base64 encode/decode preserves payload
@given(st.text())
def test_base64_roundtrip(payload_str):
encoded = base64.b64encode(payload_str.encode()).decode()
decoded = base64.b64decode(encoded).decode()
assert decoded == payload_str
# Invariant: payload validation never crashes (may reject, never exception)
@given(st.dictionaries(st.text(), st.text() | st.integers() | st.none()))
def test_payload_validation_never_crashes(raw_payload):
# Should return True/False or raise ValidationError — never unhandled exception
try:
validate_payload(raw_payload)
except ValidationError:
pass # Expected for invalid input
# Pin known edge cases alongside random generation
@example("") # empty string
@example("\x00") # null byte
@example("a" * 10**6) # very long
@given(st.text())
def test_prompt_handling(prompt):
...
# Composite strategies for domain objects
@st.composite
def valid_payloads(draw):
return {
"task_id": draw(st.uuids()).hex,
"prompt": draw(st.text(min_size=1)),
"runtime": {"cli": draw(st.sampled_from(["claude", "codex"]))},
}
@given(valid_payloads())
def test_valid_payload_always_accepted(payload):
assert validate_payload(payload) is True
```
### Stateful Testing for State Machines
Hypothesis can generate sequences of operations and check invariants after each step:
```python
from hypothesis.stateful import RuleBasedStateMachine, rule, precondition
class TaskStateMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.task = Task(state="pending")
@rule()
@precondition(lambda self: self.task.state == "pending")
def assign(self):
self.task.transition("assigned")
assert self.task.state == "assigned"
@rule()
@precondition(lambda self: self.task.state == "running")
def complete(self):
self.task.transition("succeeded")
assert self.task.state == "succeeded"
# Invariant: terminal states never change
@invariant()
def terminal_states_are_final(self):
if self.task.state in ("succeeded", "failed", "cancelled"):
with pytest.raises(InvalidTransition):
self.task.transition("running")
TestTaskStates = TaskStateMachine.TestCase
```
## Mutation Testing
Mutation testing answers: "If someone introduced a bug, would our tests catch it?"
Tools make small code changes (replacing `>` with `>=`, `True` with `False`, deleting statements) and check if tests still pass. Surviving mutants = test gaps.
### Setup with mutmut
```toml
# pyproject.toml
[tool.mutmut]
paths_to_mutate = "entrypoint/"
tests_dir = "tests/"
runner = "python -m pytest tests/ -x -q"
```
```bash
# Run mutation testing
mutmut run
# See surviving mutants
mutmut results
# Inspect a specific mutant
mutmut show 42
```
### Practical Guidance
- **Target: mutation score above 80%.** Scores above 90% have diminishing returns (equivalent mutants).
- **Focus on business logic** — validators, state machines, parsers. Skip glue code.
- **Use mutation testing to audit AI-generated tests.** This is the most powerful combination: AI writes tests from spec, mutation testing verifies those tests catch real faults.
- **Run on changed files only in CI** (full suite is slow). Full run nightly or pre-release.
## Test Architecture
### The Testing Pyramid for Spec-Driven Projects
| Layer | Proportion | Speed | What it catches |
|---|---|---|---|
| Unit tests | 60-70% | <1ms each | Logic errors, boundary violations, state machine bugs |
| Property-based | 10-15% | ~10ms each | Edge cases humans miss, roundtrip failures, crash inputs |
| Integration | 15-20% | ~100ms each | Component interaction bugs, mock/reality divergence |
| E2E / acceptance | 5-10% | ~1s+ each | Full-chain failures, deployment config issues |
### Test Isolation Principles
- **No test depends on another test's state.** Each test sets up its own preconditions.
- **No test depends on execution order.** `pytest-randomly` catches order dependencies.
- **No test touches the real filesystem outside `tmp_path`.** Monkeypatch paths that default to production locations (like `/workspace`).
- **No test makes network calls.** Mock HTTP, subprocess, and socket calls.
- **Integration tests are marked** (`@pytest.mark.integration`) and excluded by default.
### Fixture Architecture
```python
# conftest.py — shared fixtures, not test logic
@pytest.fixture
def minimal_payload():
"""Smallest valid payload — tests shouldn't need more unless testing specific fields."""
return {"task_id": "test-123", "prompt": "do something", "runtime": {"cli": "claude"}}
@pytest.fixture
def encode_payload():
"""Helper: dict → base64 string (how the dispatcher passes payloads)."""
def _encode(d):
return base64.b64encode(json.dumps(d).encode()).decode()
return _encode
# Per-module conftest for module-specific fixtures
# tests/test_dispatcher/conftest.py
@pytest.fixture
def mock_backend():
"""Fake container backend that records calls without Docker."""
...
```
### Negative Tests Are as Important as Positive Tests
For every "this works" test, write at least one "this fails correctly" test:
```python
# Positive: valid payload accepted
def test_p1_valid_payload_loads():
...
# Negative: missing required field rejected
def test_p3_missing_prompt_raises():
...
# Negative: wrong type rejected
def test_p_prompt_wrong_type_raises():
...
# Negative: extra unknown fields are ignored (not rejected)
def test_p_unknown_fields_ignored():
...
```
## AI Agent Testing Patterns
### The Two-Phase Rule
**Never let the same agent write both tests and implementation in one pass.** An agent that writes tests and code together will unconsciously write tests that verify its own broken assumptions.
The workflow:
1. **Phase 1:** Agent reads spec → writes tests. Human reviews tests against spec.
2. **Phase 2:** Agent (or different agent) reads spec + tests → writes implementation until tests pass.
### Hidden Test Splits
Hold back some tests that the implementing agent never sees. Use them as a final validation:
```python
# tests/test_payload.py — agent sees these during development
def test_p1_load_from_env_var(): ...
def test_p2_missing_payload_exits_1(): ...
# tests/test_payload_hidden.py — agent never sees these, run post-implementation
# (Marked with a custom marker, excluded from default run)
@pytest.mark.hidden
def test_p1_load_from_file_fallback(): ...
@pytest.mark.hidden
def test_p_concurrent_payload_loads(): ...
```
### Regression Tests from Real Bugs
Every bug found in production or during integration testing becomes a permanent test case:
```python
def test_regression_crlf_corruption():
"""Regression: smtp-oauth-relay converted \\r\\n to \\n, breaking quoted-printable.
Fixed by as_bytes(policy=email_policy.SMTP). See memory/gotchas-email-relay.md."""
...
```
These are the highest-value tests because they catch proven failure modes.
## Test Quality Metrics
### What to Measure
| Metric | Target | Why |
|---|---|---|
| Spec coverage | 100% | Every numbered requirement has at least one test |
| Mutation score | >80% | Tests catch real faults, not just inflate coverage |
| Line coverage | >90% | Baseline hygiene (necessary but not sufficient) |
| Test speed | <10s total | Fast enough for pre-commit hooks |
| Assertion density | >1 per test | Tests that don't assert don't catch anything |
### What NOT to Measure
- **100% line coverage as a goal.** Chasing 100% leads to tests that exercise code paths without meaningful assertions.
- **Test count.** 50 well-targeted tests beat 200 shallow ones.
- **Test-to-code ratio.** The ratio depends on the module's complexity, not a universal number.
## CI Integration
### Pre-commit (Every Commit)
```bash
pytest tests/ -x -q --tb=short -m "not integration"
```
### PR Validation (Every Push)
```bash
# Unit + property-based tests
pytest tests/ -q --tb=short -m "not integration"
# Mutation testing on changed files only
mutmut run --paths-to-mutate="$(git diff --name-only main... | grep '.py$' | tr '\n' ',')"
```
### Nightly
```bash
# Full mutation testing
mutmut run
# Integration tests (requires Docker)
pytest tests/ -m integration
# Hidden test validation
pytest tests/ -m hidden
```
## Python Testing Gotchas
### `subprocess.run(check=True)` Is Invisible to Mocks
When you mock `subprocess.run`, the mock replaces the entire function — including the `check=True` logic that raises `CalledProcessError`. A mock returning `CompletedProcess(returncode=1)` won't trigger the exception even though the real code uses `check=True`. To test failure paths, use `side_effect=CalledProcessError(...)` explicitly.
### Use Routing Callables for Multi-Call Subprocess Mocks
When a function calls `subprocess.run` multiple times (e.g., git config, add, diff, commit, push), a fixed `side_effect` list is fragile and breaks when call order changes. Instead, use a routing callable that inspects the command: `mock_run.side_effect = lambda cmd, **kw: route_by_command(cmd)`. Clearer, more maintainable, and self-documenting.
### Pydantic v2 `@field_validator` Doesn't Fire for Default Values
`@field_validator('field_name')` never runs when the field takes its default value (e.g., `None`). Cross-field validation logic (e.g., "if type is X then field Y is required") silently passes when the dependent field is omitted. Use `@model_validator(mode='after')` for any validation that depends on multiple fields or needs to fire even when fields take defaults.
### Never `sys.exit()` at Module Level
`sys.exit()` in an `except ImportError` block at module level kills pytest collection entirely — all tests fail, not just the ones for that module. Use a flag pattern instead: `_HAS_DEPENDENCY = False` in the except block, then check `if not _HAS_DEPENDENCY: return 1` inside the function. This allows the module to be imported and mocked even when the optional dependency is missing.
## Anti-Patterns
### Tests that mirror implementation
**Symptom:** Test asserts that function calls happen in a specific order, using mock.assert_has_calls with exact sequences. Breaks on any refactor.
**Fix:** Test behaviour (inputs → outputs), not implementation details.
### Tests without assertions
**Symptom:** `test_it_runs()` calls the function and checks it doesn't crash. No assertion on the result.
**Fix:** Every test must assert something specific about the output, side effects, or raised exceptions.
### Overmocking
**Symptom:** Every dependency is mocked. Tests pass but integration fails because mocks don't match real behaviour.
**Fix:** Mock at the boundary (subprocess, HTTP, filesystem), not between your own modules. Use real objects for internal dependencies.
### Fragile tests
**Symptom:** Tests break when unrelated code changes. Usually caused by asserting on implementation details, shared mutable state, or execution order.
**Fix:** Test the public interface. Use fixtures for setup. Isolate each test completely.
### Testing private methods
**Symptom:** Tests import `_internal_helper` and test it directly. These break on any refactor.
**Fix:** Test through the public API. If a private method is complex enough to need its own tests, it should probably be a separate module with a public interface.

View File

@@ -1,60 +0,0 @@
# Validation & Deployment
## Validate Locally, Deploy Once
The single biggest time sink across projects is "deploy first, validate later." Real-world stats from a 9-milestone infrastructure project showed 50-60% of commits were fixes that could have been caught locally.
**Always validate before pushing:**
- `helm template` for Helm chart values
- `kustomize build` (or `kubectl kustomize`) for Kustomize apps
- `kubectl apply --dry-run=server` for K8s naming/schema issues
- `docker run <app> validate-configuration` for apps that support it (Authelia, Homepage, etc.)
- `docker inspect` for unfamiliar container images before writing init containers
- Lint/typecheck/test for application code
**Batch fixes locally, push once.** Each push-sync-crash-fix cycle wastes minutes and clutters Git history.
## Test the Full Chain Immediately
After wiring up any new service or endpoint, test end-to-end from the user's perspective right away. Don't assume intermediate steps working means the whole chain works.
- `curl --resolve domain:443:<ip> https://domain` to test bypassing DNS/proxy layers
- Test from the actual consumer (not same-namespace test pods for network policies)
- Test DNS resolution after deploying FQDN-based policies
## Pre-Flight Checks
Before starting a deploy or automation phase:
- Verify SSH keys are loaded (`ssh -T git@<host>`)
- Confirm environment variables and credentials are available
- Check that the target environment is in the expected state
- Verify DNS records resolve as expected
## Scripts That Change Config Must Self-Verify
After updating and restarting a service, the script should test that the change actually took effect (e.g., curl an API endpoint, check a config value). A "success" message without verification hides failures.
## Check Container Image Runtime Requirements First
Before writing deployment manifests (StatefulSets, Deployments, init containers), check the image's runtime expectations: UID it runs as, writable directories it needs, filesystem layout. Use `docker inspect` or image documentation.
Modern images often run as non-root with specific writable directory requirements that aren't obvious from docs alone. Discovering these at deploy time wastes an entire push-crash-fix cycle per missed requirement.
## Verify Counts and Summaries Mechanically
After editing specification or documentation files that include summary counts (e.g., "14 requirements"), verify them with `grep` or `wc` rather than counting manually. Manual counting of dozens of items is error-prone and produces incorrect summaries that erode trust in the documentation.
## Test Pre-Commit Hooks Manually After Adding Dependencies
Run `bash .githooks/pre-commit` (or your hook path) manually after adding new dependencies or changing test imports. Hidden virtual environments (`.venv/`) that the hook discovers before system Python can cause `ModuleNotFoundError` at commit time even though tests pass from the terminal. Discovering hook failures during a real commit wastes debugging effort on environment issues rather than code issues. After adding a dependency, check all Python environments: `find . -name "activate" -o -name "pytest"` to discover venvs, and install into each.
## Order Multi-Step Migrations Carefully
When performing multi-step changes on remote systems (port changes, firewall rules, service migrations), plan explicit ordering to avoid lockout:
1. Open the new path first (new port, new firewall rule)
2. Migrate the service to use the new path
3. Add redirects or backward-compatibility rules
4. Remove the old path
Doing all steps at once risks losing access if any step fails. Plan the ordering upfront, not mid-deploy.

View File

@@ -0,0 +1,32 @@
# Agent Dispatch Pre-flight
## When this applies
Before starting `/loop ... /orchestrate`, `/dispatch`, or any unattended or overnight agent batch. Run this checklist once, before the loop starts — not mid-run.
## Checklist — all must pass before the loop starts
1. **CP reachable.** `curl -fsS "$CP_URL/health"` returns 200. `CP_URL` is defined in `claude/agent-runtimes-cp.md` — read that file for the correct value and any auth headers required.
2. **A dispatcher is polling.** Check the CP dispatchers endpoint (see `agent-runtimes-cp.md`). A queued task with no live dispatcher sits forever — confirm at least one dispatcher is active before queuing work.
3. **Scaffolding present.**
- `.agent-tasks.json` exists and passes `jq empty .agent-tasks.json` (valid JSON).
- All templates, repos, and input files referenced by tasks are resolvable from this host.
- Agent push keys are loaded (`ssh-add -l`) — container agents push branches back; a missing key silently fails the push.
4. **Auth done this session.** `CLAUDE_CODE_OAUTH_TOKEN` is set in the environment or readable from `~/dev/claude/secrets/claude/long_lived_oauth_token`. Confirm before launching containers.
Do not start the loop until all four pass. Surface any failure to the user and wait for remediation.
## Circuit-breaker
Track task-state changes across invocations. After **5 consecutive invocations with no state change** (no task moved from pending→running, running→completed/failed, etc.): stop dispatching, print a stuck-queue report listing each task and its current status, and wait for the user. Never run an unattended loop without this guard.
## Recovery
If a dispatch batch needs to be aborted mid-run:
- Drain queued tasks: set pending tasks to `cancelled` in `.agent-tasks.json` before the next invocation so the orchestrator does not launch them.
- Let running containers finish or `docker stop` them explicitly.
- Re-dispatch from a clean state once the root cause is resolved.

View File

@@ -0,0 +1,64 @@
# Agent Runtimes Control Plane
## Which CP to use
**Always use the hosted CP (`agents.oreillyit.nz`) for real agent work.** Localhost is for testing only.
| Use case | CP to use |
|---|---|
| Real implementation tasks, spec work, any multi-step agent work | `https://agents.oreillyit.nz/api` |
| Smoke-testing a new task payload format, debugging CP behaviour locally | `http://localhost:8100` |
Reason: workloads on the hosted CP are not coupled to this laptop's uptime. If the laptop sleeps or is closed, tasks on localhost stall or die. Tasks on the hosted CP keep running.
Since the M11 dashboard deployment, Traefik routes `/` to the React dashboard and `/api/*` to the CP API. Always use `https://agents.oreillyit.nz/api` as the CP_URL — not the bare hostname.
```bash
export CP_URL=https://agents.oreillyit.nz/api
scripts/dispatch-task --login ... # first time per session — opens browser for OIDC
scripts/agent-monitor --login --filter "project=<project-name>" --filter "age<1h"
```
## Task dispatch — always use a template or workflow
**Never submit tasks via raw `curl` without explicit user approval.** Raw curl bypasses:
- Harness selection (agents start in empty containers with no credentials or context)
- `pre_actions` clone (agents have no repo to work on)
- `agent_repo` persistence (work is lost when the container exits)
- `requires_tags` validation (tasks may be picked up by incompatible dispatchers)
Default approach — always one of:
1. `scripts/dispatch-task --template <name>` for single implementation tasks
2. `scripts/dispatch-workflow` for multi-node DAG workflows
3. `/manual-workflow` skill for interactive workflows with human review gates
## Template selection
| Work type | Template |
|---|---|
| Backend/CP implementation (migrations, APIs) | `opus-code-repo` |
| Frontend implementation | `sonnet-code-repo` |
| Planning / architecture | `opus-planning` |
| Security review | `opus-security-review` |
| Spec writing | `opus-spec-writer` |
| Test writing | `opus-test-writer` |
Standard params for all code templates:
```bash
--template-param repo_url=git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git \
--template-param agent_repo_url=git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes-agents.git
```
If raw curl is genuinely needed (e.g., testing a new payload field), state the reason and get explicit user confirmation before submitting.
## Monitoring
Recommend the user run **agent-monitor** in a separate terminal:
```bash
scripts/agent-monitor --login --filter "project=<project-name>" --filter "age<1h"
```
Adjust `age` to suit the session — `1h` is a good default for hosted CP work since tasks persist across laptop sleep.
For full control plane usage (submitting tasks, checking logs, cancelling, common workflows): **`~/dev/claude/projects/agent-runtimes/readme/control-plane-operations.md`**.

View File

@@ -0,0 +1,27 @@
# Context Offloading with `/ask-minimax`
`/ask-minimax` delegates a task to a MiniMax M3 sub-session that reads and writes files directly. Only a short summary flows back — file contents never load into this session. Reach for it when the **file payload dwarfs the answer payload**.
## Use it for
- Summarising large files (logs, dumps, generated reports >500 lines)
- Extracting specific facts from multiple files or long reference docs
- Generating big files (migrations, config bundles, fixtures, large docs) where the content does not need to flow back
- Format conversion of large files (CSV↔JSON, XML↔YAML, etc.)
- Bulk find-and-extract across many files where only the matches matter
## Skip it for
- Iterative design or debugging — the main session needs the content in context
- Small files (under a few hundred lines) — overhead exceeds savings
- Tasks where you will immediately re-read the result to act on it
- Anything requiring tools MiniMax cannot use (web fetch, MCP, browser, agent dispatch)
- Architecture or quality-sensitive output — MiniMax is for grunt work, not nuanced reasoning
## How to delegate
Pass file *paths*, not file *contents*. If you read the input files yourself before invoking, you have already paid the token cost the skill exists to avoid.
## Failure handling
MiniMax intermittently returns `invalid system role` and other transient errors. Retry once. If it fails again, fall back to a Claude subagent with the same brief — Sonnet for file-heavy grunt work, Opus for reasoning-grade consults. Never silently drop the task. Log new error shapes to the project's `memory/gotchas-*.md`.

View File

@@ -0,0 +1,75 @@
# Documentation Standards
Every project maintains standard markdown files. Index files (MEMORY.md, CONTEXT.md, BESTPRACTICES.md, SPEC.md, CLAUDE.md at root) are **thin indexes** pointing to detail files in subdirectories — not large documents themselves.
## CLAUDE.md (per-project)
The primary reference for Claude sessions. Should contain:
- Project overview and architecture
- Repository structure (keep updated as the project evolves)
- Key design decisions with rationale
- Conventions and coding standards
- Environment details (IPs, URLs, credentials references)
- Common operations / how-to recipes
For long projects, follow the same thin-index pattern as MEMORY.md — keep CLAUDE.md as the always-on guardrails + a "when to read which detail file" pointer table, and put per-subsystem conventions in `claude/<topic>.md`.
## MEMORY.md (Tiered Memory System)
**MEMORY.md** is a **thin index only** — one-line descriptions with links to topic files in `memory/`. No content lives in MEMORY.md itself.
**memory/** contains the actual content, split by topic:
- `memory/project-status.md` — current milestone, what's next, blockers
- `memory/network.md` — IPs, VIPs, subnets, topology
- `memory/gotchas-<topic>.md` — gotchas grouped by technology (`gotchas-cilium.md`, `gotchas-authelia.md`)
- `memory/process-lessons.md` — how-to-work-with-this-repo lessons for Claude
- `memory/m<N>-reflection.md` — one file per milestone reflection (time-bound, per-file is natural)
- `memory/decisions.md` — architecture and design decisions made during planning
Principles:
- **Split by topic, not by time.** A Cilium gotcha belongs in `gotchas-cilium.md` whether discovered in M5 or M8.
- **Milestone reflections are the exception** — inherently time-bound, one file per milestone.
- **Index descriptions matter.** "Cilium L2/LB gotchas and externalTrafficPolicy quirks" beats "cluster stuff".
- **Prune aggressively.** If a gotcha was fixed (e.g., chart upgraded past the bug), delete it. Stale memory is worse than no memory.
- **Each memory file should be self-contained and greppable.**
- **Deduplicate with CLAUDE.md.** Stable conventions live in CLAUDE.md. Memory holds learnings, gotchas, reflections. If something in memory has graduated to a stable convention, move it to CLAUDE.md and remove it from memory.
When reading memory at session start: read MEMORY.md (the index), then selectively read topic files relevant to the current task. Don't read all memory files unless doing a broad review.
When writing memory after a milestone: create the reflection file, update affected topic files, update the index.
## CONTEXT.md (Active Work Focus)
Thin index — one-line descriptions with links to detail files in `context/`. Answers "what should I focus on right now?"
**context/** contains one file per active work stream: background, status, what to test, future direction.
Principles:
- **Keep it current.** Remove entries when work is complete. CONTEXT.md reflects what's actively in progress, not history.
- **Link, don't inline.** Index stays small so `context-load` doesn't bloat the system prompt.
- **Orient agents.** CONTEXT.md is the primary mechanism for pointing independent agents (including container agents) at the right work.
- **Complement, don't duplicate.** CLAUDE.md = stable conventions. MEMORY.md = accumulated learnings. CONTEXT.md = *current* focus.
## BESTPRACTICES.md (Best Practices Index)
Index of generalised best practices extracted from real project work via `/distill-best-practices`. Only exists in `claude-foundations` — other projects inherit it via `context-load` walking up the directory hierarchy.
Thin index pointing to topic files in `best-practices/` (e.g., `kubernetes.md`, `helm.md`). Read only files relevant to the current project's stack.
`/distill-best-practices` maintains both the topic files and the index.
## FUTURE.md
Backlog of improvement ideas, each with:
- **Problem:** what's painful or manual today
- **Idea:** what the improvement looks like
- **Open questions:** unknowns to research before starting
- **Depends on:** other items or milestones that should come first
## README.md
Human-readable project documentation:
- Architecture summary
- Quick start / setup instructions
- Milestone table with status
- Scripts section listing every script with purpose and usage

96
claude/impeccable.md Normal file
View File

@@ -0,0 +1,96 @@
# Impeccable — design skill: install, maintenance, usage
[Impeccable](https://impeccable.style/) (GitHub: `pbakaus/impeccable`, Apache 2.0) is a
design vocabulary system for AI agents: 23 design commands, a per-project DESIGN.md
contract, and a deterministic 46-rule anti-slop detector.
## How it is installed here (non-standard — do not "fix")
Two skills in the **user profile** (`~/.claude/skills/`) **and mirrored into every
`~/.claude-*` profile's `skills/` dir** (octopus, octopus-anthropic, oreillyit,
oreillyit-anthropic, oreillyit-minimax — not `-secrets`), deliberately arranged so
the full skill never auto-fires:
| Path | What | Key frontmatter |
|---|---|---|
| `~/.claude/skills/impeccable/` | Full skill (SKILL.md + `reference/` + `scripts/`), copied from repo `.claude/skills/impeccable` | `disable-model-invocation: true` **(added locally — not upstream)** |
| `~/.claude/skills/impeccable-hint/` | Hand-written ~40-line shim carrying Impeccable's original auto-trigger description | `user-invocable: false` |
Rationale: the upstream description is engineered to auto-trigger on almost any
frontend vocabulary, and a full invocation loads ~1020k tokens (SKILL.md body +
mandatory reference reads). The shim auto-fires instead and merely *suggests*
`/impeccable <command>` to the user (once per session); only the user invokes the
real skill. Idle cost ≈ one description in the skill listing; hint invocation ≈ a few
hundred tokens.
Deliberately **not** installed globally:
- the detector **hook** (`settings.json``hook.mjs`) — false-positive-prone on Hugo
Go-template source (flags `src="{{ … }}"` as broken-image, em-dashes in comments)
- the `impeccable-manual-edit-applier` **agent** — only Live mode uses it, and Live
mode overlaps hugo-accelerator's editorial widget + branch previews
## Updating to a new upstream version
`npx impeccable install` / the plugin marketplace will NOT maintain this arrangement.
Update manually:
```bash
cd "$(mktemp -d)"
curl -sL https://github.com/pbakaus/impeccable/archive/refs/heads/main.tar.gz | tar xz
rm -rf ~/.claude/skills/impeccable
cp -r impeccable-main/.claude/skills/impeccable ~/.claude/skills/
```
After re-applying the frontmatter edit below, mirror both skill dirs into every
profile:
```bash
for p in ~/.claude-octopus ~/.claude-octopus-anthropic ~/.claude-oreillyit \
~/.claude-oreillyit-anthropic ~/.claude-oreillyit-minimax; do
rm -rf "$p/skills/impeccable" "$p/skills/impeccable-hint"
cp -r ~/.claude/skills/impeccable ~/.claude/skills/impeccable-hint "$p/skills/"
done
```
Then **re-apply the local edit** — add below `user-invocable: true` in
`~/.claude/skills/impeccable/SKILL.md`:
```yaml
disable-model-invocation: true
```
If upstream's `description:` changed, copy the new text into
`~/.claude/skills/impeccable-hint/SKILL.md` so the shim's trigger surface stays in
sync. If upstream added/renamed commands, refresh the shim's command table.
Installed version: check `version:` in `~/.claude/skills/impeccable/SKILL.md`
(3.9.1 as of 2026-07-22).
## Detector (no skill involvement, plain npx)
```bash
npx impeccable detect <path|url> [--json] # 46 deterministic rules, no LLM
npx impeccable ignores add-value overused-font Inter --reason "Brand font"
```
- **Output streams:** the human-readable report goes to **stderr**; `--json` goes to
stdout. In CI, capture with `2>&1 | grep -v 'npm WARN'` — a bare `2>/dev/null`
silently discards the entire text report.
- Run against **rendered output** (`public/` after a Hugo build), never template
source — raw Go templates false-positive.
- Staging URLs are Authelia-gated → the detector would score the login page; use a
local build or port-forward.
- Needs Node ≥ 22.12 per engines; observed working on Node 18 with EBADENGINE
warnings only. Pin the version in any CI use (no `@latest`).
- Ignores/config live in `.impeccable/config.json` per repo — a template-sync surface
if adopted fleet-wide (hugo-accelerator `template_consumers.json`).
## Planned integrations (recommendations, 2026-07-22)
- **hugo-ci (M13):** `npx impeccable detect public/ --json` post-render, advisory
first, gate later.
- **cms-proxy AI edit / M6 drafting:** per-customer DESIGN.md (bootstrap interview or
`/impeccable document`) injected into the AI edit prompt alongside component schemas.
- **agent-runtimes:** detector as post-task verify step for frontend tasks; full skill
baked only into design-scoped harness templates (Node 22+ images).

View File

@@ -0,0 +1,23 @@
# Knowledge Distillation Pipeline
Three skills form a continuous learning pipeline across projects:
1. **`/log`** — Run at end of session. Captures decisions, gotchas, open questions to `memory/log/YYYY-MM-DD.<HHMMSS>.md` in the current project. Also prunes old reflected logs.
2. **`/reflect-logs`** — Run periodically. Processes unprocessed session logs into topic memory files (`memory/gotchas-*.md`, `memory/process-lessons.md`, etc.). Flags stale entries. Tracks state in `.reflection-state.json`.
3. **`/distill-best-practices`** — Run from any project. Reads changed memory files across all tracked projects and proposes updates to `claude-foundations/best-practices/`. Tracks state in `best-practices/.distill-state.json`.
## State files
- **`.reflection-state.json`** — per-project, tracks which logs have been reflected on (md5 hashes of log content)
- **`best-practices/.distill-state.json`** — in claude-foundations, tracks git SHAs per project at time of last distillation
- **`settings.yaml`** — in claude-foundations, configures log retention (default 7 days), max logs per reflection run, tracked project list
## Log format
Session logs use structured markdown with parseable section headers: Summary, Decisions, Gotchas Discovered (tagged with `[topic]` for routing), Open Questions, Key Context, Process Notes. Empty sections are omitted.
## Pruning
- Reflected logs older than `log.retention_days` (default: 7) are automatically deleted by `/log`
- Unreflected logs older than `log.warn_unreflected_days` (default: 14) trigger a warning instead of deletion
- `/reflect-logs` flags stale memory entries (version-specific bugs that have been fixed, manual processes that have been automated)

17
claude/milestones.md Normal file
View File

@@ -0,0 +1,17 @@
# Milestones
Break projects into numbered milestones (M1, M2, ...). Every milestone completion MUST include:
1. **Verification script** (`scripts/verify-m<N>.sh`) — automated checks confirming all milestone outcomes. Idempotent, non-destructive, returns non-zero on failure. Colour output (green/red) for pass/fail.
2. **Milestone reflection** in `memory/m<N>-reflection.md` — review the **entire conversation** from the start of the milestone (not just the final state) and capture:
- **Process improvements:** what slowed us down, wrong assumptions, backtracking, what could be automated. What would make this milestone faster if we redid it from scratch?
- **Key knowledge for reproduction:** critical facts, gotchas, non-obvious config details that someone (or a future Claude session) would need to recreate this milestone reliably. Include version-specific quirks, network/subnet constraints, debugging detours.
- **Scripts and automation:** existing scripts that proved valuable, new scripts that would condense multi-step manual processes, patterns that could be extracted. Look at the conversation history for command sequences run repeatedly — these are scripting candidates.
- **Future improvement ideas:** things that surfaced but don't belong in current scope. Add to `FUTURE.md` with the standard Problem/Idea/Open questions/Depends on format.
3. **Updated README.md** — scripts section, milestone table, any new setup steps.
4. **Updated CLAUDE.md** — repo structure, conventions, new patterns discovered. (After the split: this may mean updating a `claude/<topic>.md` detail file rather than the root CLAUDE.md.)
5. **Update affected topic files in `memory/`** (new gotchas, updated status) and the MEMORY.md index.

22
claude/new-projects.md Normal file
View File

@@ -0,0 +1,22 @@
# New Projects
When the user picks "New project!" from the session-start menu, or otherwise asks to start a new project:
1. Create a new directory under `~/dev/claude/<project-name>/`
2. **Ask the user** which Gitea user/org the repo should be created under (e.g., `homelab`, `skynet`, `oreillyit`, a personal user) before setting up the remote
3. Create the initial standard files:
- **ABOUT.md** — One-sentence project description (see `documentation-standards.md`). Picked up by `context-load` for the session-start menu.
- **CLAUDE.md** — Project-specific architecture, conventions, repo structure, and working instructions for Claude
- **MEMORY.md** — Thin index pointing to `memory/` topic files
- **FUTURE.md** — Backlog ideas (Problem/Idea/Open questions/Depends on format)
- **README.md** — Human-readable overview, quick start, milestones, scripts reference
4. Parse sibling project CLAUDE.md files in `~/dev/claude/` and bring over related practices, guidelines, and learnings that apply to the new project's stack
5. **For coding projects with multiple milestones:** Read `best-practices/spec-driven-development.md` and `best-practices/test-driven-development.md`. Create `SPEC.md` and `spec/` directory. Write specs before writing code — see `spec-driven-development.md`.
## ABOUT.md format
```markdown
description: One sentence describing what this project is
```
Every project **must** have an ABOUT.md. Keep the description under ~80 characters.

23
claude/plan-mode.md Normal file
View File

@@ -0,0 +1,23 @@
# Plan Mode
When working in plan mode (`permission_mode: plan`):
**Before calling `ExitPlanMode`**, always write the complete plan to a file in the project root:
- **Filename:** `[MILESTONE]-[PURPOSE]-PLAN.md` — e.g. `M2-auth-PLAN.md`, `M3-monitoring-PLAN.md`
- MILESTONE: the milestone identifier (e.g. `M2`) or a short label if not milestone-scoped (e.g. `initial`)
- PURPOSE: a short kebab-case description of what the plan covers
- **Contents:** the full plan as developed in the planning conversation — steps, decisions, rationale, open questions
- **Location:** project root (same directory as CLAUDE.md)
This file becomes the implementation reference for the session that follows plan mode.
## Keep the plan updated during implementation
After each phase or significant step, update PLAN.md:
- Mark the phase/step status as **Complete**
- Add key commits, references, or artifacts produced
- Document deviations from the original plan (what changed and why)
- Note verification results
This keeps the plan accurate as a living document — useful for resuming across sessions, reflecting on the milestone, and understanding what actually happened vs. what was planned.

View File

@@ -0,0 +1,12 @@
# Script & Skill Documentation
Every script in `claude-foundations` and every skill in `custom-claude-skills` must have a corresponding memory file in `claude-foundations/memory/`:
- **Scripts:** `memory/script-<name>.md` — purpose, usage, how it works, gotchas
- **Skills:** `memory/skill-<name>.md` — purpose, usage, how it works, gotchas, which projects use it
Each memory file should be self-contained and referenced from `claude-foundations/MEMORY.md` (the index). Future sessions can then understand what tooling exists without reading every script and SKILL.md from scratch.
**When creating a new script or skill:** create the memory file and update the MEMORY.md index as part of the same commit.
**When creating or editing a skill:** run `validate-skill <path/to/SKILL.md>` before committing. The validator catches known restriction violations that have repeatedly broken skills — `$VAR` in paths, `${VAR}` syntax, `$()` substitution, uncovered binaries in `allowed-tools`, and more. A skill must pass with zero errors before it is committed. Warnings should be reviewed but are acceptable.

View File

@@ -0,0 +1,19 @@
# Scripting Conventions
## Bash safety
- Open every script with `set -euo pipefail` — fail fast on errors, unset vars, and pipeline failures.
- `grep` exits 1 on no match — under `set -e` use `grep ... || true`; under `pipefail`, beware `grep ... | head` triggering SIGPIPE (use `|| true` on the grep side).
- One stdin per process — never pipe into a command that also reads a heredoc; pick one input source.
- Validate JSON before consuming: `jq empty <file>` or `python3 -m json.tool <file>`. Especially before dispatch loops that iterate over JSON state.
- Quote all expansions: `"$var"`, `"${array[@]}"`. Bare expansions split on whitespace.
- Edit-tool `replace_all` is a substring match — a short `old_string` like `2` will corrupt `24``244`. Make `old_string` unique (add surrounding context) or use individual targeted edits.
## Conventions
- All scripts live in `scripts/` and run from the repository root
- Scripts should be idempotent and safe to re-run
- Use colour output for pass/fail indicators in verification scripts
- Verification scripts should check for default/insecure credentials and print remediation instructions on failure
- Scripts should exit non-zero on failure so `&&` chains work naturally
- **Never hardcode secrets, tokens, or access keys in scripts.** Accept them via environment variables, stdin, or `@file` references. If a script needs a secret at runtime, read it from `~/dev/claude/secrets/` or accept it as a parameter — never embed it.

49
claude/source-control.md Normal file
View File

@@ -0,0 +1,49 @@
# Source Control
## Hosts and orgs
- All projects on **Gitea** (`gitea.oreillyit.nz`) as primary remote — prefer this hostname over `gitea.homelab.internal` (same instance, the public name enables external access)
- Migrate existing remotes from `gitea.homelab.internal` to `gitea.oreillyit.nz` when convenient
- **`skynet`** org: AI-focused projects. Owned by `ai_enablement`.
- **`homelab`** org: infrastructure projects (cluster-bootstrap, etc.). Owned by `cluster-administrator`.
- **`oreillyit`** org: non-AI internal O'Reilly IT tools. Owned by `ai_enablement`.
- Optionally push-mirror to GitHub for public visibility.
## SSH aliases
Pattern: `gitea.oreillyit.nz-<username>`.
- `gitea.oreillyit.nz-homelab` → authenticates as `cluster-administrator` (key: `~/.ssh/gitea-cluster-admin`)
- `gitea.oreillyit.nz-ai-enablement` → authenticates as `ai_enablement` (key: `~/.ssh/gitea.ai-enablement`)
- `gitea.oreillyit.nz-accelerators` → (key: `~/.ssh/gitea.accelerators.2026`)
Git remote URL format: `git@gitea.oreillyit.nz-<user>:<org>/<repo>.git`
- Example: `git@gitea.oreillyit.nz-ai-enablement:skynet/custom-claude-skills.git`
## SSH agent pre-flight
Before the first push of a session:
1. Derive the alias: `git remote get-url origin` — the host segment is the SSH alias.
2. Run `ssh-add -l` and read the **full output**. Keys are often pre-loaded — check before asking. Use `grep -i <keyname>` (case-insensitive) to find the relevant entry; a case-sensitive grep will miss keys whose comments use different capitalisation.
3. If the key is present, proceed. Only ask the user to `ssh-add ~/.ssh/<keyfile>` if it is genuinely absent. Claude cannot answer a passphrase prompt.
4. Confirm auth: `ssh -T git@<alias>` — expect a Gitea welcome message.
In non-interactive contexts (CI, container agents, unattended loops), use `GIT_SSH_COMMAND='ssh -o BatchMode=yes' git push` so a missing credential fails fast instead of hanging on a passphrase prompt.
## Push failure recovery
| Symptom | Likely cause |
|---|---|
| `Permission denied (publickey)` | Key not loaded, or wrong alias in remote URL |
| Push hangs silently | Passphrase prompt in non-interactive context — use `BatchMode=yes` |
| 403 after API-created repo | SSH user not added as collaborator — add via Gitea UI or API |
Commit locally first, surface the failure explicitly, and never end a session with finished work unpushed and unmentioned. Record the recovery step in CONTEXT.md.
## Working rules
- **Always pull before planning work** — run `git pull --ff-only` when entering a project. Work may have been pushed from another machine or by container agents. If the pull fails (diverged history, uncommitted changes), warn the user before proceeding.
- Meaningful commit messages; prefer small, focused commits over large batches.
- Enable pre-commit hooks where appropriate (secret detection, linting, formatting).
- Never commit secrets in plaintext — use SOPS + age or equivalent encryption.

View File

@@ -0,0 +1,37 @@
# Spec-Driven Development
**Any coding project with multiple milestones MUST have specs before code.** Hard requirement, not a suggestion. Workflow: Plan → Spec → Test → Code.
At project start or when starting a new milestone, always read:
- `best-practices/spec-driven-development.md` — spec structure, requirement numbering, scenarios, maintenance
- `best-practices/test-driven-development.md` — edge case discovery, property-based testing, AI agent testing patterns
## Required artifacts
Every multi-milestone coding project must have:
1. **`SPEC.md`** — Index file at the project root. Lists all spec files with a "when to read" column. Same thin-index pattern as MEMORY.md.
2. **`spec/` directory** — One spec file per subsystem. Each spec follows: Overview, Responsibilities, Dependencies, Data Model, Requirements (numbered), Scenarios (given/when/then).
3. **Numbered requirements** — Each spec uses a prefix (e.g., `IG-1` for ingestion, `DB-1` for database). Requirements must be independently testable and unambiguous.
4. **Test files that reference spec IDs** — every test function name includes its requirement ID: `test_ig3_dedup_by_message_ts`.
## Workflow
1. **Plan** — architecture decisions, milestone breakdown, technology choices (PLAN.md)
2. **Spec** — detailed contracts, data models, interfaces, requirements, scenarios (spec/)
3. **Test** — write tests from the spec before code exists. They should all fail.
4. **Code** — implement until tests pass. Minimum code to satisfy the spec.
5. **Update** — if implementation reveals spec issues, update spec → test → code in that order.
## When to write specs
- **Before M1 implementation begins** — write specs for all subsystems in M1's scope
- **Before each subsequent milestone** — specs for new subsystems, updates to existing specs for changes
- **Spec changes and test changes ship in the same commit**
- **Code changes that affect interfaces require spec changes in the same commit**
## What does NOT need a spec
- Infrastructure-only projects (Helm values, Kustomize manifests, Ansible playbooks) — declarative, not behavioural
- Single-script utilities — a well-commented script with a test is sufficient
- Documentation-only changes

28
claude/status-line.md Normal file
View File

@@ -0,0 +1,28 @@
# Status Line
A persistent bar at the bottom of Claude Code shows the current topic, model, and context usage: `[Model Name] topic | N% context`.
## Setting the topic
After the user selects a project or describes their task (i.e., after the first response where the status line has had a chance to run), set the topic:
```bash
~/.claude/status/set-topic.sh "$(pwd)" "project-name: brief task description"
```
Examples:
- `~/.claude/status/set-topic.sh "$(pwd)" "brainiac-app: M2 web frontend"`
- `~/.claude/status/set-topic.sh "$(pwd)" "cluster-bootstrap: Cilium upgrade"`
- `~/.claude/status/set-topic.sh "$(pwd)" "General chat"`
## When to update
- **Session start:** set the topic once the user picks a project or task
- **Focus change:** update if the user shifts to a different project or task mid-session
- **Keep it short:** aim for `project: task` format, under ~40 characters
## How it works
The status line script (`scripts/statusline.sh`) runs after each assistant message. It writes the session ID to `/tmp/claude-session-id-<md5 of cwd>`, which `set-topic.sh` reads to find the correct per-session topic file at `~/.claude/status/<session-id>/claude-topic.txt`.
Early calls are safe: if `set-topic.sh` is called before the status line has run (first message), the topic is queued to a pending file and automatically applied when the status line first runs after the next response.

View File

@@ -0,0 +1,28 @@
# Transcript Tracking System — Pending Verification
## Status
Built and committed (2026-04-13, commit `6fd0dac`). Pending first real-run verification.
## What was built
- `pre-compact-backup.sh` — fixed to derive transcript path from `session_id+cwd`; now registers each backup in `~/.claude/transcript-backups/tracking.json`
- `extract-transcripts.py` — manages tracking.json; modes: `--register`, `--list`, `--extract`, `--mark-all-processed`
- `list-transcripts-here.sh` — wrapper for `--list $(pwd)` (needed because SKILL.md bang commands reject `$()` substitution)
- `/log` skill (in custom-claude-skills) — spawns Sonnet subagent to read JSONL backups and write `HHMMSS-transcripts.md` companion log
## What to verify on first real run
When `/log` is run in a session where context compression fired at least once:
1. Check `~/.claude/transcript-backups/tracking.json` has an entry for the session with `processed: false`
2. Run `/log` — it should show unprocessed transcripts in the pre-gathered metadata
3. The Sonnet subagent should write `memory/log/YYYY-MM-DD.HHMMSS-transcripts.md` in the **project directory** (absolute path)
4. After `/log` completes, `tracking.json` entry should show `processed: true` with a `log_file` path
**Known risk:** The subagent prompt template asks it to write to `[PROJECT_ABS_PATH]/memory/log/...` — if it uses a relative path from the wrong cwd, the file lands in the wrong place. If that happens, update the subagent prompt in `/log` SKILL.md to include an explicit `cd [CWD]` instruction.
## Files
- Hook: `~/dev/claude/projects/claude-foundations/hooks/pre-compact-backup.sh`
- Scripts: `~/dev/claude/projects/claude-foundations/scripts/extract-transcripts.py` and `list-transcripts-here.sh`
- Symlinked to: `~/.claude/scripts/`
- Skill: `~/dev/claude/projects/custom-claude-skills/skills/log/SKILL.md`
- Tracking state: `~/.claude/transcript-backups/tracking.json`

View File

@@ -1,17 +1,36 @@
#!/usr/bin/env bash
# Pre-compact hook: save a copy of the session transcript before compaction.
# Claude Code pipes JSON to stdin with session_id, transcript_path, cwd, etc.
# Claude Code pipes JSON with session_id, cwd, hook_event_name (no transcript_path).
# Derives the transcript location from session_id + cwd.
# After saving, registers the backup in ~/.claude/transcript-backups/tracking.json.
set -euo pipefail
# Parse stdin JSON for transcript path and session ID
# Parse stdin JSON
INPUT="$(cat)"
TRANSCRIPT_PATH="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)"
SESSION_ID="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)"
TRIGGER="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('trigger','unknown'))" 2>/dev/null)"
CWD="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null)"
PROVIDED_PATH="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)"
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
echo "pre-compact-backup: no transcript found, skipping" >&2
if [ -z "$SESSION_ID" ]; then
echo "pre-compact-backup: missing session_id in hook input, skipping" >&2
exit 0
fi
# Try provided path first (older Claude Code versions), then derive from session_id + cwd
if [ -n "$PROVIDED_PATH" ] && [ -f "$PROVIDED_PATH" ]; then
TRANSCRIPT_PATH="$PROVIDED_PATH"
elif [ -n "$CWD" ]; then
# Derive transcript path: /home/paul/dev/claude -> -home-paul-dev-claude
PROJECT_DIR="$(echo "$CWD" | tr '/' '-')"
TRANSCRIPT_PATH="$HOME/.claude/projects/${PROJECT_DIR}/${SESSION_ID}.jsonl"
else
echo "pre-compact-backup: cannot locate transcript (no cwd or transcript_path), skipping" >&2
exit 0
fi
if [ ! -f "$TRANSCRIPT_PATH" ]; then
echo "pre-compact-backup: transcript not found at ${TRANSCRIPT_PATH}, skipping" >&2
exit 0
fi
@@ -20,11 +39,21 @@ BACKUP_DIR="$HOME/.claude/transcript-backups"
mkdir -p "$BACKUP_DIR"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}-${SESSION_ID:0:8}-${TRIGGER}.jsonl"
BACKUP_NAME="${TIMESTAMP}-${SESSION_ID:0:8}.jsonl"
BACKUP_FILE="${BACKUP_DIR}/${BACKUP_NAME}"
cp "$TRANSCRIPT_PATH" "$BACKUP_FILE"
# Prune backups older than 30 days
find "$BACKUP_DIR" -name "*.jsonl" -mtime +30 -delete 2>/dev/null || true
echo "pre-compact-backup: saved $(wc -l < "$BACKUP_FILE") lines to ${BACKUP_FILE##*/}" >&2
# Register backup in tracking.json using the register helper
python3 "$HOME/.claude/scripts/extract-transcripts.py" \
--register "$BACKUP_NAME" \
--session-id "$SESSION_ID" \
--cwd "$CWD" \
2>/dev/null \
&& TRACKED=" [tracked]" \
|| TRACKED=" [tracking failed]"
echo "pre-compact-backup: saved $(wc -l < "$BACKUP_FILE") lines to ${BACKUP_NAME} [project: ${CWD}]${TRACKED}" >&2

View File

@@ -10,6 +10,12 @@ if [ -z "$CWD" ]; then
exit 0 # Can't determine cwd, allow through
fi
# Projects can opt out by placing .claude/skip-plan-file-check in their root.
# Used when the project manages plans via an external system (e.g. a work-items CP).
if [ -f "$CWD/.claude/skip-plan-file-check" ]; then
exit 0
fi
if ls "$CWD"/*-PLAN.md 2>/dev/null | grep -q .; then
exit 0 # Plan file exists, allow exit
fi

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"last_run": "2026-04-19T09:51:20Z",
"processed": {
"log/2026-04-13.140716.md": "a2e7bfbbe892117df48526fb236ea367"
}
}

View File

@@ -68,6 +68,38 @@ Topic files at `~/.claude/status/<session-id>/claude-topic.txt`. Session ID uniq
Claude has no direct access to its own session ID. The status line script writes it to `/tmp/claude-session-id-<md5 of cwd>`, which `set-topic.sh` reads to find the correct per-session topic directory.
## Status line: Pending-topic queue for bootstrap race condition
`set-topic.sh` is called on the first message, before `statusline.sh` has written the session ID file. Instead of erroring, it writes to `/tmp/claude-pending-topic-<hash>`. `statusline.sh` picks this up on its next run via `mv` (atomic, no double-application). Simple and self-healing — statusline already runs after every response.
## Status line: Config must exist in every profile settings file
Each profile (`~/.claude`, `~/.claude-octopus`, `~/.claude-oreillyit`) has its own `settings.json` with no inheritance. The `statusLine` entry must be added to each independently.
Each profile (`~/.claude`, `~/.claude-octopus`, `~/.claude-oreillyit`) has its own `settings.json` with no inheritance. The `statusLine` entry and `Bash(~/.claude/status/set-topic.sh:*)` permission must be added to each independently.
## Best practices: One file per topic, not combined mega-documents
api-design.md and llm-code-security.md are separate files despite both being "security-adjacent". API design covers versioning, pagination, idempotency — not just security. Self-contained topic files are independently referenceable and can be loaded selectively by skills and agents.
## Review skills: Read-only output, not auto-edit
/review-plan and /review-spec are read-only (allowed-tools: Read, Glob, Grep). Review output informs the human rather than auto-editing plans/specs. This avoids unintended changes and reduces permission prompts. Both skills load all 5 design/security best practice files upfront — better to over-load context than miss a relevant check.
## Transcript backups: Sonnet (not Haiku) for /log transcript-analysis subagent
Gotcha detection requires judgment about backtracking and failed attempts mid-session. Haiku tends to see only the final outcome and miss the failure mode that led to it. Sonnet in its own fresh context window produces usable transcript companion logs; Haiku did not.
## Transcript backups: /log pre-gathers metadata only, subagent reads content
`/log` calls `list-transcripts-here.sh` to pull compact JSON metadata (backup names, session IDs, paths) — no transcript content. Full JSONL content is read only by the Sonnet subagent in its own context window via `extract-transcripts.py --extract`. Keeps the parent skill's context small while still giving the subagent everything it needs.
## Transcript backups: Tracking file is JSON at `~/.claude/transcript-backups/tracking.json`
Machine-readable JSON (not a markdown table) because both the pre-compact hook (bash) and the /log skill / subagent (python) need to update it. JSON parses trivially from both; markdown tables do not.
## Skill-helper scripts: Live in claude-foundations, symlinked to `~/.claude/scripts/`
`~/.claude/` always resolves to the default profile directory regardless of `CLAUDE_CONFIG_DIR`, making `~/.claude/scripts/` a stable location accessible from all profiles. Scripts live canonically in `claude-foundations/scripts/` and `install-hooks.sh` symlinks them in. Same pattern as hooks.
## install-hooks: Curated `SKILL_HELPERS` array, not blanket symlink
Not every script in `claude-foundations/scripts/` belongs in `~/.claude/scripts/` — only those referenced by skills. `install-hooks.sh` keeps an explicit `SKILL_HELPERS` array as the authoritative list of which scripts to symlink. Regular scripts (statusline.sh, set-topic.sh, etc.) are accessed via their full repo path.

13
memory/gotchas-hooks.md Normal file
View File

@@ -0,0 +1,13 @@
# Hooks Gotchas
## PreCompact hook no longer receives `transcript_path` in JSON input
**Symptom:** `pre-compact-backup.sh` silently skipped every run — no backup file produced, no error.
**Cause:** Claude Code's PreCompact hook JSON input no longer includes `transcript_path` (changed from older versions). The hook was reading an empty value and short-circuiting.
**Fix:** Derive the transcript path from `session_id` and `cwd` instead: `~/.claude/projects/$(echo "$cwd" | tr '/' '-')/<session_id>.jsonl`. Both fields are still present in the hook input JSON.
## Hooks fail silently under `set -e` when an input field is missing
**Symptom:** Hook appears to run (exit 0) but produces no output or side effects.
**Cause:** A missing jq-extracted field returns empty, then subsequent commands operate on the empty string and either no-op or skip conditional branches without error.
**Fix:** Always validate required inputs early in the hook: `if [ -z "$session_id" ] || [ -z "$cwd" ]; then echo "missing input" >&2; exit 1; fi`. Log failures to stderr so they surface in Claude Code hook diagnostics.

7
memory/gotchas-python.md Normal file
View File

@@ -0,0 +1,7 @@
# Python Gotchas
## `sys.exit()` inside bare `except: pass` is swallowed
**Symptom:** A loop that should terminate on first match with `sys.exit(0)` kept iterating and printing multiple lines.
**Cause:** `sys.exit()` raises `SystemExit`, which is a `BaseException`. A bare `except:` (or `except Exception:`) inside the loop body catches it and continues iteration.
**Fix:** Use `break` instead of `sys.exit()` when the exit is inside an exception handler, or narrow the except clause to the specific exception you care about (e.g., `except (KeyError, ValueError):`). Never use bare `except:` — it masks `SystemExit`, `KeyboardInterrupt`, and real bugs.

View File

@@ -16,7 +16,7 @@
**Symptom:** Bang-command like `git log --since="$(git log ...)"` fails even though `Bash(git *)` is in allowed-tools.
**Cause:** The permission checker rejects any command containing `$()` subshells regardless of the outer pattern match.
**Fix:** Keep bang-commands simple. If complex logic is needed, have the skill instructions tell Claude to run it via tool calls instead.
**Fix:** Keep bang-commands simple. If complex logic is needed, either (a) have the skill instructions tell Claude to run it via tool calls instead, or (b) create a thin wrapper shell script that runs the `$()` substitution internally and call the wrapper from the bang-command (see `list-transcripts-here.sh` for the wrapper pattern).
## Relative paths in bang-commands resolve differently based on CWD

View File

@@ -0,0 +1,11 @@
# Statusline Gotchas
## set-topic.sh errors "No session ID found" on first message
Symptom: `set-topic.sh` always fails on the first assistant message because `/tmp/claude-session-id-<hash>` doesn't exist yet — `statusline.sh` hasn't run to create it.
Fix: Implemented pending-topic queue. `set-topic.sh` writes to `/tmp/claude-pending-topic-<hash>` when session ID is missing. `statusline.sh` picks it up on next run.
## md5sum hash inconsistency between scripts
Symptom: Hash mismatch between `statusline.sh` and `set-topic.sh` when computing the cwd-based filename.
Fix: Both scripts must use `echo -n "$CWD" | md5sum` — the `-n` flag (no trailing newline) is critical for consistent hashes.

View File

@@ -1,32 +0,0 @@
# Session Log — 2026-03-17
## Summary
Built a `validate-skill` script to catch known SKILL.md restriction violations before deployment. Researched all historical skill breakages across session logs and git commits, then implemented a comprehensive validator with 43 tests. Also added "Reproduce Before Fixing" best practice, built `unreflected-logs` script, and fixed 7 real errors the validator found in existing skills.
## Decisions
- Decision: Add "Reproduce Before Fixing" to `best-practices/debugging.md` rather than TDD — Rationale: TDD covers regression tests as artifacts; debugging covers the workflow of how to approach a bug
- Decision: `validate-skill` lives in small-scripts, not claude-foundations — Rationale: It's a standalone utility script, follows the spec-first pattern, symlinked to `~/sbin`
- Decision: `$VAR` in path context is an ERROR, `~/` is a WARN, `/home/` is an ERROR suggesting `~/` — Rationale: Permission checker rejects `$HOME/path` shell expansion; `~/` is more portable than `/home/paul` but still may be rejected by some sandbox modes
- Decision: Fix `$HOME/...` bang-commands by replacing with Read tool instructions — Rationale: Read tool doesn't go through the permission checker's shell expansion check; more reliable than any path syntax in bang-commands
## Gotchas Discovered
- **[skills]** Symptom: `/log` skill fails with "Shell expansion syntax in paths requires manual approval" on `cat $HOME/dev/claude/...` — Fix: Replace bang-commands using `$VAR` in paths with plain-text instructions to use the Read tool at runtime
- **[skills]** Symptom: `$HOME` (without braces) is also rejected by permission checker, not just `${HOME}` — Fix: Added `$VAR` in path context check to `validate-skill` (regex: `\$[A-Za-z_][A-Za-z0-9_]*/`)
- **[bash]** Symptom: `grep -n '^\`'` with escaped backtick doesn't match literal backtick in files — Fix: Use `grep -n '^!` + backtick without escaping; in single-quoted strings the backslash is literal, not an escape
- **[bash]** Symptom: `((PASS++))` when PASS=0 evaluates to falsy, killed by `set -e` — Fix: Use `PASS=$((PASS + 1))` which always succeeds as an assignment
## Key Context
- `validate-skill` checks: frontmatter (delimiters, name, description), bang-commands (`${VAR}`, `$VAR` in paths, `$()`, `~/`, `../`, `/home/`), allowed-tools coverage (uncovered binaries, overly broad patterns)
- Full skill failure timeline from git history: 7 commits in custom-claude-skills, 4 are fixes. `/distill-best-practices` was fixed 3 times.
- Historical failure patterns cataloged: `$()` subshell, `${VAR}` syntax, `$VAR` in paths, absolute paths blocked by sandbox, relative paths fragile, broad `Bash(git *)`, hardcoded filenames that changed, edits needing new session
- CLAUDE.md now requires running `validate-skill` before committing any skill changes
- 3 skills fixed this session: `/log`, `/reflect-logs`, `/distill-best-practices` — all had `$HOME/...` in bang-commands replaced with Read tool instructions
## Process Notes
- Researching the full history (logs + commits) before building the validator was valuable — it revealed that `$HOME` (not just `${HOME}`) is rejected, which we wouldn't have caught from docs alone
- The validator immediately proved its value by finding 7 real errors in production skills on first run against the real codebase

View File

@@ -1,23 +0,0 @@
# Session Log — 2026-03-18
## Summary
Created a shared infrastructure catalog repo (`homelab/infrastructure-docs`) documenting all homelab systems with dependency tracking. 20 system files covering physical hosts, cluster core, networking, identity, platform services, and applications.
## Decisions
- Decision: Infrastructure knowledge goes in a dedicated repo, not MEMORY.md — Rationale: Infrastructure maps are authoritative and maintained (add/remove systems), not accumulated learnings. Different lifecycle than gotchas/reflections.
- Decision: Located at `~/dev/claude/docs/infrastructure/homelab/` with parent structure supporting future locations (e.g., customer sites) — Rationale: User expects to have infrastructure at multiple logical locations in future.
- Decision: Repo in `homelab` org on Gitea, not `skynet` — Rationale: Infrastructure documentation, not AI-focused.
- Decision: Each system file uses consistent structure (what it does, depends on, depended on by, managed by) — Rationale: Enables tracing dependencies in either direction.
## Gotchas Discovered
- **[gitea]** Symptom: Push to new repo failed with "User permission denied for writing" — Fix: The SSH alias `gitea.oreillyit.nz-homelab` authenticates as `cluster-administrator`, but repo was created by `ai_admin` via API. Added `cluster-administrator` as admin collaborator via API before push succeeded.
## Key Context
- `ai_admin` Gitea token is in `cluster-bootstrap/local_secrets/gitea_ai_admin` (not in `~/dev/claude/secrets/`)
- Created repo via Gitea API: `POST /api/v1/orgs/homelab/repos`
- Added collaborator via API: `PUT /api/v1/repos/homelab/infrastructure-docs/collaborators/cluster-administrator`
- Memory reference added to both claude-foundations MEMORY.md and Claude profile memory
## Process Notes
- Good pattern: discussing the conceptual approach (MEMORY vs SPEC vs dedicated catalog) before jumping into implementation
- The Gitea permission issue could be avoided by always adding `cluster-administrator` as collaborator when creating repos via `ai_admin` API token for the `homelab` org

View File

@@ -1,18 +0,0 @@
# Session Log — 2026-03-23
## Summary
Set up a persistent status line for Claude Code that displays session topic, model name, and context window percentage. Scripts were created, moved into claude-foundations, and documentation updated across CLAUDE.md, MEMORY.md, and README.md. Status line config was added to all three profile settings files (~/.claude, ~/.claude-octopus, ~/.claude-oreillyit).
## Decisions
- Decision: Use per-session topic files at `~/.claude/status/<session-id>/claude-topic.txt` — Rationale: Session ID from the status line JSON uniquely identifies each session, allowing multiple concurrent sessions to have independent topics
- Decision: Status line script writes session ID to `/tmp/claude-session-id-<md5 of cwd>` — Rationale: Claude has no direct access to its own session ID; the status line script bridges this by writing it to a predictable path keyed by working directory
- Decision: Canonical scripts live in `claude-foundations/scripts/`, symlinked from `~/.claude/status/` — Rationale: Follows the same pattern as hooks (canonical in repo, symlinked to ~/.claude/) for version control and consistency
- Decision: Status line config added to all three profile settings files — Rationale: Each profile (default, octopus, oreillyit) has its own settings.json; statusLine must be present in each
## Gotchas Discovered
- **[claude-code]** Symptom: `set-topic.sh` fails on the first message of a session because the status line hasn't run yet to write the session ID file — Fix: This is expected; set the topic from the second message onward. The bootstrap caveat is documented in CLAUDE.md and the memory file.
## Key Context
- Claude Code status line receives session JSON on stdin with fields: `session_id`, `model`, `context_window`, `cwd`, `cost`, `rate_limits`, etc.
- Status line script runs after each assistant message, debounced at 300ms
- Three profile settings files need to stay in sync: `~/.claude/settings.json`, `~/.claude-octopus/settings.json`, `~/.claude-oreillyit/settings.json`

View File

@@ -1,19 +0,0 @@
# Session Log — 2026-03-25
## Summary
Fixed the `set-topic.sh` bootstrap race condition where calling it before `statusline.sh` had run caused an error. Implemented a pending-topic queue mechanism and added the permission rule across all three Claude profiles.
## Decisions
- Decision: Queue pending topics to `/tmp/claude-pending-topic-<hash>` instead of erroring — Rationale: `set-topic.sh` is called early in sessions (first message) before `statusline.sh` has written the session ID file. Erroring was noisy and unhelpful since the topic could just be deferred one response.
- Decision: Have `statusline.sh` pick up and apply pending topics via `mv` — Rationale: Keeps the logic simple — statusline already runs after every response, so it's the natural place to consume the pending file. Using `mv` is atomic and avoids double-application.
- Decision: Add `Bash(~/.claude/status/set-topic.sh:*)` to all three profile settings files — Rationale: The script is called every session start and is safe to auto-approve.
## Gotchas Discovered
- **[statusline]** Symptom: `set-topic.sh` always errored on first message with "No session ID found" because `/tmp/claude-session-id-<hash>` doesn't exist until `statusline.sh` runs after the first response — Fix: Write topic to a pending file; `statusline.sh` picks it up on its next run.
## Key Context
- Three profile settings files need to stay in sync: `~/.claude/settings.json`, `~/.claude-octopus/settings.json`, `~/.claude-oreillyit/settings.json`
- The `statusline.sh` hash uses `echo -n "$CWD" | md5sum` (note the `-n` flag — important for hash consistency with `set-topic.sh`)
## Process Notes
- Clean fix — two small script changes plus doc updates. Tested both the happy path (session ID exists) and the pending path (no session ID yet).

View File

@@ -0,0 +1,26 @@
# Session Log — 2026-04-13
## Summary
Built a complete pre-compaction transcript backup tracking system: fixed the broken `pre-compact-backup.sh` hook, created `extract-transcripts.py` and `list-transcripts-here.sh`, updated the `/log` skill to dispatch a Sonnet subagent for transcript analysis, moved all scripts to their canonical homes in claude-foundations, and ensured both active profiles are in sync.
## Decisions
- Decision: Use Sonnet (not Haiku) for transcript analysis subagent in `/log` — Rationale: gotcha detection requires judgment about backtracking and failed attempts; Haiku tends to see only the final outcome and miss the failure mode that led to it
- Decision: `/log` pre-gathers only transcript metadata (compact JSON) not content — Rationale: full transcript content would overflow context; content reading delegated to the Sonnet subagent in its own fresh context window
- Decision: Tracking file at `~/.claude/transcript-backups/tracking.json` (machine-readable JSON) — Rationale: both hook (bash) and skill (python) need to update it; JSON is simpler to parse than markdown tables from bash
- Decision: Scripts live in `claude-foundations/scripts/`, symlinked to `~/.claude/scripts/` — Rationale: consistent with hooks pattern; `~/.claude/scripts/` is a fixed path accessible from all profiles since `~/.claude/` always resolves to the default profile dir
- Decision: `install-hooks.sh` extended with a curated `SKILL_HELPERS` array to manage skill-helper script symlinks — Rationale: not all claude-foundations scripts belong in `~/.claude/scripts/`, only ones referenced by skills
## Gotchas Discovered
- **[hooks]** Symptom: `pre-compact-backup.sh` was silently skipping every run — Fix: Claude Code's PreCompact hook no longer provides `transcript_path` in the JSON input (changed from older versions); derive path from `session_id` + `cwd` using `~/.claude/projects/$(cwd | tr '/' '-')/<session_id>.jsonl`
- **[python]** Symptom: cwd extraction loop printed multiple lines instead of stopping at first match — Fix: `sys.exit(0)` inside `except: pass` is caught as `SystemExit`; use a `break` or don't use bare `except` when early exit is needed inside exception handlers
- **[skills]** Symptom: SKILL.md validator error on bang command using `$(pwd)` — Fix: `$()` substitution in SKILL.md bang commands is rejected by the Claude Code permission checker; create a wrapper shell script that runs `$(pwd)` internally and call the wrapper from the bang command instead
## Key Context
- Old transcript backups with `-auto` suffix (before tracking system) are not in `tracking.json` and will never appear in `--list`; they'll be pruned by the 30-day cleanup in the hook
- Profile parity fix: `~/.claude-octopus/skills/switch-mode` symlink was missing; added
- `~/.claude/scripts/` is the canonical location for skill-helper scripts; `~/.claude/` always resolves to the default profile dir, making it accessible from all profiles regardless of `CLAUDE_CONFIG_DIR`
- The Sonnet subagent in `/log` writes to the project's absolute `memory/log/` path — worth verifying on first real run that it uses the absolute path correctly, not a relative one from the wrong cwd

View File

@@ -39,3 +39,23 @@ Before creating a new system or document, discuss where it belongs conceptually
## Add SSH-authenticating user as collaborator when creating repos via API
When creating Gitea repos via API token (e.g., `ai_admin`), the SSH alias may authenticate as a different user (e.g., `cluster-administrator`). Always add the SSH user as admin collaborator via API before pushing.
## Use background agents for parallel independent research
When researching multiple topics that don't depend on each other, launch background subagents simultaneously. Both API design and LLM security research completed in ~5 minutes each, producing structured output with citations. Review results as they complete. Don't duplicate research work the subagent is doing.
## Include more surrounding context when Edit tool matches are ambiguous
The Edit tool fails if `old_string` matches multiple locations. When editing files with repeated patterns (like FUTURE.md items that all end with "Depends on: M9"), include unique surrounding lines (a heading, the preceding paragraph) to disambiguate. Don't use `replace_all: true` as a workaround — it changes all instances.
## Best-practices repo is separate from claude-foundations
Best practices live in skynet/best-practices (cloned at ~/dev/claude/projects/best-practices/). ~/dev/claude/BESTPRACTICES.md is a symlink to its index. context-load doesn't show the best-practices directory in the tree — remember it exists when working on best practices topics.
## Keep skill and script symlinks in sync across all profiles
Every profile (`~/.claude`, `~/.claude-octopus`, `~/.claude-oreillyit`) is independent — missing a symlink in one profile breaks that profile's slash commands without affecting others. When adding or renaming a skill or skill-helper script, verify symlinks exist in every active profile. Example that bit: `~/.claude-octopus/skills/switch-mode` symlink was missing while `~/.claude/skills/switch-mode` worked. Audit with `ls -la ~/.claude*/skills/` when touching skill layout.
## Verify subagent output paths use the project's absolute path
When `/log` (or any skill that spawns a subagent) writes files on the project's behalf, always verify the first real run that the subagent wrote to the project's **absolute** `memory/log/` path, not a relative path resolved from an unexpected cwd. A relative path silently lands in the wrong directory and the log looks missing.

View File

@@ -9,22 +9,27 @@ Gathers project context for a Claude Code session by walking from cwd upward, co
## What it loads (in order)
1. Every `CLAUDE.md` found walking up from cwd (top-down order) + directory tree (depth 3) from each
2. Every `CONTEXT.md` found walking up (top-down)
3. Every `MEMORY.md` found walking up (top-down)
4. Every `BESTPRACTICES.md` found walking up (top-down)
5. All `*.md` files in cwd (deduped against already-emitted files)
6. `git-status-report` output from the highest-level CLAUDE.md directory
1. **Paths** to every `CLAUDE.md` found walking up from cwd (not contents — Claude Code loads those natively via `claudeMd`), annotated with `description:` from sibling `ABOUT.md` if present
2. **Paths** to every project `CLAUDE.md` found under each CLAUDE.md directory (depth 2-4) — for project discovery, also annotated with ABOUT.md descriptions
3. Every `CONTEXT.md` found walking up (top-down) — full contents
4. Every `MEMORY.md` found walking up (top-down) — full contents
5. Every `BESTPRACTICES.md` found walking up (top-down) — full contents
## What it does NOT load
- CLAUDE.md contents (loaded natively by Claude Code)
- Directory trees (Claude can run `tree` on demand for fresh results)
- README.md files (human-oriented, read on demand)
- git-status-report (moved to `/housekeeping` skill)
## How it works
- Uses `discover_upward()` to walk from `$PWD` to `/`, collecting directories
- Resolves symlinks via `readlink -f` to deduplicate (e.g., `~/dev/claude/CLAUDE.md` symlink to `claude-foundations/CLAUDE.md`)
- Emits each file with a structured header (`=== FILE: ... ===`) for easy parsing
- Tree output uses `tree` with fallback to `find`
- ANSI codes are stripped from git-status-report output
- Uses `find` to discover project CLAUDE.md files under each hierarchy directory
## Gotchas
- Only walks **upward** from cwd — does not descend into subdirectories. Launch from within a project directory to get that project's context.
- Only walks **upward** from cwd for index files — but scans **downward** for project CLAUDE.md discovery. Launch from within a project directory to get that project's context.
- Index files (MEMORY.md, CONTEXT.md, BESTPRACTICES.md) should be thin indexes, not large documents, since they're injected into the system prompt.

View File

@@ -0,0 +1,41 @@
# script: extract-transcripts
**Location:** `claude-foundations/scripts/extract-transcripts.py`
**Symlinked to:** `~/.claude/scripts/extract-transcripts.py`
## Purpose
Manages the transcript backup tracking system. Registers pre-compaction JSONL backups, lists unprocessed ones per project, extracts readable conversation text for analysis, and marks entries as processed after a log is written.
## Usage
```bash
# Register a new backup (called by pre-compact-backup.sh hook)
python3 ~/.claude/scripts/extract-transcripts.py \
--register BACKUP_NAME --session-id SESSION_ID --cwd PROJECT_CWD
# List unprocessed transcripts for a project (used by /log skill)
python3 ~/.claude/scripts/extract-transcripts.py --list /home/paul/dev/claude/projects/foo
# → compact JSON array of {backup, session_id, saved_at, path, exists}
# Extract readable conversation from one backup (used by /log Sonnet subagent)
python3 ~/.claude/scripts/extract-transcripts.py --extract BACKUP_NAME
# Mark all unprocessed backups for a project as done
python3 ~/.claude/scripts/extract-transcripts.py \
--mark-all-processed /home/paul/dev/claude/projects/foo --log-file memory/log/YYYY-MM-DD.HHMMSS-transcripts.md
```
## How it works
- Tracking state lives in `~/.claude/transcript-backups/tracking.json`, keyed by backup filename
- Each entry: `{session_id, cwd, saved_at, processed, log_file, processed_at}`
- `--list` filters by `cwd == project_cwd` and `processed == false` — output is metadata only (no content), safe for skill pre-gathering
- `--extract` reads the JSONL, skips sidechain entries, extracts `user`/`assistant` message text, truncates per-message at 3000 chars
- Tool use blocks are summarised as `[tool: ToolName(key=...)]` rather than shown in full
## Gotchas
- `sys.exit(0)` inside a bare `except: pass` block is caught as SystemExit — always use specific exception types or `break` when early exit is needed inside exception handlers
- The JSONL entries for `type: "user"` without a `message` field (e.g., file-history snapshots) are skipped silently
- Old backups with `-auto` suffix (from before this system) are not in tracking.json and will never appear in `--list`

View File

@@ -4,7 +4,7 @@
## Purpose
Symlinks all hook scripts from `claude-foundations/hooks/` into `~/.claude/hooks/` and prints the `settings.json` configuration to add.
Symlinks all hook scripts from `claude-foundations/hooks/` into `~/.claude/hooks/`, and symlinks skill-helper scripts from `claude-foundations/scripts/` into `~/.claude/scripts/`. Also prints the `settings.json` configuration to add.
## Usage
@@ -13,15 +13,27 @@ cd ~/dev/claude/projects/claude-foundations
scripts/install-hooks.sh
```
One-time setup. Re-run after adding new hooks.
One-time setup. Re-run after adding new hooks or skill-helper scripts.
## How it works
1. Iterates over `hooks/*.sh`
2. Creates symlinks in `~/.claude/hooks/` (force-overwrites existing)
1. Iterates over `hooks/*.sh`, creates symlinks in `~/.claude/hooks/` (force-overwrites)
2. Iterates over a curated list of skill-helper scripts, creates symlinks in `~/.claude/scripts/`
3. Prints the JSON config for `~/.claude/settings.json` covering PreCompact, PostToolUse, and PreToolUse matchers
## Skill-helper scripts vs regular scripts
Not all `claude-foundations/scripts/` go into `~/.claude/scripts/`. Only scripts that skills reference via `~/.claude/scripts/` are installed there. The `SKILL_HELPERS` array in `install-hooks.sh` is the authoritative list. Currently: `extract-transcripts.py`, `list-transcripts-here.sh`.
Regular scripts (`statusline.sh`, `set-topic.sh`, etc.) are accessed via their full path in `claude-foundations/scripts/`.
## Profile notes
- Hooks: `~/.claude/hooks/` is referenced in all profile `settings.json` files — install once, works everywhere
- Scripts: `~/.claude/scripts/` is the only location skills reference — no per-profile script dirs needed
## Gotchas
- The printed JSON must be manually added to `settings.json` — the script doesn't edit it automatically.
- Symlinks mean the hook code stays in the repo; updates take effect immediately without re-running the script.
- Symlinks mean the hook/script code stays in the repo; updates take effect immediately without re-running the script.
- Adding a new skill-helper script requires updating the `SKILL_HELPERS` array in `install-hooks.sh` and re-running it.

View File

@@ -0,0 +1,26 @@
# script: list-transcripts-here
**Location:** `claude-foundations/scripts/list-transcripts-here.sh`
**Symlinked to:** `~/.claude/scripts/list-transcripts-here.sh`
## Purpose
Thin wrapper around `extract-transcripts.py --list "$(pwd)"`. Exists because SKILL.md bang commands (`!`command``) cannot use `$()` substitution — the Claude Code permission checker rejects it. The wrapper runs the substitution internally (in bash, where it's fine) and outputs the result.
## Usage
```bash
# From within a project directory
bash ~/.claude/scripts/list-transcripts-here.sh
# → JSON array of unprocessed transcript backups for the current cwd
```
## How it works
Single line: `python3 "$HOME/.claude/scripts/extract-transcripts.py" --list "$(pwd)"`
## Gotchas
- Must be called with `bash list-transcripts-here.sh` (not `python3`) — it's a shell wrapper, not a Python script
- Output is the same as `extract-transcripts.py --list`, so check that script's docs for the JSON format
- The `$()` substitution is the entire reason this wrapper exists; if that restriction is ever lifted from SKILL.md bang commands, this wrapper can be removed

View File

@@ -17,12 +17,12 @@ Run from any project directory. The skill will gather context from cwd upward, j
## How it works
- Uses `!`context-load`` to run the `scripts/context-load` script at skill load time
- The script output (CLAUDE.md files, trees, CONTEXT.md, MEMORY.md, BESTPRACTICES.md, git status) is injected directly into the skill prompt
- Uses `` !`context-load` `` to run the `scripts/context-load` script at skill load time
- The script output (CLAUDE.md paths, project listing, CONTEXT.md, MEMORY.md, BESTPRACTICES.md) is injected directly into the skill prompt
- Claude reads and internalizes the output, then confirms what it loaded
## Gotchas
- Depends on `context-load` being on `$PATH` (symlinked to `~/sbin/context-load`)
- Output size scales with the number of projects in the directory hierarchy — deep nesting or large index files may use significant tokens
- Output is lightweight (~50-200 lines) — CLAUDE.md contents are not included since Claude Code loads them natively
- Only loads index files, not topic files from `memory/` or `context/` — Claude must use Read tool for those if needed

View File

@@ -0,0 +1,33 @@
# skill: /end-session
**Location:** `custom-claude-skills/skills/end-session/SKILL.md`
## Purpose
End-of-session wrap-up that combines session logging, CONTEXT.md update, and project doc refresh into a single command. Replaces the manual workflow of running `/log` then separately updating CONTEXT.md and project docs.
## Usage
```
/end-session
```
No arguments. Reviews the full conversation history automatically.
## How it works
1. **Session log** (Phase 1): Creates `memory/log/YYYY-MM-DD.<HHMMSS>.md` with structured entries (Summary, Decisions, Gotchas, Open Questions, Key Context, Process Notes). Prunes old reflected logs per retention settings.
2. **Context update** (Phase 2): Assesses what's in progress, updates CONTEXT.md index and `context/<topic>.md` detail files so the next session can resume without conversation history.
3. **Doc refresh** (Phase 3): Updates FUTURE.md (new ideas), README.md (milestone table, scripts section), and CLAUDE.md (repo structure, conventions) -- only where this session actually changed something.
4. **Summary** (Phase 4): Prints what was logged, what the next session should pick up, and which docs were updated.
## Relationship to other skills
- Subsumes `/log` -- no need to run `/log` separately when using `/end-session`
- Does NOT include milestone reflection -- use `/reflect M<N>` separately when a milestone is complete
- CONTEXT.md updates follow the same thin-index pattern documented in CLAUDE.md
## Gotchas
- Pre-gathers git log, git status, MEMORY.md, CONTEXT.md, FUTURE.md at skill load time -- if you made changes after invoking the skill, the pre-gathered data may be stale (but the skill can re-read files via tool calls)
- `[topic]` tags on gotchas should match existing memory file topics for routing by `/reflect-logs`

View File

@@ -12,22 +12,31 @@ End-of-session logging. Captures key decisions, gotchas, open questions, and pro
/log
```
No arguments. Reviews the full conversation history automatically.
No arguments. Reviews the full conversation history and any pre-compaction transcript backups automatically.
## How it works
1. Creates `memory/log/` if needed
2. Reviews the conversation and extracts: Summary, Decisions, Gotchas (tagged with `[topic]`), Open Questions, Key Context, Process Notes
3. Writes a structured log file — empty sections are omitted
4. Prunes old logs: deletes reflected logs older than `retention_days` (default 7), warns about unreflected logs older than `warn_unreflected_days` (default 14)
3. Writes a structured in-context log — empty sections are omitted
4. **Transcript analysis (when backups exist):** Checks `~/.claude/transcript-backups/tracking.json` for unprocessed pre-compaction snapshots for the current project. If found, spawns a **Sonnet** subagent to read the JSONL backups (via `extract-transcripts.py --extract`) and write a companion log `HHMMSS-transcripts.md`. The subagent then marks backups as processed in `tracking.json`. Sonnet is used (not Haiku) because gotcha detection requires judgment about backtracking and failed attempts.
5. Prunes old logs: deletes reflected logs older than `retention_days` (default 7), warns about unreflected logs older than `warn_unreflected_days` (default 14)
## Dependencies
- `~/.claude/scripts/list-transcripts-here.sh` — pre-gathers unprocessed transcript metadata (small JSON, no context overflow)
- `~/.claude/scripts/extract-transcripts.py` — reads JSONL backups and manages tracking.json (used by the Sonnet subagent)
- Both scripts live in `claude-foundations/scripts/`, symlinked into `~/.claude/scripts/`
## Part of the knowledge pipeline
`/log``/reflect-logs``/distill-best-practices`
Raw session logs are input for `/reflect-logs`, which routes entries into topic memory files.
Raw session logs (and transcript companion logs) are input for `/reflect-logs`, which routes entries into topic memory files.
## Gotchas
- Trivial sessions can be skipped — the skill says so if nothing worth logging happened
- `[topic]` tags on gotchas should match existing memory file topics for routing by `/reflect-logs`
- The Sonnet subagent writes to the project's absolute `memory/log/` path; if it uses a relative path from the wrong cwd, the file ends up in the wrong place — verify on first real run
- Pre-compaction backups from before the tracking system existed (files with `-auto` in the name) are not in `tracking.json` and won't be processed; they'll be pruned by the 30-day cleanup in the hook

View File

@@ -0,0 +1,29 @@
# Skill: review-plan
## Purpose
Reviews a plan file (*-PLAN.md) against best practices before implementation begins. Catches security gaps, missing specs, untested assumptions, and API design issues early -- when they're cheap to fix.
## Usage
```
/review-plan M2-auth-PLAN.md
/review-plan # prompts to select from *-PLAN.md files
```
## How it works
1. Loads 5 best practice files: api-design, llm-code-security, spec-driven-development, test-driven-development, security-architecture
2. Reads the specified plan file
3. Evaluates against a 6-area checklist: security by design, API design, LLM code security, spec-driven development, test-driven development, operational readiness
4. Outputs a scorecard, critical gaps, recommendations, and acknowledgment of what's done well
## Best practice files referenced
- `~/dev/claude/projects/best-practices/api-design.md`
- `~/dev/claude/projects/best-practices/llm-code-security.md`
- `~/dev/claude/projects/best-practices/spec-driven-development.md`
- `~/dev/claude/projects/best-practices/test-driven-development.md`
- `~/dev/claude/projects/best-practices/security-architecture.md`
- Plus any tech-stack-specific files from the index
## Gotchas
- Read-only skill (allowed-tools: Read, Glob, Grep, Bash cat/ls/find). Cannot edit files.
- Relies on best-practices repo being cloned at `~/dev/claude/projects/best-practices/`
- The `~/` path in the bang-command generates a validator warning but works in practice

View File

@@ -0,0 +1,32 @@
# Skill: review-spec
## Purpose
Reviews a spec file against best practices for structure quality, requirement testability, security coverage, and API design. Ensures specs are implementation-ready before agents start coding.
## Usage
```
/review-spec spec/authentication.md
/review-spec # reads SPEC.md and prompts to select
```
## How it works
1. Loads 5 best practice files: spec-driven-development, test-driven-development, api-design, llm-code-security, security-architecture
2. Reads the specified spec file and SPEC.md index
3. Evaluates 4 dimensions: structure completeness, requirement quality, security coverage, testability
4. Checks every numbered requirement for testability, unambiguity, and rationale
5. Identifies missing requirements and scenarios
6. Outputs a scored review (1-5 per area), critical issues, missing requirements/scenarios, and recommendations
## Best practice files referenced
- `~/dev/claude/projects/best-practices/spec-driven-development.md`
- `~/dev/claude/projects/best-practices/test-driven-development.md`
- `~/dev/claude/projects/best-practices/api-design.md`
- `~/dev/claude/projects/best-practices/llm-code-security.md`
- `~/dev/claude/projects/best-practices/security-architecture.md`
- Plus any tech-stack-specific files from the index
## Gotchas
- Read-only skill (allowed-tools: Read, Glob, Grep, Bash cat/ls/find). Cannot edit files.
- Relies on best-practices repo being cloned at `~/dev/claude/projects/best-practices/`
- The `~/` path in the bang-command generates a validator warning but works in practice
- Counts requirements and verifies summary counts -- catches stale counts after edits

View File

@@ -0,0 +1,36 @@
# /spawn-session
## Purpose
Spawn a new, separate Claude Code session inside a detached `tmux` session with Remote
Control enabled, in a project's directory, named after the project (e.g. "Agent Runtimes",
auto-incrementing to "Agent Runtimes #2"). Lets the user start a session for another project
that persists on the host and is reachable later via `tmux attach` (over VPN) or driveable
from the Claude app.
## Usage
- `/spawn-session <project>` — spawn for a named project folder
- `/spawn-session <project> --mode <mode>` — override engagement mode
- `/spawn-session <project> --attach` — attach after spawning
- `/spawn-session` — lists projects and asks which one
## How it works
Thin wrapper over the `claude-tmux` script (in `small-scripts`, symlinked to `~/sbin`).
The skill only resolves the project name from `$ARGUMENTS` (or asks) and shells out to
`claude-tmux <project> [flags]`. All real logic — folder resolution, mode selection
(profile `last-mode` unless `--mode`), Title-Casing, ` #N` auto-increment, tmux creation,
and launching `claude --remote-control` via `claude-profile` — lives in the script.
## Gotchas
- **Separate process, not a sub-agent.** The spawned session does not share the current
conversation's context. It's a full interactive Claude Code session the user drives.
- **Remote Control only registers once `claude` is running.** The script guarantees this by
passing the engagement mode explicitly so `claude-profile` never stalls at its interactive
picker. Don't "fix" the skill to omit the mode.
- **Default profile is `oreillyit-anthropic`.** Only override with `--profile` on request.
- **Concurrent same-profile sessions** race on the shared `active-mode.env`, but each
session's cwd takes precedence for project auto-selection, so the right project is picked.
- Spec + tests: `small-scripts/specs/claude-tmux.spec.md`, `tests/test-claude-tmux.sh`.

View File

@@ -5,17 +5,13 @@
# Outputs structured context to stdout, suitable for --append-system-prompt.
#
# What it loads:
# - Every CLAUDE.md found walking up from cwd (top-down order)
# - Directory tree (depth 3) from each CLAUDE.md location
# - Paths to every CLAUDE.md found walking up from cwd (not contents — Claude Code loads those natively)
# - Paths to every project CLAUDE.md found under each CLAUDE.md directory (for project discovery)
# - Every CONTEXT.md found walking up from cwd (top-down order)
# - Every MEMORY.md found walking up from cwd (top-down order)
# - Every BESTPRACTICES.md found walking up from cwd (top-down order)
# - All *.md files in cwd
# - git-status-report from the highest-level CLAUDE.md dir
set -uo pipefail
TREE_DEPTH=3
# --- Helpers ---
emit_header() {
@@ -33,16 +29,15 @@ emit_file() {
echo ""
}
emit_tree() {
local dir="$1"
emit_header "TREE: $dir (depth $TREE_DEPTH)"
tree -L "$TREE_DEPTH" --charset utf-8 -I '.git|node_modules|__pycache__|.venv|venv|customers' "$dir" 2>/dev/null \
|| find "$dir" -maxdepth "$TREE_DEPTH" -not -path '*/.git/*' -not -path '*/.git' -not -path '*/customers/*' -not -path '*/customers' | sort
echo ""
}
strip_ansi() {
sed 's/\x1b\[[0-9;]*m//g'
# Extract description from ABOUT.md in the same directory as a CLAUDE.md
get_project_description() {
local claude_path="$1"
local dir
dir="$(dirname "$claude_path")"
local about="$dir/ABOUT.md"
if [[ -f "$about" ]]; then
sed -n 's/^description:[[:space:]]*//p' "$about" | head -1
fi
}
# --- Discovery: walk from cwd upward ---
@@ -65,10 +60,8 @@ claude_dirs=()
context_dirs=()
memory_dirs=()
bestpractices_dirs=()
all_hierarchy_dirs=()
while IFS= read -r dir; do
all_hierarchy_dirs+=("$dir")
[[ -f "$dir/CLAUDE.md" ]] && claude_dirs+=("$dir")
[[ -f "$dir/CONTEXT.md" ]] && context_dirs+=("$dir")
[[ -f "$dir/MEMORY.md" ]] && memory_dirs+=("$dir")
@@ -90,21 +83,53 @@ else
emit_header "CLAUDE PROFILE: $profile_name ($profile_dir)"
fi
# --- Output: CLAUDE.md files (top-down) + tree from each ---
# --- Output: CLAUDE.md paths (not contents — Claude Code loads those natively) ---
# Also discover project CLAUDE.md files under each directory for project listing
# Track files we've already emitted to avoid duplicates
declare -A emitted_files
emit_header "CLAUDE.md FILES"
for dir in "${claude_dirs[@]}"; do
# Resolve symlinks for dedup — two paths might point to the same file
real_path="$(readlink -f "$dir/CLAUDE.md")"
if [[ -z "${emitted_files[$real_path]:-}" ]]; then
emit_file "$dir/CLAUDE.md"
desc="$(get_project_description "$dir/CLAUDE.md")"
if [[ -n "$desc" ]]; then
echo "- $dir/CLAUDE.md — $desc"
else
echo "- $dir/CLAUDE.md"
fi
emitted_files["$real_path"]=1
fi
emit_tree "$dir"
done
# Discover project CLAUDE.md files (depth 2-4 under each CLAUDE.md dir)
# This replaces the old tree output — gives Claude a fresh project listing
project_claudes=()
for dir in "${claude_dirs[@]}"; do
while IFS= read -r f; do
real_path="$(readlink -f "$f")"
if [[ -z "${emitted_files[$real_path]:-}" ]]; then
project_claudes+=("$f")
emitted_files["$real_path"]=1
fi
done < <(find "$dir" -mindepth 2 -maxdepth 4 -name CLAUDE.md -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/__pycache__/*' 2>/dev/null | sort)
done
if [[ ${#project_claudes[@]} -gt 0 ]]; then
echo ""
echo "Project CLAUDE.md files:"
for f in "${project_claudes[@]}"; do
desc="$(get_project_description "$f")"
if [[ -n "$desc" ]]; then
echo "- $f — $desc"
else
echo "- $f"
fi
done
fi
echo ""
# --- Output: CONTEXT.md files (top-down) ---
for dir in "${context_dirs[@]}"; do
@@ -134,27 +159,3 @@ for dir in "${bestpractices_dirs[@]}"; do
emitted_files["$real_path"]=1
fi
done
# --- Output: all *.md files in cwd ---
if compgen -G "$PWD"/*.md > /dev/null 2>&1; then
for md_file in "$PWD"/*.md; do
[[ -f "$md_file" ]] || continue
real_path="$(readlink -f "$md_file")"
if [[ -z "${emitted_files[$real_path]:-}" ]]; then
emit_file "$md_file"
emitted_files["$real_path"]=1
fi
done
fi
# --- Output: git status report ---
if [[ ${#claude_dirs[@]} -gt 0 ]]; then
highest_dir="${claude_dirs[0]}"
git_status_report="$(command -v git-status-report 2>/dev/null || true)"
if [[ -n "$git_status_report" ]]; then
emit_header "GIT STATUS: $highest_dir"
"$git_status_report" "$highest_dir" 2>/dev/null | strip_ansi || true
fi
fi

265
scripts/extract-transcripts.py Executable file
View File

@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""
Manage and extract session transcript backups for the /log skill.
Modes:
--register BACKUP_NAME --session-id SID --cwd CWD
Register a new backup file in tracking.json (called by pre-compact hook).
--list CWD
List unprocessed transcripts for a project as JSON (small output, for /log skill).
--extract BACKUP_NAME
Extract readable conversation text from one backup (for subagent use).
--mark-processed BACKUP_NAME LOG_FILE
Mark a backup as processed, recording which log file captured it.
--mark-all-processed CWD LOG_FILE
Mark ALL unprocessed backups for a cwd as processed.
Tracking file: ~/.claude/transcript-backups/tracking.json
"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
BACKUP_DIR = Path.home() / ".claude" / "transcript-backups"
TRACKING_FILE = BACKUP_DIR / "tracking.json"
MAX_TEXT_LEN = 3000 # max chars per message before truncating
def load_tracking():
if TRACKING_FILE.exists():
with open(TRACKING_FILE) as f:
return json.load(f)
return {}
def save_tracking(tracking):
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
with open(TRACKING_FILE, "w") as f:
json.dump(tracking, f, indent=2)
def now_utc():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def extract_text_from_content(content):
"""Extract plain text from a message content field (string or list of blocks)."""
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
parts = []
for block in content:
if not isinstance(block, dict):
continue
btype = block.get("type")
if btype == "text":
text = block.get("text", "").strip()
if text:
parts.append(text)
elif btype == "tool_use":
name = block.get("name", "?")
inp = block.get("input", {})
keys = list(inp.keys())[:3] if isinstance(inp, dict) else []
summary = ", ".join(f"{k}=..." for k in keys) if keys else "..."
parts.append(f"[tool: {name}({summary})]")
return " ".join(parts)
return ""
def extract_conversation(jsonl_path):
"""Read a JSONL transcript; return list of (role, text) tuples."""
turns = []
try:
with open(jsonl_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("type") not in ("user", "assistant"):
continue
if entry.get("isSidechain"):
continue
msg = entry.get("message", {})
if not isinstance(msg, dict):
continue
role = msg.get("role", entry.get("type", "?"))
content = msg.get("content", "")
text = extract_text_from_content(content)
if not text:
continue
if len(text) > MAX_TEXT_LEN:
text = text[:MAX_TEXT_LEN] + f"... [truncated, {len(text)} chars total]"
turns.append((role, text))
except Exception as e:
turns.append(("error", f"Failed to read transcript: {e}"))
return turns
# ---------------------------------------------------------------------------
# Modes
# ---------------------------------------------------------------------------
def cmd_register(args):
"""Register a new backup in tracking.json."""
try:
idx = args.index("--register")
backup_name = args[idx + 1]
session_id = args[args.index("--session-id") + 1]
cwd = args[args.index("--cwd") + 1]
except (ValueError, IndexError) as e:
print(f"Usage: --register BACKUP_NAME --session-id SID --cwd CWD", file=sys.stderr)
sys.exit(1)
tracking = load_tracking()
tracking[backup_name] = {
"session_id": session_id,
"cwd": cwd,
"saved_at": now_utc(),
"processed": False,
"log_file": None,
}
save_tracking(tracking)
print(f"Registered {backup_name} for {cwd}")
def cmd_list(cwd):
"""List unprocessed transcripts for a cwd as compact JSON."""
tracking = load_tracking()
unprocessed = [
{
"backup": fname,
"session_id": entry["session_id"][:8],
"saved_at": entry.get("saved_at", "?"),
"path": str(BACKUP_DIR / fname),
"exists": (BACKUP_DIR / fname).exists(),
}
for fname, entry in sorted(tracking.items(), key=lambda x: x[1].get("saved_at", ""))
if entry.get("cwd") == cwd and not entry.get("processed", False)
]
print(json.dumps(unprocessed, indent=2))
def cmd_extract(backup_name):
"""Extract readable conversation from one backup file."""
backup_path = BACKUP_DIR / backup_name
if not backup_path.exists():
print(f"Backup not found: {backup_path}", file=sys.stderr)
sys.exit(1)
tracking = load_tracking()
entry = tracking.get(backup_name, {})
print(f"{'='*72}")
print(f"Transcript: {backup_name} (pre-compaction snapshot)")
print(f"Project: {entry.get('cwd', 'unknown')}")
print(f"Session: {entry.get('session_id', '?')[:8]} | Saved: {entry.get('saved_at', '?')}")
print(f"{'='*72}")
turns = extract_conversation(backup_path)
if not turns:
print("[no readable conversation found]")
return
for role, text in turns:
label = "USER" if role == "user" else "ASST"
print(f"\n[{label}] {text}")
print(f"\n[{len(turns)} turns extracted]")
def cmd_mark_processed(backup_name, log_file):
"""Mark one backup as processed."""
tracking = load_tracking()
if backup_name not in tracking:
print(f"Warning: {backup_name} not found in tracking, adding entry", file=sys.stderr)
tracking[backup_name] = {}
tracking[backup_name]["processed"] = True
tracking[backup_name]["processed_at"] = now_utc()
tracking[backup_name]["log_file"] = log_file
save_tracking(tracking)
print(f"Marked {backup_name} as processed → {log_file}")
def cmd_mark_all_processed(cwd, log_file):
"""Mark all unprocessed backups for a cwd as processed."""
tracking = load_tracking()
count = 0
for fname, entry in tracking.items():
if entry.get("cwd") == cwd and not entry.get("processed", False):
tracking[fname]["processed"] = True
tracking[fname]["processed_at"] = now_utc()
tracking[fname]["log_file"] = log_file
count += 1
save_tracking(tracking)
print(f"Marked {count} transcript(s) as processed → {log_file}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
args = sys.argv[1:]
if not args:
print(__doc__)
sys.exit(1)
if "--register" in args:
cmd_register(args)
elif "--list" in args:
idx = args.index("--list")
cwd = args[idx + 1] if idx + 1 < len(args) else ""
if not cwd:
print("Usage: --list CWD", file=sys.stderr)
sys.exit(1)
cmd_list(cwd)
elif "--extract" in args:
idx = args.index("--extract")
backup_name = args[idx + 1] if idx + 1 < len(args) else ""
if not backup_name:
print("Usage: --extract BACKUP_NAME", file=sys.stderr)
sys.exit(1)
cmd_extract(backup_name)
elif "--mark-all-processed" in args:
idx = args.index("--mark-all-processed")
cwd = args[idx + 1] if idx + 1 < len(args) else ""
log_idx = args.index("--log-file") if "--log-file" in args else -1
log_file = args[log_idx + 1] if log_idx >= 0 and log_idx + 1 < len(args) else ""
if not cwd or not log_file:
print("Usage: --mark-all-processed CWD --log-file LOG_FILE", file=sys.stderr)
sys.exit(1)
cmd_mark_all_processed(cwd, log_file)
elif "--mark-processed" in args:
idx = args.index("--mark-processed")
backup_name = args[idx + 1] if idx + 1 < len(args) else ""
log_idx = args.index("--log-file") if "--log-file" in args else -1
log_file = args[log_idx + 1] if log_idx >= 0 and log_idx + 1 < len(args) else ""
if not backup_name or not log_file:
print("Usage: --mark-processed BACKUP_NAME --log-file LOG_FILE", file=sys.stderr)
sys.exit(1)
cmd_mark_processed(backup_name, log_file)
else:
print(f"Unknown arguments: {args}", file=sys.stderr)
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,26 +1,49 @@
#!/usr/bin/env bash
# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/
# and print the settings.json configuration to add.
# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/,
# and symlink skill-helper scripts from claude-foundations/scripts/ into ~/.claude/scripts/.
# Prints the settings.json configuration to add for hooks.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
HOOKS_SRC="$(cd "$SCRIPT_DIR/../hooks" && pwd)"
HOOKS_DST="$HOME/.claude/hooks"
SCRIPTS_DST="$HOME/.claude/scripts"
# ---------------------------------------------------------------------------
# Hooks
# ---------------------------------------------------------------------------
mkdir -p "$HOOKS_DST"
echo "Installing hooks from $HOOKS_SRC$HOOKS_DST"
echo ""
echo "Installing hooks: $HOOKS_SRC$HOOKS_DST"
for HOOK in "$HOOKS_SRC"/*.sh; do
NAME="$(basename "$HOOK")"
ln -sf "$HOOK" "$HOOKS_DST/$NAME"
echo "$NAME"
done
# ---------------------------------------------------------------------------
# Skill-helper scripts (scripts that skills reference via ~/.claude/scripts/)
# Not all claude-foundations scripts go here — only the ones used by skills.
# ---------------------------------------------------------------------------
mkdir -p "$SCRIPTS_DST"
echo ""
echo "Hooks symlinked. Ensure your ~/.claude/settings.json includes:"
echo "Installing skill-helper scripts: $SCRIPT_DIR$SCRIPTS_DST"
SKILL_HELPERS=(
"extract-transcripts.py"
"list-transcripts-here.sh"
)
for SCRIPT in "${SKILL_HELPERS[@]}"; do
SRC="$SCRIPT_DIR/$SCRIPT"
if [ -f "$SRC" ]; then
ln -sf "$SRC" "$SCRIPTS_DST/$SCRIPT"
echo "$SCRIPT"
else
echo "$SCRIPT (not found at $SRC)"
fi
done
echo ""
echo "Done. Ensure your ~/.claude/settings.json (and profile settings.json files) include:"
echo ""
cat <<'EOF'
{
@@ -50,3 +73,6 @@ cat <<'EOF'
}
}
EOF
echo ""
echo "Note: hooks are referenced as ~/.claude/hooks/ (default profile path) in all profile settings.json files."
echo "Note: skill-helper scripts are installed to ~/.claude/scripts/ only — profiles reference this shared path."

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# Wrapper for extract-transcripts.py --list, passing the current working directory.
# Used by the /log skill's pre-gathered context (bang commands can't use $() substitution).
python3 "$HOME/.claude/scripts/extract-transcripts.py" --list "$(pwd)"

View File

@@ -9,6 +9,26 @@ DATA=$(cat)
SESSION_ID=$(echo "$DATA" | jq -r '.session_id // empty')
MODEL=$(echo "$DATA" | jq -r '.model.display_name // "unknown"')
CONTEXT_PCT=$(echo "$DATA" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
FIVE_HR=$(echo "$DATA" | jq -r '.rate_limits.five_hour.used_percentage // empty' | cut -d. -f1)
SEVEN_DAY=$(echo "$DATA" | jq -r '.rate_limits.seven_day.used_percentage // empty' | cut -d. -f1)
MODE_TAG=""
MODEL_DISPLAY="$MODEL"
if [[ -n "${CLAUDE_CONFIG_DIR:-}" && -f "$CLAUDE_CONFIG_DIR/active-mode.env" ]]; then
env_file="$CLAUDE_CONFIG_DIR/active-mode.env"
MODE_TAG=$(grep '^CLAUDE_MODE_TAG=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
raw_driver=$(grep '^CLAUDE_DRIVER=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
escalates_to=$(grep '^CLAUDE_ESCALATES_TO=' "$env_file" 2>/dev/null | cut -d= -f2- || true)
if [[ -n "$raw_driver" ]]; then
# Capitalize first letter of driver (sonnet→Sonnet, haiku→Haiku, opus→Opus)
driver_cap="$(tr '[:lower:]' '[:upper:]' <<< "${raw_driver:0:1}")${raw_driver:1}"
if [[ "$escalates_to" == "opus" ]]; then
MODEL_DISPLAY="${driver_cap}→Opus"
else
MODEL_DISPLAY="$driver_cap"
fi
fi
fi
TOPIC=""
if [[ -n "$SESSION_ID" ]]; then
@@ -33,8 +53,23 @@ if [[ -n "$SESSION_ID" ]]; then
fi
fi
if [[ -n "$TOPIC" ]]; then
echo "[$MODEL] $TOPIC | ${CONTEXT_PCT}% context"
else
echo "[$MODEL] ${CONTEXT_PCT}% context"
USAGE=""
if [[ -n "$FIVE_HR" ]]; then
USAGE=" | 5h: ${FIVE_HR}%"
if [[ -n "$SEVEN_DAY" ]]; then
USAGE="$USAGE 7d: ${SEVEN_DAY}%"
fi
fi
# Prepend mode tag to topic if set
if [[ -n "$MODE_TAG" && -n "$TOPIC" ]]; then
TOPIC="${MODE_TAG} · ${TOPIC}"
elif [[ -n "$MODE_TAG" ]]; then
TOPIC="$MODE_TAG"
fi
if [[ -n "$TOPIC" ]]; then
echo "[$MODEL_DISPLAY] $TOPIC | ${CONTEXT_PCT}% context${USAGE}"
else
echo "[$MODEL_DISPLAY] ${CONTEXT_PCT}% context${USAGE}"
fi

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# sync-best-practices.sh — Sync best-practices/ from skynet/best-practices repo
# Usage: scripts/sync-best-practices.sh [--dry-run]
set -euo pipefail
REPO_URL="https://gitea.oreillyit.nz/skynet/best-practices.git"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BP_DIR="$REPO_ROOT/best-practices"
BESTPRACTICES_MD="$REPO_ROOT/BESTPRACTICES.md"
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
echo "[dry-run] No changes will be written or committed"
fi
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
echo "Cloning $REPO_URL ..."
git clone --depth=1 "$REPO_URL" "$TMPDIR/best-practices"
# Copy all .md files from the source repo into best-practices/
echo "Copying .md files to $BP_DIR ..."
if ! $DRY_RUN; then
find "$TMPDIR/best-practices" -maxdepth 1 -name "*.md" -exec cp {} "$BP_DIR/" \;
# Also copy subdirectory .md files preserving structure
find "$TMPDIR/best-practices" -mindepth 2 -name "*.md" | while read -r src; do
rel="${src#$TMPDIR/best-practices/}"
dest="$BP_DIR/$rel"
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
done
else
echo "[dry-run] Would copy:"
find "$TMPDIR/best-practices" -name "*.md" | sed "s|$TMPDIR/best-practices/||"
fi
# Update BESTPRACTICES.md topic list from INDEX.md, preserving the redirect header
INDEX_MD="$TMPDIR/best-practices/INDEX.md"
if [[ -f "$INDEX_MD" ]]; then
echo "Updating $BESTPRACTICES_MD topic list from INDEX.md ..."
if ! $DRY_RUN; then
# Extract the redirect header (everything up to and including the first blank line after the blockquote)
HEADER=$(awk '/^> \*\*Note:\*\*/{found=1} found{print} found && /^$/{exit}' "$BESTPRACTICES_MD")
if [[ -z "$HEADER" ]]; then
# Fallback: preserve first 4 lines (the note block)
HEADER=$(head -4 "$BESTPRACTICES_MD")
fi
# Write header + blank line + INDEX.md content
{
echo "$HEADER"
echo ""
cat "$INDEX_MD"
} > "$BESTPRACTICES_MD"
else
echo "[dry-run] Would update $BESTPRACTICES_MD from INDEX.md"
fi
else
echo "No INDEX.md found in source repo — skipping BESTPRACTICES.md update"
fi
# Commit if there are changes
if ! $DRY_RUN; then
cd "$REPO_ROOT"
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit."
else
git add best-practices/ BESTPRACTICES.md
git commit -m "Sync best-practices from skynet/best-practices"
echo "Committed sync changes."
fi
fi
echo "Done."

View File

@@ -11,10 +11,27 @@ distill:
projects_dir: projects # Relative to CLAUDE_PROJECT_ROOT
projects: # Projects to scan for memory changes
- agent-runtimes
- agent-runtimes-deploy
- ai-image-gen
- brainiac-app
- claude-foundations
- cluster-apps/agent-runtimes
- cluster-apps/octopus-deploy
- cluster-bootstrap
- contracts
- crud-accelerator
- custom-claude-skills
- dns-manager
- hugo-accelerator
- hugo-gabby-oreilly-counselling-content
- hugo-oreillyconsulting-integration
- small-scripts
extra_projects: # Projects outside projects_dir (paths relative to CLAUDE_PROJECT_ROOT)
- path: small-scripts
- path: octopus/ai-assisted-migration
- path: octopus/customer-issue-sync
- path: octopus/duel-image
- path: octopus/goes
- path: octopus/PlatformHub-Demo
- path: octopus/policy-demo
- path: octopus/slb
- path: octopus/the-case-for-agentic