Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40405cd95a | ||
|
|
39ac61c411 | ||
|
|
2cb6b54a5e | ||
|
|
1448963823 | ||
|
|
58687c68a6 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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.",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
|
||||
@@ -9,6 +9,7 @@ the SKILL.md, not the table.
|
||||
<!-- BEGIN skills-table -->
|
||||
| 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 |
|
||||
| `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 |
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
@@ -61,4 +61,28 @@ if [ -f "$PENDING" ]; then
|
||||
done
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
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"
|
||||
[ "$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