Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40405cd95a | ||
|
|
39ac61c411 | ||
|
|
2cb6b54a5e | ||
|
|
1448963823 | ||
|
|
58687c68a6 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills",
|
"name": "skills",
|
||||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||||
"version": "0.15.0",
|
"version": "0.16.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ the SKILL.md, not the table.
|
|||||||
<!-- BEGIN skills-table -->
|
<!-- BEGIN skills-table -->
|
||||||
| Skill | Description | Use when | Surfaces |
|
| Skill | Description | Use when | Surfaces |
|
||||||
|-------|-------------|----------|----------|
|
|-------|-------------|----------|----------|
|
||||||
|
| `adr` | Architecture Decision Records — house format, and how to record a decision you cannot make yet | Recording an architectural decision; revisiting one; or deciding between options where the right answer genuinely is not knowable yet | claude-code, library |
|
||||||
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | claude-code, library |
|
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | claude-code, library |
|
||||||
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present | claude-code, library |
|
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present | claude-code, library |
|
||||||
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code | claude-code, library |
|
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code | claude-code, library |
|
||||||
@@ -21,6 +22,7 @@ the SKILL.md, not the table.
|
|||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
||||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | claude-code, library |
|
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | claude-code, library |
|
||||||
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | claude-code, library, chat |
|
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | claude-code, library, chat |
|
||||||
|
| `lineage` | Backward decision archaeology — was this tried before, and does the reason it failed still hold | Before proposing something that feels new; when an idea keeps resurfacing; when you inherit code whose reason is gone | claude-code, library |
|
||||||
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; \"take this off my plate\"; before a plan exists | claude-code, library, chat |
|
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; \"take this off my plate\"; before a plan exists | claude-code, library, chat |
|
||||||
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
|
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
|
||||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | claude-code, library |
|
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | claude-code, library |
|
||||||
|
|||||||
@@ -61,4 +61,28 @@ if [ -f "$PENDING" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- the ratchet must actually ratchet ---------------------------------------
|
||||||
|
# "This list only shrinks" was asserted in five places and enforced in none. A
|
||||||
|
# traceability probe falsified it in one run: add an unsectioned skill, append it
|
||||||
|
# to the list, gate returns 0. So compare against the list as committed at HEAD —
|
||||||
|
# a convention nothing checks is a convention, and this repo has spent a day on
|
||||||
|
# what those cost.
|
||||||
|
if [ -f "$PENDING" ]; then
|
||||||
|
if ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
echo "WARN: $ROOT is not a git checkout — cannot verify the pending list only shrank" >&2
|
||||||
|
elif ! git -C "$ROOT" cat-file -e HEAD:bin/escalation-pending.txt 2>/dev/null; then
|
||||||
|
: # first commit introducing the file — nothing to compare against yet
|
||||||
|
else
|
||||||
|
before=$(git -C "$ROOT" show HEAD:bin/escalation-pending.txt 2>/dev/null \
|
||||||
|
| sed 's/#.*//' | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
||||||
|
now=$(sed 's/#.*//' "$PENDING" | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
||||||
|
added=$(printf '%s\n' "$now" | grep -vxF "$before" 2>/dev/null || true)
|
||||||
|
for entry in $added; do
|
||||||
|
[ -n "$entry" ] || continue
|
||||||
|
echo "ERROR: '$entry' was added to bin/escalation-pending.txt — the list only shrinks. Write its '## Escalation' section instead of exempting it." >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
exit $fail
|
exit $fail
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
---
|
||||||
|
name: adr
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Architecture Decision Records — house format, and how to record a decision you cannot make yet"
|
||||||
|
use_when: "Recording an architectural decision; revisiting one; or deciding between options where the right answer genuinely is not knowable yet"
|
||||||
|
description: Write an Architecture Decision Record in this estate's format — YAML frontmatter, Status/Context/Decision/Consequences/Validation — including the case where the tradeoff is genuinely context-dependent and forcing a resolution would be worse than preserving the tension. Use when recording or superseding an architectural decision, or when a decision keeps getting re-litigated because nobody wrote down why.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An ADR is the *record* of a decision — the artifact someone reads in eight months when
|
||||||
|
they are about to undo it without knowing why it exists. It does not confer the decision;
|
||||||
|
a person with authority does that, and this skill never is that person.
|
||||||
|
|
||||||
|
Two things make one useful, and neither is the prose quality: a **format consistent
|
||||||
|
enough to grep across repos**, and an honest account of **what was not decided**.
|
||||||
|
|
||||||
|
## Before you write
|
||||||
|
|
||||||
|
Both of these change what goes in the ADR, so they are not cross-references, they are
|
||||||
|
steps:
|
||||||
|
|
||||||
|
1. **`lineage`** — was this decided before and reversed? The reason it was dropped is
|
||||||
|
usually the most valuable sentence in your Context, and if an earlier ADR exists,
|
||||||
|
yours supersedes it rather than ignoring it.
|
||||||
|
2. **`grill-me`** (Full Grill) — find the hole now. It is cheaper here than in the
|
||||||
|
Consequences section of the ADR that supersedes yours.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Any choice where a reasonable alternative existed and was not taken
|
||||||
|
- Superseding or amending an earlier ADR (never edit the old one — see Status)
|
||||||
|
- A decision that keeps getting re-litigated, which means the reasoning was never recorded
|
||||||
|
- A tradeoff that is genuinely context-dependent — see *Preserving a productive tension*
|
||||||
|
|
||||||
|
**Do not use for:** implementation choices with no alternative worth naming, or anything
|
||||||
|
a code comment answers. An ADR per function is an ADR nobody reads.
|
||||||
|
|
||||||
|
## The format
|
||||||
|
|
||||||
|
**Where it goes:** `docs/adr/NNNN-kebab-slug.md`, zero-padded to four digits, numbered
|
||||||
|
sequentially within the repo. Frontmatter you cannot locate is not greppable, and half
|
||||||
|
the value of the format is that `docs/adr/*` is a known path in every repo here.
|
||||||
|
|
||||||
|
**The number is repo-local.** `cad` ADR-0002 and `infra` ADR-0002 are different
|
||||||
|
documents. When referring across repos — including in `supersedes` — qualify it:
|
||||||
|
`cad#0002`. Within a repo the bare number is fine.
|
||||||
|
|
||||||
|
Frontmatter first. It exists so the corpus is machine-readable — "which ADRs does 0004
|
||||||
|
supersede", "what is still `proposed`" — and a bespoke header block, however well
|
||||||
|
written, cannot answer those.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
adr: 0004
|
||||||
|
title: <the decision, as a sentence — not "database choice">
|
||||||
|
status: proposed | accepted | superseded | amended
|
||||||
|
date: 2026-08-29
|
||||||
|
deciders: <who accepted it, by name — or "pending: <name>" while proposed>
|
||||||
|
supersedes: <adr number, or ->
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR-0004 — <same title>
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**The frontmatter is authoritative for status and date.** This section exists for the one
|
||||||
|
thing the frontmatter cannot hold: *why* it moved — what superseded it and on what
|
||||||
|
evidence, or what accepting it was contingent on. If it would only restate the YAML,
|
||||||
|
delete it.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The forces. What is true that makes this a decision rather than a default — constraints,
|
||||||
|
the failure that prompted it, what was tried. Name the unknowns explicitly; a decision
|
||||||
|
made under uncertainty is not weaker for saying so, it is auditable.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
One sentence of what was decided, then why *this* option rather than the named
|
||||||
|
alternatives. Address the strongest case against, not the weakest.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:** what this buys.
|
||||||
|
|
||||||
|
**Negative / watch:** what it costs, and the specific thing to watch that would tell you
|
||||||
|
it was wrong. "Some added complexity" is not a consequence; "the event schema is now the
|
||||||
|
coupling surface between cad and every executor — version it deliberately" is.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
How you will know. A link to the experiment or the metric that proves or kills it, and
|
||||||
|
the condition that fires a revisit. An ADR with no validation section is a preference
|
||||||
|
with a template around it.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preserving a productive tension
|
||||||
|
|
||||||
|
Some tradeoffs are not unresolved because nobody thought hard enough. They are
|
||||||
|
context-dependent, the context is not settled, and picking now means picking blind.
|
||||||
|
Forcing resolution there produces a confident record that is wrong half the time.
|
||||||
|
|
||||||
|
Record the tension instead, in the Decision section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Provisional: **<option A>**, on this reasoning: <what makes it safe to pick now>.
|
||||||
|
|
||||||
|
<The strongest case for the option you did NOT take, and why it does not win today. The
|
||||||
|
plain format demands this and a preserved tension does not get an exemption — the whole
|
||||||
|
point is that the loser is still live.>
|
||||||
|
|
||||||
|
### The tension, preserved
|
||||||
|
|
||||||
|
- **Option A** — <what it assumes, what it costs>
|
||||||
|
- **Option B** — <what it assumes, what it costs>
|
||||||
|
|
||||||
|
**What we do not know:** <the variable that decides it>
|
||||||
|
|
||||||
|
**Preservation strategy:** <what we build now so BOTH stay reachable. This is the
|
||||||
|
technical content of the ADR, not a one-liner — enumerate it. It is usually the part both
|
||||||
|
options need anyway, which is often most of the work: the schema, the interface seam, the
|
||||||
|
idempotency key. Name each piece and say which option it keeps alive.>
|
||||||
|
|
||||||
|
**Resolution triggers:** <plural, and one per unknown. Two independent unknowns do not
|
||||||
|
collapse into one condition, and a trigger may exist that neither unknown implies.>
|
||||||
|
```
|
||||||
|
|
||||||
|
Two rules keep this from becoming an excuse:
|
||||||
|
|
||||||
|
1. **A preserved tension still ships a decision.** "We will decide later" is not one.
|
||||||
|
Prefer the option whose correctness does not depend on the unknown, say so, and mark
|
||||||
|
it provisional.
|
||||||
|
|
||||||
|
**When no such option exists** — and often none does, because a tension is productive
|
||||||
|
precisely when both options are hostage to the same unknown — do not manufacture a
|
||||||
|
reason that one of them is. That reads as reasoned and will not be re-examined, which
|
||||||
|
makes it worse than an honest arbitrary pick. Fall back, in order: the option that is
|
||||||
|
cheaper to reverse; the one with the lower switching cost given your preservation
|
||||||
|
strategy; the one already built. Then **say that is why you picked it.** "Chosen
|
||||||
|
because it is the cheaper mistake" is a real justification and an auditable one.
|
||||||
|
2. **The resolution trigger must be observable by someone who is not you.** "When it
|
||||||
|
becomes a problem" is not a trigger. "When executor count passes five and they are all
|
||||||
|
long-lived services in one cluster" is. A trigger nobody can evaluate is a decision
|
||||||
|
that quietly becomes permanent.
|
||||||
|
|
||||||
|
`cad`'s ADR-0001 does this informally under *"Known refinements (recorded, deliberately
|
||||||
|
not built yet)"* — sovereignty deferred to a 3-class model *"until a client actually opts
|
||||||
|
into an EU provider"*. That clause is the pattern: option, cost, and the event that
|
||||||
|
forces the choice.
|
||||||
|
|
||||||
|
## Status discipline
|
||||||
|
|
||||||
|
- **Never edit an accepted ADR to change its decision.** Write a new one and set
|
||||||
|
`supersedes`. The record of what was believed, and when, is the whole value.
|
||||||
|
- **Amend** for corrections that do not change the decision (a broken link, a clarified
|
||||||
|
consequence). Note the amendment inline with its date.
|
||||||
|
- **`proposed` is not a parking space.** An ADR that sits proposed for a month *with no
|
||||||
|
named decider* is a decision nobody is making; either accept it provisionally or close
|
||||||
|
it as declined. With a named decider it is a decision waiting on a person, which is a
|
||||||
|
different thing and is chased rather than closed.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Anti-pattern | Why it fails |
|
||||||
|
|---|---|
|
||||||
|
| Consequences that are all positive | Then it was not a decision, it was an announcement |
|
||||||
|
| "We chose X because it is better" | Names no alternative and no force; unfalsifiable |
|
||||||
|
| Written after the code shipped | Reverse-engineers a justification. Record the reasoning while it is still contested |
|
||||||
|
| One ADR amended repeatedly | Destroys the history the format exists to keep |
|
||||||
|
| No Validation section | Nothing distinguishes a good decision from a lucky one |
|
||||||
|
| A tension "resolved" by picking arbitrarily | The next person re-litigates it, having lost the reasoning |
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Writes a file, so it acts — but it decides nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the deciders are not you — which, for an agent, is always. An
|
||||||
|
ADR records a decision someone with authority made; writing one to *create* that
|
||||||
|
authority is how a preference acquires a document number.
|
||||||
|
|
||||||
|
So the deliverable is a draft marked `proposed` with **the person who must accept it named
|
||||||
|
in `deciders` as `pending: <name>`**, and that hand-back stated in the response, not left
|
||||||
|
implied. Naming the decider is the escalation, not a formality — an unnamed one is how a
|
||||||
|
proposal sits for a month.
|
||||||
|
|
||||||
|
That is the intended state for an agent-authored ADR, and it is not the parking-space
|
||||||
|
failure below: a `proposed` ADR with a named decider is waiting on someone; one without is
|
||||||
|
waiting on nobody.
|
||||||
|
|
||||||
|
Also stop when the Context cannot be written without inventing a constraint. A missing
|
||||||
|
force means the decision has not been thought through yet, and an ADR built on an
|
||||||
|
invented one is worse than none: it will be cited.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- `lineage` and `grill-me` — see *Before you write*; they change the content, so they are
|
||||||
|
steps rather than references.
|
||||||
|
- `spec-driven-dev` / `feature-spec` — an ADR is the *why*; a spec is the *what*. If you
|
||||||
|
are describing behaviour, you are writing a spec.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
name: lineage
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Backward decision archaeology — was this tried before, and does the reason it failed still hold"
|
||||||
|
use_when: "Before proposing something that feels new; when an idea keeps resurfacing; when you inherit code whose reason is gone"
|
||||||
|
description: Backward decision archaeology. Find out whether an idea was already tried here, why it was dropped, and whether that reason still applies — using git history, decision records and the issue tracker rather than asking whoever is still around. Use before proposing a change that feels novel, when inheriting unexplained code, or when the same idea keeps being re-litigated.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lineage
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Most "new" ideas in an established codebase are revivals. The valuable question is never
|
||||||
|
"is this a good idea" in the abstract — it is **"was this tried, what killed it, and is
|
||||||
|
the killer still alive?"**
|
||||||
|
|
||||||
|
A past failure is not evidence against trying again. It is context for trying again
|
||||||
|
differently, and the difference between those two readings is usually one commit message.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Before proposing something that feels new in a codebase older than you
|
||||||
|
- When an idea keeps resurfacing and being re-argued from scratch
|
||||||
|
- When you inherit code and cannot tell whether a constraint is load-bearing or vestigial
|
||||||
|
- Before writing an ADR — the reason a thing was dropped belongs in your Context section
|
||||||
|
|
||||||
|
**Do not use for:** code you wrote this week, or a question the current code answers.
|
||||||
|
Archaeology on a live construction site is just reading.
|
||||||
|
|
||||||
|
## The tool
|
||||||
|
|
||||||
|
```sh
|
||||||
|
skills/lineage/lineage.sh <term> [repo-path]
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs the searches people skip because they are fiddly to type. In particular
|
||||||
|
`git log -S<term>`, which finds commits where the term's **occurrence count changed** —
|
||||||
|
so it surfaces the *removal* as well as the introduction. A plain `--grep` never finds a
|
||||||
|
deletion, and the deletion is the whole point.
|
||||||
|
|
||||||
|
It also lists files that once carried the term and no longer exist, and names the two
|
||||||
|
sources it cannot search for you.
|
||||||
|
|
||||||
|
**A no-match is not a finding.** The first version of this tool said a null meant "this is
|
||||||
|
new here"; a pressure test caught it asserting exactly that about the most-discussed
|
||||||
|
removed feature in this repo — the search term merely had a space in it. One string
|
||||||
|
against one repo proves nothing. Try at least three variants (the identifier name, a
|
||||||
|
one-word form, the name it would have had) and the brain and tracker searches before
|
||||||
|
telling anyone an idea is novel. This is the same rule as the anti-patterns table below,
|
||||||
|
and it is there because the tool broke it.
|
||||||
|
|
||||||
|
Note also that `--grep` takes a regex and `-S` takes a literal string, so a term
|
||||||
|
containing `.`, `[`, `*` or `+` matches differently in the two halves of the output.
|
||||||
|
|
||||||
|
## The sequence
|
||||||
|
|
||||||
|
**1. Search on the identifier, not the concept.** If the thing had a name in the code —
|
||||||
|
a function, a flag, a config key — `git log --all -S<name>` typically returns exactly two
|
||||||
|
commits, its birth and its death, on one screen. That is the fastest path to the answer in
|
||||||
|
this whole sequence.
|
||||||
|
|
||||||
|
Path-scoped log (`git log --all -- <file>`) is the weaker move and it is the one people
|
||||||
|
reach for: a function deleted from a file that still exists is the *dominant* removal
|
||||||
|
shape, and the path search buries it under every other commit that touched the file.
|
||||||
|
|
||||||
|
Concept words are the fallback, and they are lossy — a term with a space in it will miss
|
||||||
|
the record entirely. Try several spellings before you conclude anything.
|
||||||
|
|
||||||
|
**2. Read the removal commit's full body, not its subject.**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git log -1 --format=%B <sha>
|
||||||
|
git show --stat <sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
This is where the answer almost always is. A subject line says *what*; the body says
|
||||||
|
*why*, and the why is what you came for. Read the diff too — what was removed alongside
|
||||||
|
it tells you how load-bearing it was.
|
||||||
|
|
||||||
|
**3. Search outside git — that is where this estate keeps its reasoning.** Two sources,
|
||||||
|
both routinely decisive, neither reachable from a commit log:
|
||||||
|
|
||||||
|
- **The brain** (`brain_query`). Postmortems and generalized rules live there. In the run
|
||||||
|
that shaped this skill, the single best artifact — the rule abstracted from the incident,
|
||||||
|
with detection one-liners — came from the brain and from nowhere else.
|
||||||
|
- **The issue tracker, including comments.** The commit records *that* something was
|
||||||
|
removed; the argument for it is usually in a comment. Search closed issues especially —
|
||||||
|
a closed issue is where a decision usually is.
|
||||||
|
|
||||||
|
A git-only search finds the what and misses the why.
|
||||||
|
|
||||||
|
**4. Test whether the failure conditions still hold — empirically, not from the record.**
|
||||||
|
This is the step that makes the whole exercise worth doing, and it is the one that gets
|
||||||
|
hand-waved. Write the conditions down as separate claims, then **run a command per claim**:
|
||||||
|
|
||||||
|
| Claim from the record | How you check it today |
|
||||||
|
|---|---|
|
||||||
|
| "absolute links break across two path roots" | `readlink` one, `test -e` it on this host |
|
||||||
|
| "it is gitignored, so nobody sees the rot" | `grep` the ignore file |
|
||||||
|
| "the detector does not cover it" | `grep` the detector for the path it scans |
|
||||||
|
|
||||||
|
Twenty seconds each, and it converts *"the record claims X"* into *"X is true right now"* —
|
||||||
|
which is the only form of the answer worth giving. A constraint that has since been
|
||||||
|
removed turns a dead idea live; one that is still true kills a revival on the spot.
|
||||||
|
|
||||||
|
**5. Separate the mechanism from the goal.** A revival proposal usually bundles both, and
|
||||||
|
they can have opposite verdicts: the goal is still worth wanting, the mechanism is the one
|
||||||
|
that failed. Splitting them is often the whole finding — you end up agreeing with the
|
||||||
|
proposer and rejecting their implementation, which is a far more useful answer than yes
|
||||||
|
or no.
|
||||||
|
|
||||||
|
## What "it was tried and failed" can mean
|
||||||
|
|
||||||
|
| Reading | Tell | What to do |
|
||||||
|
|---|---|---|
|
||||||
|
| **Structurally impossible** | The removal commit says *why* it cannot work, not that it broke | Believe it. Look for a different mechanism for the same goal |
|
||||||
|
| **Failed under conditions that changed** | The blocker names a version, a host, a tool that has since moved | Re-open it, citing what changed |
|
||||||
|
| **Failed on execution** | It broke, was reverted, nobody diagnosed it | Genuinely undecided. The revival is legitimate; budget for the diagnosis |
|
||||||
|
| **Never actually tried** | Discussed, an issue exists, no commit | There is no evidence either way. Say so rather than inheriting someone's pessimism |
|
||||||
|
| **Still there, in a different shape** | The idea returned under another name | The most missable one — check before you build it twice |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Answer the question that was asked, then show your work:
|
||||||
|
|
||||||
|
- **Verdict** — tried / not tried / tried and later restored differently
|
||||||
|
- **The decisive artifact** — commit sha, issue number, or ADR, so the reader can check
|
||||||
|
- **What killed it**, quoted from the source rather than paraphrased
|
||||||
|
- **Whether that still applies**, per condition, not as a single yes or no
|
||||||
|
|
||||||
|
If the decisive evidence is one commit message, quote it. A conclusion that makes the
|
||||||
|
reader re-run your search has not saved them anything.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Anti-pattern | Why it fails |
|
||||||
|
|---|---|
|
||||||
|
| `--grep` only | Finds discussion, misses deletions. The deletion is the evidence |
|
||||||
|
| Reading the commit subject and stopping | Subjects say what; you need why |
|
||||||
|
| Treating a past failure as a verdict | Conditions change. That is the entire question |
|
||||||
|
| Git-only search | The argument is usually in the issue tracker |
|
||||||
|
| "It was tried and it failed" with no sha | Unverifiable, and it will be re-litigated next quarter |
|
||||||
|
| Assuming absence means it was never considered | It may have been rejected in a conversation. Say what you searched |
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only. Reports what the record says; changes nothing and revives nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the record is genuinely silent — no commit, no issue, no ADR.
|
||||||
|
State what you searched and that nothing exists, rather than inferring intent from
|
||||||
|
absence. Someone deciding not to build a thing and nobody ever considering it look
|
||||||
|
identical in git, and only one of them is an argument.
|
||||||
|
|
||||||
|
Also stop when the answer turns on why a *person* did something and the record does not
|
||||||
|
say. Ask them. Reconstructing a colleague's reasoning from a diff is how a guess acquires
|
||||||
|
a citation.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- `adr` — what you find here belongs in the Context section of the ADR you are about to
|
||||||
|
write. If the thing was decided before, your ADR supersedes that one; say so.
|
||||||
|
- `debug` — the same discipline aimed at a failure rather than a decision: evidence
|
||||||
|
before hypothesis, and the removal commit is evidence.
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# lineage.sh — decision archaeology for one term.
|
||||||
|
#
|
||||||
|
# Answers "why does X exist, was it tried before, and is this 'new' idea a
|
||||||
|
# revival?"
|
||||||
|
#
|
||||||
|
# Its real value is that it makes you run `git log -S` at all — that is a nudge,
|
||||||
|
# not a capability. -S finds commits where the term's occurrence COUNT changed,
|
||||||
|
# so it surfaces a removal; --grep only finds people talking about one. On an
|
||||||
|
# identifier name (`wire_claude_repo`) -S typically returns exactly two commits,
|
||||||
|
# birth and death, which is the fastest path to the answer in this whole file.
|
||||||
|
#
|
||||||
|
# Note the two searches do not match the same way: --grep takes a regex, -S takes
|
||||||
|
# a literal string. A term containing . [ * + behaves differently in each.
|
||||||
|
#
|
||||||
|
# Usage: lineage.sh <term> [repo-path]
|
||||||
|
#
|
||||||
|
# Read-only: nothing here writes, and it never touches the index or worktree.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
[ $# -ge 1 ] || { echo "usage: lineage.sh <term> [repo-path]" >&2; exit 2; }
|
||||||
|
TERM_=$1
|
||||||
|
REPO=${2:-.}
|
||||||
|
|
||||||
|
git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1 || {
|
||||||
|
echo "lineage: $REPO is not a git repository" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Say which repo, always. The default is the cwd, and in a worktree-heavy setup
|
||||||
|
# that is routinely not the repo the caller means — a confident null against the
|
||||||
|
# wrong tree is the worst thing this tool can produce.
|
||||||
|
TOPLEVEL=$(git -C "$REPO" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$REPO")
|
||||||
|
printf 'searching: %s term: "%s"\n' "$TOPLEVEL" "$TERM_"
|
||||||
|
|
||||||
|
FMT='%h %ad %an %s'
|
||||||
|
DATE='--date=short'
|
||||||
|
found=0
|
||||||
|
|
||||||
|
section() { printf '\n== %s ==\n' "$1"; }
|
||||||
|
|
||||||
|
# 1. Commits whose MESSAGE mentions it — the stated reasoning.
|
||||||
|
msgs=$(git -C "$REPO" log --all -i --grep="$TERM_" --format="$FMT" $DATE 2>/dev/null || true)
|
||||||
|
if [ -n "$msgs" ]; then
|
||||||
|
found=1
|
||||||
|
section "Commits that talk about it (message)"
|
||||||
|
printf '%s\n' "$msgs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Commits where the term was ADDED or REMOVED — the actual history.
|
||||||
|
# -S counts occurrences, so a deletion shows up here and nowhere else.
|
||||||
|
picks=$(git -C "$REPO" log --all -S"$TERM_" --format="$FMT" $DATE 2>/dev/null || true)
|
||||||
|
if [ -n "$picks" ]; then
|
||||||
|
found=1
|
||||||
|
section "Commits where it appeared or disappeared (-S)"
|
||||||
|
printf '%s\n' "$picks"
|
||||||
|
printf '\n Oldest is the introduction. Anything after it that removed the term is\n'
|
||||||
|
printf ' the interesting one: read that commit before proposing this again.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Files that once contained it and no longer exist.
|
||||||
|
deleted=$(git -C "$REPO" log --all --diff-filter=D --name-only --format='%h' -S"$TERM_" 2>/dev/null \
|
||||||
|
| grep -v '^[0-9a-f]\{7,\}$' | grep -v '^$' | sort -u || true)
|
||||||
|
if [ -n "$deleted" ]; then
|
||||||
|
found=1
|
||||||
|
section "Files deleted that carried it"
|
||||||
|
printf '%s\n' "$deleted"
|
||||||
|
printf '\n Recover one with: git -C %s show <sha>^:<path>\n' "$REPO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Decision records naming it.
|
||||||
|
docs=$(git -C "$REPO" grep -l -i "$TERM_" -- 'docs/adr/*' 'docs/decisions/*' 'docs/*.md' 'ADR*' 2>/dev/null || true)
|
||||||
|
if [ -n "$docs" ]; then
|
||||||
|
found=1
|
||||||
|
section "Decision records naming it"
|
||||||
|
printf '%s\n' "$docs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This estate keeps most of its reasoning OUTSIDE the repo, so the two searches
|
||||||
|
# this tool cannot run are usually the decisive ones. Say so every time rather
|
||||||
|
# than letting their absence read as a null result.
|
||||||
|
section "Not searched here — do these by hand"
|
||||||
|
printf 'brain: brain_query "%s" — decisions and postmortems live there, not in docs/\n' "$TERM_"
|
||||||
|
printf 'gitea: closed issues and their COMMENTS. The argument for a removal is\n'
|
||||||
|
printf ' usually in a comment; the commit only records that it happened.\n'
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
printf '\nno match for the exact string "%s" in %s.\n' "$TERM_" "$TOPLEVEL"
|
||||||
|
printf 'This is NOT evidence the idea is new. It is one string against one repo.\n'
|
||||||
|
printf 'Before concluding anything, try: the identifier or function name; a\n'
|
||||||
|
printf 'one-word variant (spaces and punctuation break the match); the name it\n'
|
||||||
|
printf 'would have had; and the brain and issue searches above.\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n-- what this cannot tell you --------------------------------------------\n'
|
||||||
|
printf 'Why it was removed, if it was. The commit subject is a summary; read the\n'
|
||||||
|
printf 'body, the PR, and the issue it closed. A past failure under conditions that\n'
|
||||||
|
printf 'no longer hold is not evidence against trying again — it is context for\n'
|
||||||
|
printf 'trying again differently.\n'
|
||||||
@@ -81,5 +81,47 @@ f=$(fixture comments); skill "$f" alpha no
|
|||||||
printf '# a comment, and a blank line follow\n\nalpha\n' > "$f/bin/escalation-pending.txt"
|
printf '# a comment, and a blank line follow\n\nalpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
expect 0 'comments and blank lines in the list are ignored' "$f"
|
expect 0 'comments and blank lines in the list are ignored' "$f"
|
||||||
|
|
||||||
|
# --- the ratchet must actually ratchet ---------------------------------------
|
||||||
|
# Five places in this repo claimed the pending list "only shrinks" and nothing
|
||||||
|
# enforced it. A traceability probe falsified that in one run: add an unsectioned
|
||||||
|
# skill, append it to the list, gate returns 0. A convention stated five times is
|
||||||
|
# still a convention.
|
||||||
|
#
|
||||||
|
# Enforcement compares against the list as committed at HEAD, so the fixtures
|
||||||
|
# below need a real git repo.
|
||||||
|
|
||||||
|
git_fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills" "$_f/bin"
|
||||||
|
( cd "$_f" && git init -q . && git config user.email t@example.com && git config user.name test )
|
||||||
|
skill "$_f" alpha no
|
||||||
|
printf 'alpha\n' > "$_f/bin/escalation-pending.txt"
|
||||||
|
( cd "$_f" && git add -A && git commit -qm baseline )
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_same)
|
||||||
|
expect 0 'an unchanged list passes' "$f"
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_shrink)
|
||||||
|
skill "$f" alpha yes
|
||||||
|
: > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 0 'removing an entry (and writing its section) passes' "$f"
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_grow)
|
||||||
|
skill "$f" beta no
|
||||||
|
printf 'alpha\nbeta\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 1 'adding a NEW entry fails — the list only shrinks' "$f" 'beta'
|
||||||
|
|
||||||
|
# Outside a git repo the comparison is impossible; that must not be silent.
|
||||||
|
f=$(fixture ratchet_nogit); skill "$f" alpha no
|
||||||
|
printf 'alpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
_out=$(sh "$SUT" "$f" 2>&1)
|
||||||
|
if printf '%s' "$_out" | grep -qi 'cannot verify'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok an unverifiable ratchet says so rather than passing silently\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL an unverifiable ratchet says so rather than passing silently\n%s\n' "$_out"
|
||||||
|
fi
|
||||||
|
|
||||||
printf '\ncheck_escalation: %d passed, %d failed\n' "$pass" "$fail"
|
printf '\ncheck_escalation: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
[ "$fail" -eq 0 ]
|
[ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for skills/lineage/lineage.sh — the decision-archaeology helper.
|
||||||
|
#
|
||||||
|
# The techniques this automates are the ones people skip because they are
|
||||||
|
# fiddly to type: `git log -S` to find when a thing was introduced AND removed,
|
||||||
|
# --diff-filter=D to find files that once existed, and grepping decision records
|
||||||
|
# for the term. A skill that only *describes* those is a skill that gets read and
|
||||||
|
# not run (#11, #19's "small robust tools over prose").
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/skills/lineage/lineage.sh"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/lineage_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# A repo where a thing was built, written about, and then removed.
|
||||||
|
mkdir -p "$WORK/repo/docs/adr"
|
||||||
|
cd "$WORK/repo" || exit 1
|
||||||
|
git init -q .
|
||||||
|
git config user.email t@example.com
|
||||||
|
git config user.name test
|
||||||
|
|
||||||
|
printf 'func WidgetCache() {}\n' > cache.go
|
||||||
|
git add -A && git commit -qm "feat: add WidgetCache for lookup speed"
|
||||||
|
|
||||||
|
printf -- '---\nadr: 0001\ntitle: use a WidgetCache\n---\nWe chose WidgetCache over a database index.\n' > docs/adr/0001-widgetcache.md
|
||||||
|
git add -A && git commit -qm "docs: record the WidgetCache decision"
|
||||||
|
|
||||||
|
git rm -q cache.go
|
||||||
|
git commit -qm "revert: drop WidgetCache, it went stale under concurrent writes"
|
||||||
|
|
||||||
|
cd - >/dev/null || exit 1
|
||||||
|
out=$(sh "$SUT" WidgetCache "$WORK/repo" 2>&1)
|
||||||
|
|
||||||
|
check() {
|
||||||
|
if printf '%s' "$out" | grep -q "$1"; then
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$2"
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s\n output had no %s\n%s\n' "$2" "$1" "$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check 'add WidgetCache' 'finds the commit that introduced it'
|
||||||
|
check 'drop WidgetCache' 'finds the commit that removed it — the revival question'
|
||||||
|
check '0001-widgetcache.md' 'finds the decision record naming it'
|
||||||
|
check 'cache.go' 'names the file that no longer exists'
|
||||||
|
|
||||||
|
# A no-match must NOT be reported as evidence the idea is new. The first version
|
||||||
|
# of this tool said "it means this is new here" and a pressure test caught it
|
||||||
|
# asserting exactly that about the most-discussed removed feature in this repo —
|
||||||
|
# the search term simply had a space in it. That is the inference the skill's own
|
||||||
|
# anti-pattern table forbids, printed in the tool's voice.
|
||||||
|
out=$(sh "$SUT" NeverExisted "$WORK/repo" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q 'NOT evidence the idea is new' \
|
||||||
|
&& printf '%s' "$out" | grep -qi 'identifier'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok a no-match refuses to imply novelty and suggests other terms\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL a no-match refuses to imply novelty and suggests other terms\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The repo actually searched is always stated — a confident null against the
|
||||||
|
# wrong tree is this tool's worst failure mode, and the default is the cwd.
|
||||||
|
out=$(sh "$SUT" WidgetCache "$WORK/repo" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q "searching: "; then
|
||||||
|
pass=$((pass + 1)); printf ' ok names the repo it searched\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL names the repo it searched\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The sources it cannot reach are named every run, so their absence never reads
|
||||||
|
# as a null result.
|
||||||
|
if printf '%s' "$out" | grep -q 'brain_query' && printf '%s' "$out" | grep -qi 'closed issues'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok names the sources it cannot search\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL names the sources it cannot search\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nlineage: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Reference in New Issue
Block a user