diff --git a/claude/commands/analyze-service.md b/claude/commands/analyze-service.md index dac9562..8b69dc0 100644 --- a/claude/commands/analyze-service.md +++ b/claude/commands/analyze-service.md @@ -21,15 +21,27 @@ If the service obviously belongs to one repo, scope there; if ambiguous, search ## Cache / index (local pointer & nuance layer) -Alongside the live recon, `/analyze-service` keeps a **local, out-of-repo, gitignored** markdown pointer/nuance sheet per service, so each run front-loads "this bit us with X" instead of re-discovering every gotcha from scratch. The sheet holds **pointers + nuance only** — never live state, never re-derived config values. - -- **Location:** `~/.claude/analyze-service-index//.md` — a LOCAL cache under the user's home `.claude`, **not** versioned in any cluster repo (nothing to gitignore inside a repo — it never lands there). -- **``** is the basename of the owning repo root the service resolved into: `datapacket-talos`, `homelab-talos`, or (if it lives in neither infra repo) the current working repo's basename. Derived from the same locate step below — no separate assumption. If those infra-repo roots ever move, this slug derivation moves with them. -- **``** is normalized: lowercase, any char outside `[a-z0-9._-]` → `-`, collapsed — applied identically on read and write so `External DNS` / `externaldns` / `external-dns` all resolve to one `external-dns.md`. Match `$ARGUMENTS` against the filename **and** the file's `aliases:` before deciding a service has no index yet. -- **Lazy — nothing pre-created.** `~/.claude/analyze-service-index/` may not exist; the dir + a service file appear only on a confirmed write-back (see "## Write-back (opt-in)"). +Alongside the live recon, `/analyze-service` keeps a **markdown pointer/nuance sheet per service under `~/.claude`**, so each run front-loads "this bit us with X" instead of re-discovering every gotcha. It holds **pointers + nuance only** — never live state, never re-derived config values. + +- **Location:** `~/.claude/analyze-service-index//.md` — local, never inside a cluster repo or `devrc`. But **not "outside git": each `/` is its own remote-less git repo** (the store root is not one) — see 🔴 **Store safety** before running any git command there. +- **``** defaults to the basename of the owning repo root the service resolved into: `datapacket-talos`, `homelab-talos`, else the current working repo's basename — derived from the locate step below, no separate assumption. A scope **need not be a repo**: a ritual owned by no repo, or a client spanning several, may use a plain scope word — a deliberate choice, so say so in the brief. +- **``** is normalized: lowercase, `_` → `-`, any other char outside `[a-z0-9.-]` → `-`, collapsed — applied identically on read and write **and to `aliases:` before comparing**, so `External DNS` / `externaldns` / `external-dns` land on one file, and so do `image_ingestion` / `image-ingestion`. The `_` fold matters: the index links `_`-spelled `MEMORY.md` slugs (`bastion_config_stale_until_reload_2026_07_08`). **Keep the pre-fold spelling in `aliases:`** — it stays a valid ref and records how the thing is really written. +- **Kind qualification — only when disambiguation is needed.** One slug can name two KINDS of thing (`devrc/repo-cos` is both a code subsystem and the weekly ritual about it): qualify with `..md` (`repo-cos.process.md`), kind ∈ `service` | `process` | `org` | `doc`. A trailing dot-segment is a kind **only if it is in that enum**, else it's part of the slug. 🔴 **Bare `.md` stays the default: no existing file is renamed, and a scope with no qualified filename behaves exactly as before.** +- **Resolution — ambiguity is an ERROR, never a shadow.** Two tiers; an alias can never outrank a filename: + 1. **Filename tier** — normalized ref vs `.md` *and* every `..md` in the scope. A ref naming its own kind (`repo-cos.process`) matches only that qualified file. + 2. **Alias tier** — normalized `aliases:` across the scope, consulted **only if tier 1 returned zero hits**. + One hit → use it. **>1 in a tier → never pick: stop, call the ref ambiguous and list the candidates** (`repo-cos.md` vs `repo-cos.process.md`) for the user to choose. Zero in both → no index yet. +- **Lazy** — a scope dir or service file may not exist yet; it appears only on a confirmed write-back (see "## Write-back (opt-in)"). + +🔴 **Store safety.** The content is **curated, irreplaceable, not re-derivable by re-running recon**, with no off-machine backup. Inside any scope dir: +- **Never `git stash`** — `refs/stash` is repo-**global** and concurrent sessions share this store, so your stash can be popped or dropped by another session. Set work aside with `cp /tmp/…` instead. +- **Never `git reset --hard`, `git clean`, or `git checkout --`** — each destroys curated content that has no other copy. +- **Never add a remote, never push**, and never copy a line into `devrc` (PUBLIC) or any public repo, issue, PR, gist or commit message. devrc `60e6d9d` exists because this data class had to be scrubbed out of a public repo retroactively. +- Each scope's own `README.md` states the policy governing it — **read it before writing there**. **File schema** (markdown, so prose is surfaced verbatim via Read and reads well in a diff): -- **Front-matter — identity only:** `service` (canonical name, matches filename), `aliases` (alternate spellings), `repo` (owning repo basename — human note), `namespace` (human note; may be `multiple` for umbrella services). No machine/location fields. +- **Front-matter — identity + sensitivity only:** `service` (canonical name, matches the filename's slug part), `aliases` (alternate spellings, incl. pre-normalization ones), `scope` (owning repo basename or the non-repo scope word — **replaces `repo:`**, which older files still carry and reads as `scope`), `sensitivity` (below), `namespace` (**optional** — keep it for load-bearing k8s infra, `multiple` for umbrella services; **omit rather than write `n/a`**), `kind` (optional; only meaningful on a kind-qualified filename). No machine/location fields. +- 🔴 **`sensitivity:` — fail-safe: absent means sensitive.** One of `client-confidential` | `personal` | `public`; **absent or unrecognized ⇒ `client-confidential`, never public**, and `public` is a deliberate operator claim a recon run may never infer. Live, not hypothetical — entries carry client-identifying infrastructure detail, down to named individuals. Handling rules are in 🔴 **Store safety** above; this spec only **marks**. - **`## What it is`** — one-line description. For an umbrella/multi-instance service (redis, monitoring, meilisearch) enumerate the instances in prose — it's an index OF instances, not one location. - **`## Pointers`** — each entry is a path/slug + one-clause why, **never a copy** of the pointed-to content: - `manage-* skill:` the matching skill (e.g. `manage-redis`) — invoke it for ops. @@ -37,13 +49,13 @@ Alongside the live recon, `/analyze-service` keeps a **local, out-of-repo, gitig - `claudedocs handoff(s):` handoff doc path(s). - **`## Nuance / work-history`** — dated bullets, newest-first, ≤2 lines each: a gotcha, a lying/misleading status condition, a revert or bump that explains why someone was looking, an incident tie-in. Prune-on-resolve. -**Read at recon START.** When an index file exists for the resolved service, surface its `## Pointers` + `## Nuance / work-history` **before** re-discovering gotchas below — front-load the curated recall, then run locate/config/live live as usual. Label these `from index` in the brief. A read miss (no file) just proceeds with today's behavior and may offer to create the file on write-back. +**Read at recon START** — front-load the curated recall before re-discovering anything; mechanics in step 1 below. A miss proceeds with today's behavior and may offer to create the file on write-back. ## Recon steps -1. **Locate (deterministic, parallel).** Glob/grep the service name across the repo root(s) to find its directory + manifests: `kustomization.yaml`, `HelmRelease`, `Deployment`/`StatefulSet`/`DaemonSet`, `ConfigMap`, `*values*.yaml`. Identify the **namespace** and the owning **kustomization/Flux Kustomization**. Prefer the Grep/Glob tools; for a broad sweep dispatch an **Explore** subagent and have it return file paths + the key config excerpts (not whole-file dumps). +1. **Locate (deterministic, parallel).** Glob/grep the service name across the repo root(s) to find its directory + manifests: `kustomization.yaml`, `HelmRelease`, `Deployment`/`StatefulSet`/`DaemonSet`, `ConfigMap`, `*values*.yaml`. Identify the **namespace** and the owning **kustomization/Flux Kustomization**. Prefer the Grep/Glob tools; for a broad sweep dispatch an **Explore** subagent returning file paths + key config excerpts (not whole-file dumps). - Once the owning repo (hence ``) is known, **read `~/.claude/analyze-service-index//.md` if it exists and surface its `## Pointers` + `## Nuance / work-history` first** (labelled `from index`), before deriving any gotchas below — this is the front-load. The locate/config/live steps still run live every time; the index only supplies pointers and prior nuance, never a cached location or status. A miss just proceeds. + Once the owning `` is known, do the index read described above — resolve the ref, surface `## Pointers` + `## Nuance / work-history` (labelled `from index`) **before** deriving any gotchas below. Locate/config/live still run live every time; an ambiguous ref stops for a choice. 2. **Config.** Read the manifests found. Pull out the load-bearing knobs: image/chart version, replicas/HPA, resources, key env/ConfigMap values, mounted secrets (names only — never print secret contents), exposed routes/services, dependsOn. @@ -58,12 +70,12 @@ Alongside the live recon, `/analyze-service` keeps a **local, out-of-repo, gitig ## Output — recon brief -Header line: which index file was read + hit/miss — e.g. `index: datapacket-talos/redis.md — pointers loaded` or `index: none (first run)`. +Header line: which index file resolved + hit/miss — e.g. `index: datapacket-talos/redis.md — pointers loaded`, `index: none (first run)`, or `index: AMBIGUOUS — repo-cos.md | repo-cos.process.md (pick one)`. - **Service** + one-line "what it is". -- **Pointers / nuance** (`from index`): the `## Pointers` + `## Nuance / work-history` surfaced at recon start, if any — curated recall to follow for detail, not this-run observation. Omit if the index missed. -- **Lives at**: repo + path(s) as `file:line` (clickable), namespace, owning kustomization. Always **re-derived live** (Phase 1 never caches location). -- **Config**: the load-bearing knobs (version, scale, resources, key values, routes, deps). Always **re-derived live**. +- **Pointers / nuance** (`from index`): the `## Pointers` + `## Nuance / work-history` surfaced at recon start, if any. Omit if the index missed. +- **Lives at**: repo + path(s) as `file:line` (clickable), namespace, owning kustomization (Phase 1 never caches location). +- **Config**: the load-bearing knobs (version, scale, resources, key values, routes, deps). - **Live**: pod/HR/kustomization status + anything unhealthy — or "unverified (no cluster access)". - **Recent changes**: last few commits touching it, flag any revert/bump. - **Gotchas**: anything non-obvious you hit (lying status conditions, stale comments, ephemeral-vs-durable, etc). @@ -80,7 +92,7 @@ Recon stays **read-only by default** — the index is mutated only when a run su 2. **After** the brief, evaluate whether it surfaced anything **notable** (below). 3. Nothing notable → **do nothing**, say `index unchanged`. 4. A proposed change → present it as a **unified diff** against the current index file (or "new file" for first-ever), one compact block, and ask a single yes/no: *"append this to the index? (y/N)"*. -5. **Write only on explicit confirm.** On confirm, re-read the file (so a concurrent append isn't clobbered), re-apply the change to current bytes, then plain Write to `~/.claude/analyze-service-index//.md` (creating the dir/file if first-ever). On decline, discard — the recon result already stood on its own. The write is local and final; there is no commit/worktree step (the file is outside every repo). +5. **Write only on explicit confirm.** On confirm, re-read the file (so a concurrent append isn't clobbered), re-apply the change to current bytes, then plain Write to `~/.claude/analyze-service-index//.md` (creating the dir/file if first-ever; use `..md` **only** when a same-slug entry of another kind already exists, and say why in the diff). On decline, discard. The write is local and final — **nothing leaves the machine**: no remote, no push. But it is **not** outside git; committing the scope repo is the store's own concern (an out-of-band autocommit), never this command's, so **write the file and run no git command** (🔴 **Store safety**). **Notable — append-worthy** (matches the "Gotchas" spirit + the `MEMORY.md` "durable lesson, not status" bar): - A **gotcha**: non-obvious behavior, a lying/misleading status condition, an ephemeral-vs-durable trap, a wrong-looking-but-correct error string. @@ -88,19 +100,17 @@ Recon stays **read-only by default** — the index is mutated only when a run su - An **incident tie-in**: the recon connected the service to a firing alert / a known `MEMORY.md` slug / a handoff — record the pointer. - A **new pointer** discovered (a `manage-*` skill or slug the index didn't yet reference). -**NOT notable — never append:** -- Routine **healthy** state (pods Ready, canary Succeeded, no events) — live status, belongs nowhere durable. -- Config **values** (replica counts, image tags, env) — re-derived live, never persisted. -- Anything already captured verbatim by a pointer target — add/keep the pointer, don't copy the content. +**NOT notable — never append:** routine healthy state, config values, or anything a pointer target already captures. These are the "Bloat discipline" rules below, applied at the append decision. -**Auto-discovered pointers** (propose in the diff, still confirm-gated — a bad match must be rejectable). Curate the starting set — **propose at most ~5-7 candidates, never a raw match list**; the human still confirms/rejects each in the diff, but a dump is unusable: -- `manage-* skill`: match the service name against skill names/descriptions in `.claude/skills/*/SKILL.md` (e.g. `redis`→`manage-redis`). Skill-name matching is already precise — keep as-is. -- `MEMORY.md slug`: **filename-match first** — propose slugs whose *filename* contains the normalized service token (or one of the index file's `aliases`), e.g. `*redis*.md`; these are the slugs actually ABOUT the service and are the primary signal. **Only if that yields <3**, fall back to content-grep of the memory dir — but **rank candidates by mention count / density and propose only the top few**, never the raw `grep -il` list (naive content-grep is far too broad: `redis` returns ~90 slugs vs ~15 actually redis-centric). -- `claudedocs handoff`: same spirit — **prefer handoff filenames containing the normalized service token**; only density-rank a content-grep fallback if filename-match is too thin, and cap the proposal count. +**Auto-discovered pointers** (propose in the diff, still confirm-gated — a bad match must be rejectable). Curate the starting set: **propose at most ~5-7 candidates, never a raw match list** — a dump is unusable even though the human confirms each: +- `manage-* skill`: match the service name against skill names/descriptions in `.claude/skills/*/SKILL.md` (e.g. `redis`→`manage-redis`). +- `MEMORY.md slug`: **filename-match first** — propose slugs whose *filename* contains the normalized service token (or an `aliases` entry), e.g. `*redis*.md`; those are the slugs actually ABOUT the service. **Only if that yields <3**, fall back to content-grep of the memory dir, but **rank by mention density and propose only the top few**, never the raw `grep -il` list (it is far too broad: `redis` returns ~90 slugs vs ~15 actually redis-centric). +- `claudedocs handoff`: same — **prefer filenames containing the normalized token**; density-rank a content-grep fallback only if that is too thin, and cap the count. **Bloat discipline** (mirrors the `MEMORY.md` memory-hygiene rules): -- **Pointers, not copies** — `## Pointers` is paths/slugs + a one-clause why; domain detail stays in the skill/slug/handoff it points at. -- **NEVER persist live status** — pod counts, Ready/NotReady, canary phase, event tails, current image tag/replica values. Re-derived every run. This is the single most important anti-bloat rule. +- **Pointers, not copies** (schema above) — domain detail stays in the skill/slug/handoff it points at. +- **NEVER persist live status** — pod counts, Ready/NotReady, canary phase, event tails, current image tag/replica values. Re-derived every run — the single most important anti-bloat rule. + - **No live probe ⇒ persist the DERIVATION, not the reading.** For a process/ritual entry ("is this still being followed?") there is no `kubectl` two seconds away — so record *how to take the reading and what a stale one looks like*: "liveness = mtime of the exclusions file vs. the timer's last fire; stale ⇒ mtime predates the last two fires." The method is durable; the answer it gave ("last followed 2026-08-01") is live status exactly like a pod count, and stays forbidden. - **Dated nuance bullets, newest-first, ≤2 lines each.** - **Prune-on-resolve** — when a gotcha is fixed / incident closed / revert superseded, **remove** the bullet (its durable form lives in the slug/handoff it points to). The index is a live pointer sheet, not an append-only log.