From aebbbe0e35957f33236bced3b3207adbf4af8613 Mon Sep 17 00:00:00 2001 From: Zach Lowden Date: Thu, 6 Aug 2026 22:00:13 -0500 Subject: [PATCH 1/2] fix(analyze-service): an alias could silently shadow a real index entry, and an unmarked client-sensitive file read as publishable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four surgical repairs to the `/analyze-service` index schema, from an experiment that hand-authored a `process` entry and an `org` entry in the current schema to find where it breaks. The larger "generalize into subsystems" proposal was REJECTED on that evidence — no `type:`-driven sections, no dependency graph, no CLI. Spec/prose only; no code, no behaviour outside the command. 1. Addressing — the shadowing bug. Two defects were conflated. A slug can name two different KINDS of thing (`devrc/repo-cos` is both a code subsystem and the weekly ritual about it), and the old resolver matched filename *and* `aliases:` in one breath — so an alias silently won and a later real entry became unreachable, with no error. Now: an optional `..md` filename used ONLY when disambiguation is needed; a two-tier resolver where an alias can never outrank a filename match; and >1 hit in a tier is a hard ambiguity ERROR that lists candidates instead of picking one. Bare `.md` resolves exactly as before — backward compatibility is stated as a hard requirement, and none of the 20 existing files is renamed or resolves differently. Also folds `_` -> `-` during normalization (the old rule kept `_`, so `image_ingestion` != `image-ingestion` while the index links `_`-spelled MEMORY.md slugs), keeping the pre-fold spelling in `aliases:`. 2. Sensitivity marking. New `sensitivity:` front-matter field, `client-confidential | personal | public`, with a FAIL-SAFE default: absent or unrecognized means client-confidential, never public. Not hypothetical — live entries carry a client bastion's public IP and SSH port, client hostnames, and a named client engineer. The no-remote / never-copy-into-a- public-repo rule is written down alongside it, pointing at 60e6d9d, which exists because this exact data class had to be scrubbed retroactively. Marking only; enforcement lands separately. 3. Front-matter repair. `repo:` -> `scope:`, and a scope may be a non-repo word (a ritual is owned by no repo; a client spans several). `` derivation stays the default so nothing breaks; older files carrying `repo:` still read as `scope`. `namespace:` becomes OPTIONAL rather than removed — genuinely load-bearing for k8s infra, meaningless for the non-infra entries that were writing `n/a`. 4. Process-liveness convention. "Is this still being followed?" is THE question for a process subsystem, but the anti-bloat rule forbids persisting live status — correctly, for infra, where a kubectl is two seconds away. For a ritual there is no live probe. Convention: persist the DERIVATION METHOD and the stale-tell, never the current reading. Written as an application of the existing rule, not an exception to it. Byte budget: 12,460 -> 15,358 (+2,898). Offsetting evictions in the same commit — the recon-start index-read paragraph was stated twice nearly verbatim (kept the operational copy in step 1), the "Always re-derived live" tags on Lives at/Config duplicated the Provenance-honesty line below them, and the "Pointers, not copies" bullet restated the schema it sits under. Verification: the public-IP and client-hostname gates pass on this file, and were negative-controlled — injecting a routable IP into it makes test_no_public_ips fail with this exact path, so the green is a real read of this file rather than a scan that misses it. The full pytest suite fails identically (same 26, same names) at this branch and at the untouched base checkout, so nothing here is a regression; those 26 are ad-hoc-nix-shell toolchain gaps, not code. Scope note: deliberately does NOT touch scripts/analyze-service-index/, nix/home.nix or scripts/claude-hooks/ — PR #361 owns those and this file is not in its diff, so the two do not overlap. Co-Authored-By: Claude Opus 5 (1M context) --- claude/commands/analyze-service.md | 33 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/claude/commands/analyze-service.md b/claude/commands/analyze-service.md index dac95624..31a35df5 100644 --- a/claude/commands/analyze-service.md +++ b/claude/commands/analyze-service.md @@ -21,15 +21,21 @@ 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. +Alongside the live recon, `/analyze-service` keeps a **local markdown pointer/nuance sheet per service, outside every repo you work in**, 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 store under the user's home `.claude`; it never lands inside a cluster repo or `devrc`. +- **``** 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. Repo-derived is the default; a non-repo scope is a deliberate choice — 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` is the default and resolves exactly as today — backward compatibility is a hard requirement: no existing file is renamed, and a scope with no qualified filename behaves identically to 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 hit in a tier → never pick: stop, call the ref ambiguous, list the candidate filenames** (`repo-cos.md` vs `repo-cos.process.md`) and let the user choose. Zero in both → 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)"). **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 — human note; **replaces `repo:`**, which older files still carry and which reads as `scope`), `sensitivity` (below), `namespace` (**optional** — keep it where it's load-bearing k8s infra, `multiple` for umbrella services; **omit it rather than writing `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 a client bastion's public IP + SSH port, client hostnames, and a named client engineer. So the store **must never gain a git remote**, and **no line of it may be copied into `devrc` (PUBLIC) or any other public repo, PR body, issue, gist or commit message** — devrc commit `60e6d9d` exists because this exact data class had to be scrubbed out of a public repo retroactively. This spec **marks**; enforcement is separate. - **`## 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 +43,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). - 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, a miss just proceeds. 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 +64,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**. +- **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 +86,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 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). **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. @@ -99,8 +105,9 @@ Recon stays **read-only by default** — the index is mutated only when a run su - `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. **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. +- **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. This is the single most important anti-bloat rule. + - **No live probe ⇒ persist the DERIVATION, not the reading.** For a process/ritual entry the load-bearing question is "is this still being followed?", and 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. Same rule, applied where the probe is a method rather than a command — not an exception to it. - **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. From 9404ab4739fa3a784ce9607c902bfe9e97f78eca Mon Sep 17 00:00:00 2001 From: Zach Lowden Date: Thu, 6 Aug 2026 22:21:59 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix(analyze-service):=20the=20store=20is=20?= =?UTF-8?q?a=20git=20repo=20now=20=E2=80=94=20"there=20is=20no=20commit=20?= =?UTF-8?q?step"=20invited=20destructive=20git=20into=20an=20irreplaceable?= =?UTF-8?q?,=20unbacked-up=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claude/commands/analyze-service.md` is the protocol an agent reads *before* writing to `~/.claude/analyze-service-index/`. That store is now one git repo per scope (verified: `/` is a repo on `trunk`, no remote; the store root is not a repo). Three statements in the file predated that. Two were reworded earlier in this PR; the dangerous one survived: - "The write is local and final; there is no commit/worktree step (the file is outside every repo)." + "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)." An agent told "outside every repo" has no reason to check for one before reaching for the stash stack, a hard reset, a clean, or a checkout-- inside a store that is curated, irreplaceable, unbacked-up and client-sensitive. `refs/stash` is repo-global and concurrent sessions share it. Per RULES.md, a comment is a claim too — a false safety comment is what leads a maintainer to delete the guard it describes. Also fixed, same staleness class, found by sweeping the rest of the file: - The earlier rewording ("outside every repo you work in" / "never lands inside a cluster repo or devrc") was true but still only said where the store ISN'T, so it left the same inference intact. The Location bullet now states positively that each scope is its own remote-less git repo. - "Lazy — nothing pre-created. ~/.claude/analyze-service-index/ may not exist" — the store root exists today; scoped to "a scope dir or service file may not exist yet". Adds a Store safety block mirroring the scope README's wording (the four destructive git operations, no remote, no push, no copying into a public repo) so the two agree rather than drift. Wording is deliberately true both before and after the autocommit timer lands — that timer is NOT deployed yet (`analyze-service-index-commit.timer` does not exist on this host), so the file asserts a design, not a deployed state. Byte discipline: this file loads on every run and had grown +2,898 (+23%) in this PR. Net this round: 15,358 -> 15,349 (-9). Paid for by evicting the "NOT notable" list (a verbatim restatement of "Bloat discipline" — one rule, one place), folding the sensitivity bullet's handling rules into the new Store safety block, and compressing prose without dropping a rule. Nix-managed: takes effect only after `home-manager switch`. Co-Authored-By: Claude Opus 5 (1M context) --- claude/commands/analyze-service.md | 47 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/claude/commands/analyze-service.md b/claude/commands/analyze-service.md index 31a35df5..8b69dc0c 100644 --- a/claude/commands/analyze-service.md +++ b/claude/commands/analyze-service.md @@ -21,21 +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 markdown pointer/nuance sheet per service, outside every repo you work in**, 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. +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` — a LOCAL store under the user's home `.claude`; it never lands inside a cluster repo or `devrc`. -- **``** 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. Repo-derived is the default; a non-repo scope is a deliberate choice — say so in the brief. +- **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` is the default and resolves exactly as today — backward compatibility is a hard requirement: no existing file is renamed, and a scope with no qualified filename behaves identically to before.** +- **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 hit in a tier → never pick: stop, call the ref ambiguous, list the candidate filenames** (`repo-cos.md` vs `repo-cos.process.md`) and let the user choose. Zero in both → 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)"). + 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 + 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 — human note; **replaces `repo:`**, which older files still carry and which reads as `scope`), `sensitivity` (below), `namespace` (**optional** — keep it where it's load-bearing k8s infra, `multiple` for umbrella services; **omit it rather than writing `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 a client bastion's public IP + SSH port, client hostnames, and a named client engineer. So the store **must never gain a git remote**, and **no line of it may be copied into `devrc` (PUBLIC) or any other public repo, PR body, issue, gist or commit message** — devrc commit `60e6d9d` exists because this exact data class had to be scrubbed out of a public repo retroactively. This spec **marks**; enforcement is separate. +- **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. @@ -47,9 +53,9 @@ Alongside the live recon, `/analyze-service` keeps a **local markdown pointer/nu ## 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 `` 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, 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. @@ -67,7 +73,7 @@ Alongside the live recon, `/analyze-service` keeps a **local markdown pointer/nu 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. +- **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)". @@ -86,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; use `..md` **only** when a same-slug entry of another kind already exists, and say why in the diff). 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. @@ -94,20 +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** (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. This is the single most important anti-bloat rule. - - **No live probe ⇒ persist the DERIVATION, not the reading.** For a process/ritual entry the load-bearing question is "is this still being followed?", and 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. Same rule, applied where the probe is a method rather than a command — not an exception to it. +- **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.