Commit Graph
88 Commits
Author SHA1 Message Date
mathias d6e892ac4c feat(skill): traceability — a chain of checked links, and a tool that says what it cannot check
CI / Check / Test (push) Successful in 2s
CI / Tag release (push) Successful in 1s
Forward trace: requirement → decision → commit → gate → verification → CI on
that sha → release → closure. The distinction the skill exists for is that each
link is either checked or named as unchecked; a trace where every link is
asserted is a narrative, and narratives are what audits exist to defeat.

`trace.sh` does the mechanical part — commit, files, test files touched, release
containing it, referenced issues as URLs — and then always prints the four things
git cannot prove. The GREEN run's own accounting: "the tool did the mechanical
15%. Its real contribution is the 'Cannot verify from here' block, which is a
checklist, not an output." Kept, because it is true.

The RED run established the shape by tracing a commit by hand in sixteen steps.
Its decisive moves were reading issue COMMENTS — where the decisions actually
live — and running a probe that falsified a claim. Both are now steps.

The GREEN run then used step 5 against a gate I shipped this morning, and broke
it. It also found four things wrong with what I had written:

- The slug sed used `[^/]+?` expecting a lazy quantifier. POSIX ERE has none, so
  `.git` was swallowed and every emitted evidence URL carried the suffix. It
  happened to work against Gitea and would 404 on GitHub — a bad failure for the
  one tool whose pitch is that an unchecked link must not look checked.
- "Prints every issue it references" was overclaimed: it greps the commit body,
  so it structurally cannot find the issue that cites the COMMIT. That is the
  commoner break — the issue filed afterwards that falsifies the outcome — and
  it is now step 6 with the two searches that find it.
- Step 3 told you to check which job passed, and the estate's own
  `workflow_run_status` returns no jobs. A step whose tooling dead-ends is where
  a trace becomes "the run was green"; the REST fallback is now written out.
- The output format did not survive contact. It grew four slots, each forced by
  a real trace: two requirements differing in kind (a defect report and a
  standing bar), the gate as an artifact separate from the test, the RED/GREEN
  baseline against the parent, and the commit's factual claims checked. Without
  the last one, "commit → verified" means only "this sha exists".

Also: `git describe --contains` reads local tags, so the tool now says so on
every hit rather than only when it finds nothing.

Closes #10
Bump-Type: minor
v0.18.0
2026-08-29 15:34:11 +02:00
mathias 4d6ebb626f fix(check): read the session_log payload, not the file
A traceability probe falsified this gate four hours after I shipped it. The
check was `grep -q 'session_id' "$dir/SKILL.md"` — file-scoped. So the probe
stripped session_id from all three documented payloads in tdd/SKILL.md, added
one prose line saying "the tool has a required session_id field", and the gate
returned 0. The exact regression it exists to prevent was back in the file, and
the gate said nothing.

The duration_ms rule was no better: it matched one spelling, the historical one.
Both of these passed — `"duration_ms": <wall-clock>` with a JSON-quoted key, and
`duration_ms: elapsed wall-clock ms` with no angle brackets. It was a regression
test against the nine strings that existed that morning, described in the commit
message as a rule.

Now each documented payload is checked on its own line and reported with a line
number, and duration_ms is rejected in any spelling unless it carries an actual
number — the objection was always to a placeholder no agent can fill, not to the
field.

Five new cases, each one a probe from that run: prose mentioning the word while
the payload omits it, one bad payload among several, both duration_ms spellings,
and a real measurement being allowed.

Also fixed while writing them: my first regex had `[[:space:]]*` before a
negated class that did not exclude whitespace, so the star matched empty and the
space itself tripped the check — a real measurement failed. The tests caught it
immediately, which is the argument for writing them from the probes rather than
from the intent.

Refs #36, #39
Bump-Type: patch
2026-08-29 15:34:11 +02:00
mathias 88a4fa407b chore(release): plugin.json version 0.17.1
Skip-Release: true
2026-08-29 13:27:06 +00:00
mathias 808af7b020 chore(release): plugin.json version 0.17.0
Skip-Release: true
2026-08-29 13:27:05 +00:00
mathias 40405cd95a fix(check): make the escalation ratchet actually ratchet
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
"That list only shrinks" was asserted in the commit message, the script header,
the pending file's own header, the test header, and the issue comment. It was
enforced nowhere.

A traceability probe falsified it in one run: extract the commit to a temp tree,
add a skill with no `## Escalation` section, append its name to
bin/escalation-pending.txt — gate returns 0. The list could grow silently and
forever. "Self-cleaning" was true; "only shrinks" was a social convention wearing
a mechanism's clothes, and this repo has spent a day on what those cost.

The gate now diffs the pending list against the version committed at HEAD and
fails on any added entry, naming it. Where the comparison is impossible — not a
git checkout — it says so rather than passing silently, because a ratchet that
cannot be verified must not look like one that was.

Four new cases: unchanged list, shrinking list, growing list, and the
unverifiable case.

Refs #37
Bump-Type: patch
v0.17.1
2026-08-29 15:26:24 +02:00
mathias 39ac61c411 feat(skill): lineage — backward decision archaeology, with a tool
CI / Check / Test (push) Successful in 2s
CI / Tag release (push) Successful in 1s
Ports the technique #11 asked for and ships the script #19 asks for: was this
tried, what killed it, and is the killer still alive.

The RED run did excellent archaeology unaided — fourteen steps, correct verdict,
and it split the proposal into a goal worth keeping and a mechanism that had
failed, which is the finding. What it never ran was `git log -S`, and what it
leaned on hardest was the issue tracker. Both facts shaped the skill.

The GREEN run then broke the tool's central claim, live. Asked about a feature
this repo removed and argued about at length, `lineage.sh "per repo"` printed:

  no trace of "per repo" — that is a real finding: it means this is new here

Flatly wrong; the term simply had a space in it. Worse, the skill's own
anti-pattern table forbids that exact inference — "assuming absence means it was
never considered" — so the tool was printing, in its own voice, the mistake the
skill warns against. A no-match now says it is one string against one repo, and
names the variants to try before anyone concludes novelty. The test asserts the
refusal rather than the old message.

Three more from the same run:

- The tool searched `docs/adr/*` for decision records. This repo has no `docs/`,
  and this estate keeps decisions in the brain and in gitea comments — so that
  section silently searched nothing four times, and its null was indistinguishable
  from a real one. It now names both unsearchable sources on every run, and the
  skill makes `brain_query` a step: the single best artifact in the GREEN run came
  from the brain and nowhere else.
- Step 1 steered readers toward path-scoped log and away from `-S`. Backwards. A
  function deleted from a file that still exists is the dominant removal shape,
  and `-S<identifier>` returns birth and death on one screen while the path search
  buries it. Rewritten to lead with the identifier.
- "Which conditions still hold" had no method, and the run answered it by running
  commands against the live host in twenty seconds. Now a table: one command per
  claim, because "the record says X" and "X is true right now" are different
  answers and only the second is worth giving.

Also fixed: the tool defaulted to the cwd and would return a confident null
against the wrong repo in a worktree-heavy setup. It now prints which repo it
searched, every run.

Closes #11
Bump-Type: minor
v0.17.0
2026-08-29 15:26:14 +02:00
mathias 2cb6b54a5e feat(skill): adr — the house format, and how to record a decision you cannot make yet
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
Codifies cad's ADR template (frontmatter + Status/Context/Decision/Consequences/
Validation) and formalises the productive-tensions pattern that ADR-0001 already
does informally under "Known refinements, deliberately not built yet".

The RED run is why the skill is shaped this way. An unaided agent given a
genuinely-undecidable transport choice produced a strong ADR — it invented a
"Cost of reversal" section, wrote concrete revisit triggers, and reframed the
unanswerable question ("which is better?") into an answerable one ("which
option's correctness depends on the unsettled variables?"). So the reasoning was
not the gap. What it could not guess was the house format: it invented a bullet
header instead of the frontmatter that makes the corpus greppable, and it wrote
no Validation section at all.

The GREEN run then found eleven problems. Four mattered:

- **The Overview contradicted the Escalation section in its first sentence** —
  "An ADR is not documentation of a decision. It is the decision" against
  "decides nothing". Escalation is right; the flourish is gone.
- **The escalation clause trapped every agent-authored ADR.** The deciders are
  never the agent, so `proposed` is mandatory — and the skill separately called
  a lingering `proposed` a failure mode. Now: a `proposed` ADR with a named
  decider is waiting on a person and gets chased; one without is waiting on
  nobody. Naming that person is the deliverable, not a formality.
- **No filename or path convention, in a skill whose thesis is greppability.**
  Called "the biggest hole in the skill", correctly. Now docs/adr/NNNN-slug.md,
  with the cross-repo caveat that a bare number is ambiguous between repos.
- **"Pick the option whose correctness does not depend on the unknown" assumed
  such an option always exists.** It often does not — that is what makes a
  tension productive — and the rule would then push the author to manufacture a
  reason one of them is, which reads as reasoned and never gets re-examined. A
  fallback ladder now says: cheaper to reverse, lower switching cost, already
  built — and say that is why.

Also: `deciders` had no valid value while proposed; the body Status section
duplicated frontmatter with no precedence; the tension template silently dropped
the "address the strongest case against" requirement the plain format demands;
the preservation strategy — the actual technical content — got one angle bracket
while the easy parts got full templates; resolution triggers were singular for
tensions that routinely have several; and lineage/grill-me were listed as
cross-references at the bottom when they are pre-steps that change the content.

Closes #9
Bump-Type: minor
2026-08-29 15:24:11 +02:00
mathias 1448963823 chore(release): plugin.json version 0.16.0
Skip-Release: true
2026-08-29 13:19:26 +00:00
mathias 58687c68a6 chore(release): plugin.json version 0.15.1
Skip-Release: true
2026-08-29 13:19:25 +00:00
mathias 0c7fac3686 fix(debug): close the intake gate properly — and the contradiction it exposed
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
The GREEN run confirmed the gate works: given "builds are flaky again... what's
the fix?" with no run id and no log, it produced zero hypotheses and an ordered
list of artifacts, each justified by what it would discriminate. It also caught
that the retry already shipped in that pipeline is destroying the evidence
needed to diagnose it, which is a better finding than any hypothesis would have
been.

Then it found eight problems, and the first two are mine:

**The template contradicted the rule.** I banned high/medium/low labels in
Step 2 and left them in the Step 3 output template and all three worked-example
hypotheses. "Prose loses to a format block every time" — correct, and the
template is what gets imitated. Labels are gone from both; the slot is now WHY
FIRST, which asks for the evidence that sets the order rather than a restatement
of it.

**The description never mentioned the gate.** That is the text a harness shows
before an agent decides to load anything, and it read "Generate 3-5 ranked
hypotheses" — so an agent routing on the description alone does exactly the
thing this skill was rewritten to prevent. Both description and use_when now
lead with the refusal.

The rest:

- Intake vs the failed-fixes section pointed opposite ways for a report that
  fires both, and nothing stated precedence. Intake wins, and says so: you
  cannot aim hypotheses at a premise you have no evidence about either.
- "One run narrows the search to a single component" was overclaimed exactly
  where it gets used — CI flakiness. One run *that reproduces* narrows it; a
  passing run narrows nothing.
- session_log had no phase for a correct refusal, so an intake stop logged as a
  failed read-failure. A metric that penalises the skill's best behaviour trains
  the next model to guess. Added an `intake` phase.
- The Anti-Patterns table — the part people scan in a hurry — was missing the
  failure this whole rewrite is about: answering "what's the fix?" from a report
  with no artifact.
- brain_query told you to search on an error string you may have just refused to
  proceed without. Now: query symptom + component, which may itself name the
  artifact to ask for.
- condition-based-waiting was an unconditional load on any mention of a timeout,
  including during an intake stop, which is the wrong side of the gate.

Closes #36
Bump-Type: minor
v0.16.0
2026-08-29 15:17:16 +02:00
mathias cba0fe4337 docs(tdd): fix ten defects the GREEN run found in the language references
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
The pressure test that #18 and #20 were waiting on. All six original gaps came
back ANSWERED with the lines quoted — and the run then found ten problems in
what I had written. The worst two are mine, not inherited.

**The Python exemplar was the evaluation task.** references/python.md
demonstrated `split_vat` — the exact function the test scenario asks for,
negative-amount case included. The run said so: "as a teaching example that's
over-fitted; as an evaluation it's a leak... this task can't answer it — the
reference handed over the finished artifact." So the run could not measure
whether the guidance transfers, only whether copying works. The exemplar is now
`parse_retry_after`, deliberately unrelated to anything in this estate, and it
states its own contract rather than requiring the reader to divide 12500 by 1.25
to discover it.

**mypy at RED pointed at the stub the same file forbids.** python.md taught that
a pytest ImportError is a legitimate red, then presented `mypy --strict` as a
matched pair without saying that mypy reports the same missing module — so
following both literally, the way to get mypy green at RED is to create the
module. Now stated: that error is the expected red; what must be fixed
immediately is a --strict violation inside the test file you just wrote.

The rest, each quoted in the run:

- The checklist contradicted itself two lines apart — "strike it deliberately"
  above, "Can't check all boxes? You skipped TDD" below. A struck box is by
  definition unchecked, so the Python path routed straight into "start over".
- `match=` was overclaimed as "free". It is `re.search`, so a message containing
  parens quietly matches almost anything; and it couples every error test to
  wording. The Go comparison was a strawman against `assert.Error` when
  `assert.ErrorIs` exists — the honest Python analogue is a custom exception
  type, with `match=` as the fallback.
- `pytest.approx` was listed as the soft-assert analogue. It solves float
  tolerance and has nothing to do with collecting failures; that is
  pytest-subtests or pytest-check, neither of which ships with pytest.
- The whole-shape assertion silently required value equality — on a plain class
  it compares identity and can never pass.
- "When Stuck" is a language-neutral table that prescribed `t.Helper()`.
- `__tracebackhide__` must be a local inside the helper body; at module level it
  does nothing, silently.

Refs #18, #20
Bump-Type: patch
v0.15.1
2026-08-29 15:16:20 +02:00
mathias 06592805b6 chore(release): plugin.json version 0.15.0
Skip-Release: true
2026-08-29 12:56:44 +00:00
mathias 1d140ec525 feat(check): every skill states when it stops and hands back
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Implements #5's decision. The hard part was #37's, not #5's: which skills need
an escalation rule? A hardcoded list rots, sniffing the prose for "does this
write anything" is unfalsifiable, and a frontmatter flag is the option #5
rejected wearing a hat.

So there is no acting set. Every skill answers the question, and a read-only one
answers it in a line. That turns an absence into a claim someone can disagree
with, which is the whole point — "this skill never hands back" is a design
decision worth stating, and today it was indistinguishable from nobody having
thought about it.

Eleven written here, all advisory or read-only, where the honest answer is short
and the value is in being specific rather than generic. `cognitive-load` stops
when the metric disagrees with the people reading the code, because that is
evidence about the metric. `test-design` stops when the tests are fine and the
design under test is what makes them awkward. `code-review` stops when the diff
is big enough that a review would be a skim, and says so rather than producing
confident coverage of a fraction.

The remaining thirty are the acting set — writes, dispatches, credentials,
releases — and each needs a rule someone thought about rather than a rote
paragraph, so they sit in bin/escalation-pending.txt. That list only shrinks,
and it is self-cleaning: a listed skill that HAS a section fails the gate, so an
exemption cannot outlive its reason. Mutation-verified both ways.

Six test cases including the stale-entry case, because an exemption for a skill
that no longer exists is how a gate quietly stops covering something.

Refs #37, closes the mechanism half
Bump-Type: minor
v0.15.0
2026-08-29 14:56:04 +02:00
mathias 2fcc15e9e4 chore(release): plugin.json version 0.14.0
Skip-Release: true
2026-08-29 12:53:49 +00:00
mathias b2af4e1129 feat(doctor): --online, for the release a host has never fetched
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Everything doctor checks compares copies already on the machine, deliberately —
it runs in a SessionStart hook where latency shows, and its header promises no
network I/O. That leaves exactly one case invisible, and #30 walked straight
into it: a host whose checkout, marketplace clone and installed plugin all agree
reads as clean while all three sit a release behind origin.

`--online` closes it with one `git ls-remote`, opt-in, for a scheduled per-host
job rather than the session hook. Comparison is by order, not equality, so a
host ahead of the newest tag — normal between a release commit and its tag —
stays quiet.

An unreachable remote is a WARN naming itself, not a staleness FAIL. "I could
not check" and "you are behind" are different answers, and collapsing them would
make a network blip look like a stale library.

The remote URL is never printed: a remote rewritten by url.insteadOf expands
into a live token, which is a documented way this fleet has leaked one before.

Five fixture cases through a $DOCTOR_LATEST_TAG_CMD seam, including one that
asserts the DEFAULT run performs no lookup at all — the promise in the header is
now a test, not a comment. Verified live on koala:

  $ bin/doctor --online
  FAIL behind: this host has 0.12.1; origin has released 0.13.4
  $ bin/doctor
  skills doctor: clean — 41 skills

Closes #38
Bump-Type: minor
v0.14.0
2026-08-29 14:52:52 +02:00
mathias 3c8d2f5613 chore(release): plugin.json version 0.13.4
Skip-Release: true
2026-08-29 12:50:31 +00:00
mathias 1d368d4b5d docs(skills): drop the Mode 2 routing notes, fix the contributing steps
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Seven skills carried a `## Mode 2 Routing Note` citing a routing pod ("Plan 6")
that does not exist. Several admitted in their own text that nothing about the
skill changes between modes — so it was roughly ten lines of context on every
load for zero behavioural effect. The content is not worthless, just not
per-skill: it now lives as one table in README.md, with the caveat that nothing
depends on it.

The related line inside the session_log sections stays, because that one does
motivate behaviour — but it no longer cites a component that has never shipped.
It now says what is true: pass-rate is what a future routing decision would be
based on, and a skill that never logs contributes no evidence either way.

While in README: the contributing steps were stale enough to fail CI for anyone
who followed them. They said to add a row to SKILLS_INDEX.md by hand — the table
is generated now, and `task check` fails on a hand-edited diff — and listed none
of the four frontmatter keys the gates require. Step 4 now names the
pressure-test bar from #19 rather than leaving it to be discovered.

Closes #36
Bump-Type: patch
v0.13.4
2026-08-29 14:50:14 +02:00
mathias a565b04e91 chore(release): plugin.json version 0.13.3
Skip-Release: true
2026-08-29 12:49:56 +00:00
mathias b616c51a2c fix(skills): document a session_log call the tool would actually accept
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Nine skills documented the payload. None named `session_id`, which is the
tool's only required field. So every documented call was one the tool rejects —
an agent following any of them either failed the call or invented an id, and
invented ids do not group, which means the pass-rate the /pass-rate endpoint
computes was being assembled from entries that cannot be joined.

Found by the #19 pressure test against `debug`, which invented
`debug-signed-artifact-missing-2026-08-29` and said so: "my three entries are
only groupable by luck of naming". The grep afterwards showed it was fleet-wide,
not a `debug` bug.

Also removed: `duration_ms: <wall-clock>` in eight of them. An agent does not
observe a clock. The same run omitted it "rather than fabricate", which is the
good outcome; the bad outcome is the run that fabricates, and a made-up number
in a metrics stream is worse than a missing one. It is an optional field and is
now simply absent.

bin/check-session-log gates both, with tests. This is exactly the case #19 has
in mind for a small tool over prose: the rule is a grep, so it should be a gate,
not a note asking people to remember.

Closes part of #36
Bump-Type: patch
v0.13.3
2026-08-29 14:49:20 +02:00
mathias b8bacfa7b3 chore(release): plugin.json version 0.13.2
Skip-Release: true
2026-08-29 12:22:35 +00:00
mathias c377c23e21 fix(mcp): correct the parked reason — the tokens work, the delivery does not
CI / Check / Test (push) Successful in 2s
CI / Tag release (push) Successful in 1s
The previous note blamed infra#224 for cad and gitea-mcp being unreachable.
Both claims were wrong. infra#224 is closed, and closed deliberately: rather
than widening koala-homelab's grants, a second scoped token was provisioned at
~/.op-agent-write.env for AgentSecrets. Sourcing it makes `op vault list`
return AgentSecrets, and an MCP initialize against both endpoints returns 200 —
serverInfo gitea-mcp 0.1.0 and cad aed2aaf. Nothing about the services or the
proxying is blocked.

What is actually blocked is delivery into the session environment. By design no
single identity on koala reads both vaults, and the session env comes from one
`op run --env-file ~/.op-env` under the HomeLab-only token. A mixed-vault file
does not degrade gracefully: tested on a two-line copy, `op run` aborts on the
unresolvable reference and BRAIN_MCP_TOKEN goes unresolved with it — so
appending to ~/.op-env would take out the MCP servers that currently work.

Refs #32, #33
Bump-Type: patch
v0.13.2
2026-08-29 14:22:17 +02:00
mathias 8f9d5dd8bd chore(release): plugin.json version 0.13.1
Skip-Release: true
2026-08-29 12:19:31 +00:00
mathias 7ba01abdbe chore(mcp): park cad and gitea-mcp entries, blocked on vault visibility
CI / Tag release (push) Successful in 1s
CI / Check / Test (push) Successful in 1s
Both services are now proxied from koala and reachable: tailscale serve 8445 ->
127.0.0.1:31877 (cad) and 8446 -> 127.0.0.1:30340 (gitea-mcp). Both answer 401
unauthenticated, so the path is right and auth is enforced.

They stay out of mcpServers because neither token resolves on this host. The
static tokens live in the 1Password AgentSecrets vault; koala's op identity sees
HomeLab only:

  op vault list                    -> HomeLab, and nothing else
  op://HomeLab/CAD_STATIC_TOKEN    -> could not find item
  op://HomeLab/gitea-mcp-shared-token -> could not find item

That is infra#224, not the OAuth-discovery bug #32 and #33 both flagged as the
likely blocker. A live entry that cannot authenticate would fail on every
session start, so the wiring is recorded rather than enabled.

The item names come from the ExternalSecrets, read by reference only — no
secret value was retrieved, printed, or transformed at any point.

Refs #32, #33
Bump-Type: patch
v0.13.1
2026-08-29 14:18:38 +02:00
mathias 9a81a707af chore(release): plugin.json version 0.13.0
Skip-Release: true
2026-08-29 12:10:37 +00:00
mathias 5070a738e8 docs(review): Python adaptation files for clean-code, solid and code-review
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
The canonical Python stack has been pinned in ~/dev/.context/AGENT.md since
local-dev v0.3.1, and three skills still had Go examples with no Python
counterpart. Following the structure #18 settled on, and the existing
solid/references/go-adaptation.md precedent, each gets a reference file rather
than inline dilution of the primary doc.

The stack table itself is NOT copied into any of them — each cites AGENT.md as
its home. This repo spent today learning what a second copy of a fact costs.

Content is the part a linter cannot give you:

- clean-code: `raise X from e` as the analogue of `%w` error wrapping, dataclass
  over `dict[str, Any]`, and the two traps where the broken version reads
  tidier — mutable default arguments, and `if not amount:` for a value where
  zero is legitimate.
- solid: `Protocol` over `ABC`, defined at the consumer, which is what makes DIP
  real in Python; plus what does NOT carry over from the Go rewrite (return
  structs/accept interfaces, embedding, package-as-deployment-unit).
- code-review: a Python anti-pattern table parallel to the Go one, opening with
  the rule that a finding ruff or mypy already catches is not a review comment,
  it is a question about why the gate is not running.

Refs #20
Bump-Type: minor
v0.13.0
2026-08-29 14:10:10 +02:00
mathias 8da7c9c45f docs(tdd): make the spine language-neutral, add four language references
The discipline in tdd was already language-agnostic; the checklist under it was
not. A Python user inherited a mandatory `go test -race` box they can never tick
and never got told that `mypy --strict` belongs in the red gate.

Per #18's own comment and writing-skills' Multi-Language Dilution warning, the
main SKILL.md keeps ONE canonical inline example (Go, the default stack) and the
other four languages live in reference files: python, typescript, rust, java.

The RED run drove every change. A subagent given a Python task with the Go-only
skill reported, in its own words:

- "Everything from L169 to L244 is Go-only, and the verification checklist at
  L326 silently inherits that Go-ness — which is why a Python user ends up with
  a mandatory box (-race) they can never tick and a required gate (mypy
  --strict) that isn't listed at all."
- "For a brand-new function, the first RED is always a collection-time
  ImportError... The only clean resolutions are a stub that raises
  NotImplementedError (production code before a red test?) or accepting
  ImportError as a legitimate red. The skill picks neither, and this is the
  single biggest gap." It now picks: that red is legitimate, and writing a stub
  to satisfy the runner is the Iron Law with extra steps.
- The Go table-driven exemplar branches on a `wantErr` column inside the loop.
  Translated literally that is a conditional assertion folding two behaviours
  into one test — which this skill's own quality table forbids. The agent caught
  itself mid-translation; both SKILL.md and python.md now say not to port it.

Also: the checklist names the static-analysis gate per language instead of
assuming `go vet`, and says to strike the concurrency box deliberately where no
equivalent exists — a box that cannot be ticked teaches people to skip
checklists.

Refs #18
Bump-Type: minor
2026-08-29 14:10:10 +02:00
mathias 6e5024d498 chore(release): plugin.json version 0.12.1
Skip-Release: true
2026-08-29 09:48:12 +00:00
mathias 62cfe46927 docs(superpowers): point the vendored cluster at the tools this harness has
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 0s
The vendored skills were written against a generic `Task()` and an operator
called "your human partner". Neither exists here. An agent following
dispatching-parallel-agents literally would call a tool that is not in its list.

`Task tool` becomes the `Agent` tool with `subagent_type` across all six prompt
templates and the Gemini mapping table, and the dispatch example gains the three
options that actually matter for parallel work: issuing every dispatch in ONE
message (several Agent uses in a single turn run concurrently; one per turn
serialises them), `isolation: "worktree"` so two agents cannot fight over the
same tree, and per-agent `model` override for cheap mechanical fan-out.

`Workflow` is named as the structured alternative — phases, per-finding verify,
fan-in — with its opt-in condition stated, because a skill that told an agent to
reach for it unprompted would be teaching a policy violation.

"your human partner" becomes "the operator" in five skills. Beyond phrasing, it
was factually wrong in executing-plans, which told the reader to *ask* whether
subagents are available; on this fleet Claude Code has them and Crush/Pi/OpenCode
do not, so it now says which skill to use where.

Closes #17
Bump-Type: patch
v0.12.1
2026-08-29 11:43:46 +02:00
mathias de84800892 chore(release): plugin.json version 0.12.0
Skip-Release: true
2026-08-29 09:41:44 +00:00
mathias 51a2d396fd docs(spec): EARS-lite behavioural criteria and a context snapshot
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Both skills gain EARS-lite as an option alongside prose criteria: WHEN/WHILE/IF
plus the unconditional form, for behaviour that a measurable threshold cannot
express. feature-spec also gains a Context snapshot section — the executor of a
spec routinely cannot open the architecture document it points at, so the facts
it depends on get inlined with provenance.

Pressure-tested per #19, and the RED phase argued against half the issue. A
subagent with no skill wrote a strong spec and then explained that it had
deliberately NOT inlined architecture context, "because both are owned by the
existing architecture document and duplicating them here would create two
drifting sources of truth". That objection is correct, so the section is built
around answering it: snapshot what the implementer cannot look up, always cite
source and version, and state that the source wins on disagreement. Prose
criteria came out fine unaided; EARS phrasing did not appear at all, which is
where the real gap was.

The GREEN run then found seven defects in the first draft, all fixed here:

- The example was in the same domain as the test scenario, so it got copied
  rather than read — the spec inherited an extraction-confidence requirement
  its feature request never mentioned. The example is now deliberately
  off-domain, with that failure written up as the reason.
- EARS-lite had no unconditional form, so a genuine invariant ("no instruction
  above threshold ever reaches a file") had to be bent into a WHILE that a
  system trivially satisfies by never entering the state. Added.
- "Maps 1:1 onto a test" was false for the WHILE form, which is universally
  quantified. Softened to say which forms map to one test and which do not.
- The provenance rule had no answer for the case it was written for: an author
  who also cannot read the source. Now: mark the block UNVERIFIED in those
  words and make confirmation the implementer's first task.
- "Snapshot only what this spec's decisions rest on" was narrower than the
  section's own purpose; a table name underpins no decision and still blocks
  the migration. Widened, with the omissions stated deliberately.
- "Say so in the section itself" did not say what, at what granularity. Now one
  opening line, not a per-fact annotation.
- The template had no slot for EARS lines, so their placement was a guess.
  Added as a subsection with a note that they are properties, not checkboxes.

Closes #12
Bump-Type: minor
v0.12.0
2026-08-29 11:40:42 +02:00
mathias bb912e5594 chore(release): plugin.json version 0.11.0
Skip-Release: true
2026-08-29 09:34:36 +00:00
mathias 08c935ba8a chore(debug): fold in two techniques, delete systematic-debugging
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
superpowers-systematic-debugging restated debug's core (hypothesis before fix)
across 296 lines, with a fix-implementation phase that belongs to tdd and a
rationalization table that mostly repeats debug's own Anti-Patterns. Two things
in it were not restatement, and both are now in debug: instrument every
component boundary before hypothesising when a bug spans components, and treat
three failed fixes as evidence the mechanism was never identified rather than
as a reason to try a fourth.

Not deleted with it, contrary to the issue: find-polluter.sh and the three
technique files (root-cause-tracing, defense-in-depth, condition-based-waiting)
are real assets, one of them an actual tool. They move to debug/references/ with
a stated load trigger each, because a reference nobody knows when to open is
dead weight. The pressure-test transcripts and CREATION-LOG from the upstream
fork go; they document obra's process, not ours.

Pressure-tested per #19, and the RED phase refuted the issue's premise. Two
subagents without the skill got the multi-component scenario and the four-failed-
fixes scenario; both already instrumented boundaries unprompted, and the second
refused to write a fifth fix on its own initiative. So these sections are not
correcting a demonstrated gap in this model — they are insurance for weaker
models and for prompts less well-posed than a test scenario. Recorded as such
rather than claimed as a fix.

The GREEN run applied both sections correctly and then found four defects in
them, all fixed here: the boundary rule read as suspending Iron Law 2's 3-5
hypotheses; the trigger said "reduced" when fixes that change nothing are the
stronger signal; "stop proposing a fourth" read as licence to return no
hypotheses at all; and the references had no load triggers. Its remaining
findings are pre-existing and filed as #36.

41 skills. debug 146 -> 188 lines.

Closes #14
Bump-Type: minor
v0.11.0
2026-08-29 11:33:36 +02:00
mathias 84a012cd0f chore(release): plugin.json version 0.10.2
Skip-Release: true
2026-08-29 09:28:32 +00:00
mathias e12ac99465 docs(review): cross-link the three review skills, drop a false citation
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Three similarly-named skills cover orthogonal concerns — what to check
(code-review), how to commission a reviewer (requesting), how to behave when
feedback arrives (receiving) — and none of them mentioned the other two. An
agent landing on one had no way to learn the others existed. One line each,
placed at the core-principle line where a reader is already oriented.

stage-gate-review's gate-prep step now says to run grill-me's Full Grill before
bringing an ask: a council's hard questions are cheaper answered in private, and
a gap found by grilling is one you close rather than one the gate reports.

The bug half: receiving-code-review cited "You're absolutely right!" as an
"explicit CLAUDE.md violation". That is obra's upstream convention leaking
through the vendored copy — `grep -ci "absolutely right" ~/dev/CLAUDE.md` is 0,
and so is AGENT.md. The guidance is right, the citation was inventing authority
it did not have; it now says what it actually is, performative agreement rather
than evaluation.

Closes #16
Bump-Type: patch
v0.10.2
2026-08-29 11:27:52 +02:00
mathias dc97fd4a36 chore(release): plugin.json version 0.10.1
Skip-Release: true
2026-08-29 09:25:28 +00:00
mathias d7775aac57 fix(ci): tag and bump separately — a queued run pushed a stale main
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Three runs failed with `! [rejected] HEAD -> main (non-fast-forward)`. The
runner is maxParallel=1 and shared with mathias/infra, so those jobs started
five minutes and three commits after their own push. The tagger checked out
its commit and pushed that stale HEAD to main, which main had long left behind.

My own change made it worse: pairing the manifest bump and the tag in a single
`git push` turned a partial failure into a total one, so no tag was created
either. Except once — v0.10.0 landed through the no-bump-needed branch while
the manifest stayed at 0.9.0, which is precisely the "installed hosts never
see the release" state, and which #34's ordering check then caught on main.

The tag now goes up first and alone: it names the commit this run checked out
and cannot be rejected however far main has moved. The bump is a separate
concern that must land on whatever main is NOW, so it re-fetches, rebuilds the
commit on origin/main and retries up to three times, and steps aside entirely
if a later run has already bumped past it.

plugin.json is set to 0.10.0 in this commit so the gate can pass at all — main
was red on manifest-behind-tag, which would have blocked the tag job that fixes
the tagger.

Refs #22
Bump-Type: patch
v0.10.1
2026-08-29 11:25:20 +02:00
mathias c6787e04d7 feat(index): generate the skills table from frontmatter
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
Second half of #29. The index was hand-maintained and had already drifted
once: dream and experiment-spec shipped with no row, their rows lost to a
squash-merge after a directory reorg. bin/check-index caught that after the
fact; generating removes the class.

The curated summary and trigger move into each SKILL.md as `summary:` and
`use_when:`, carried over verbatim from the existing table rather than derived
from `description:` — description is a paragraph written for model selection,
and truncating it would have produced a worse table than the one it replaced.
Knowledge now lives once, next to the skill it describes, which is the argument
that made a hand-maintained index the wrong shape to begin with.

The table is written between markers, so the wiring docs, the relationships
tree and the trigger table below it are untouched — those are prose, not data.
Rows are alphabetical now; the conceptual grouping the old order carried lives
in the relationships tree, which is the better place for it. A fourth column
shows each skill's surfaces, which is what makes #29's field visible rather
than merely enforced.

bin/check-index and its test are deleted: a generated table cannot drift in
either direction, so `generate-index --check` subsumes both of its assertions.
Verified by mutation — deleting the tdd row turns `task check` red with
"SKILLS_INDEX.md is out of date".

Closes #29
Bump-Type: minor
2026-08-29 11:23:14 +02:00
mathias 90a58f0b9e feat(skills): declare which surfaces each skill ships to
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
The repo distributes three ways — the Claude Code plugin, the symlinked
library for Crush/Pi/OpenCode, and claude.ai, which has no fetch path at all —
and treated every skill as belonging to all of them. claude.ai is not a fourth
host to mirror: gitea-ci, playwright and web-shot have no shell and no cluster
there, so a flat mirror installs skills that can never fire and degrades
selection for the ones that can.

Every SKILL.md now carries `surfaces:`, defaulting to [claude-code, library];
dream, own-the-task and grill-me add `chat`, matching what is actually
installed on claude.ai today. Claude Code accepts the extra key —
`claude plugin validate` passes and the skills still load.

bin/check-surfaces validates values, not just presence, because the failure
mode here is silent: a wrong surface removes a skill from a harness rather than
erroring. install.sh links only skills whose surfaces include `library`, and a
source that declares nothing at all is treated as foreign and linked as-is —
Anthropic's official set has no reason to know this convention. doctor's
missing-link check skips non-library skills for the same reason.

Refs #29
Bump-Type: minor
2026-08-29 10:09:52 +02:00
mathias 15737321ac fix(check): fail when the manifest is behind the tag, not merely different
CI / Check / Test (push) Failing after 0s
CI / Tag release (push) Skipped
`task check` asserted plugin.json's version EQUALS the latest tag. `git
describe` is local-only, so any checkout that had not fetched the tag CI just
pushed went red on a commit that was correct — three times in one backlog
session, on the loop push → CI tags and pushes the bump → pull → red until you
also fetch tags.

The fault the assertion exists for is the manifest failing to move: a release
whose manifest still names the old version, so no installed host ever sees the
update. That is manifest < tag. Ahead is normal and is now quiet.

Ordering needed a real comparison, and bin/doctor had already grown one for
#30, so it moves to bin/newer-version with its own tests rather than existing
twice. Seven cases including the lexical trap (0.8.10 beats 0.8.9) and a
missing component counting as zero.

Verified by mutation: manifest 0.1.0 against tag v0.9.0 exits 201 naming both
versions; manifest 9.9.9 exits 0; the real tree exits 0.

Closes #34
Bump-Type: patch
2026-08-29 10:07:27 +02:00
mathias 025f01aa80 feat(install): serve several sources instead of displacing them
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Failing after 1s
~/.agents/skills was a single link straight at this checkout. That encoded an
assumption nobody had stated — that this checkout is the only source of skills
on the host. On flamingo it was not: Crush's directory held Anthropic's
official set, and the only way to install this library was to move that set
aside. A capability loss dressed up as a tidy-up.

The canonical location becomes a real directory of per-skill symlinks, fed by
the checkout plus anything in SKILLS_EXTRA_SOURCES. Harnesses keep their one
directory link, so the count is N + one per harness rather than N x harnesses,
which is what #23 was actually about.

The three requirements #27 named, each with a test:
- collisions are logged with both paths and the checkout wins, never resolved
  in silence;
- removing a skill from a source prunes its link;
- anything in the managed directory that is not our symlink is reported and
  left in place — the installer does not delete content it did not create.

doctor learns the same model: sources are recorded at ~/.agents/skills-sources,
a link into a configured source is healthy, a link into an unknown one is not,
and the old count check becomes a per-skill check because a per-skill layout
can lose one skill without changing any total. The pre-#27 wiring is named as
legacy rather than reported as damage. count_skills needed find -L; without it
symlinked skill directories count as zero.

Verified live on koala: migrated in place to 42 per-skill links, then a second
source added a 43rd alongside them, then a re-run without that source pruned it
back to 42. Only doctor finding throughout was the genuine stale plugin.

Closes #27
Bump-Type: minor
v0.10.0
2026-08-29 10:05:46 +02:00
mathias eee62e4fc0 chore(release): plugin.json version 0.9.0
Skip-Release: true
2026-08-29 08:01:01 +00:00
mathias a2afb35d1d feat(doctor): report the installed plugin generation, not just its presence
CI / Tag release (push) Successful in 0s
CI / Check / Test (push) Successful in 1s
Every other check in doctor is about symlink paths. That is the wrong shape
for Claude Code, which has no symlinks — it has the plugin, and it gates
updates on plugin.json's version. A host that never picked up a release loads
every skill successfully and is simply a generation behind, silently.

Found live while writing this: koala had skills@mathias 0.6.0 installed, a
marketplace clone at 0.7.0 and a checkout at 0.8.4, and doctor said "clean".

doctor now compares the version recorded in installed_plugins.json against the
newest manifest already on the host — the active checkout and any marketplace
clone, filtered to plugin.json files that actually name this plugin — and fails
naming both versions. Offline, like the rest of doctor: "available" never means
a fetch.

A host with no install record is a valid symlink-only configuration and stays
clean. --json grows a "plugin":{"installed","available"} object so per-host
results can be collected without parsing prose.

Four new fixture cases: match, stale, absent record, newer marketplace clone.
Live output on koala:

  FAIL plugin-version: Claude Code has skills@mathias 0.6.0 installed but
  0.8.4 is available on this host

Closes #30
Bump-Type: minor
v0.9.0
2026-08-29 10:00:11 +02:00
mathias ad17a7c6c0 chore(release): plugin.json version 0.8.5
Skip-Release: true
2026-08-29 07:56:02 +00:00
mathias 21dbb35526 chore(skills): delete superpowers-test-driven-development
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 0s
Verified duplicate rather than assumed one. Section for section it is the same
skill as `tdd` — Overview, When to Use, Iron Law, Red-Green-Refactor with the
same dot graph, Good Tests, Common Rationalizations, Red Flags, Verification
Checklist, When Stuck, Final Rule — with TypeScript examples and obra's "your
human partner" phrasing instead of ours. `tdd` carries all of it plus the
Go-specific material (table-driven tests, t.Run, testify, go test -race), the
brain MCP integration and the Mode 2 routing note.

Its one non-SKILL.md file, testing-anti-patterns.md, is byte-identical to
skills/tdd/references/testing-anti-patterns.md, which `tdd` already points at.
Nothing was salvageable because nothing was different.

No references anywhere outside its own directory except the index row, removed
here. `atdd` is untouched — acceptance-level is a genuinely different altitude
and it already cross-refs `tdd` correctly.

Incidentally the first live catch by bin/check-index (#15): deleting the
directory turned `task check` red with "SKILLS_INDEX.md lists
'superpowers-test-driven-development', which has no ...SKILL.md" before I got
to the row.

42 skills now. Closes #13
Bump-Type: patch
v0.8.5
2026-08-29 09:55:53 +02:00
mathias 8ad2079f86 chore(release): plugin.json version 0.8.4
Skip-Release: true
2026-08-29 07:54:53 +00:00
mathias b188965161 docs(index): place experiment-spec in the relationships diagram
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Missed in 83b069a. #15 asked for three things and the table row was only two
of them: experiment-spec also belongs in the Skill Relationships tree, parallel
to feature-spec and downstream of spec-driven-dev, since it is the research/ML
substitute for feature-spec rather than a separate lifecycle.

dream needs no edge, same as trainer and session-retrospective — the issue said
so and that part was right.

Refs #15
Bump-Type: patch
v0.8.4
2026-08-29 09:54:23 +02:00
mathias ae66840a03 chore(release): plugin.json version 0.8.3
Skip-Release: true
2026-08-29 07:53:48 +00:00
mathias e7e514390b docs(web-shot): resolve shot.sh from the install, not a checkout path
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
The usage block pointed at ~/dev/.skills/web-shot/shot.sh four times. That
directory has not existed since the skills moved into this repo, and the
failure mode is worse than a broken link: web-shot exists for agents with no
local browser, so a 404 sends them to hand-roll the Job instead of looking
harder. One caller burned three attempts on playwright module resolution
before finding the script that documents that exact gotcha.

Replaced with a resolution line covering both delivery paths — the Claude Code
plugin and the ~/.agents/skills symlink — so no absolute checkout path is left
to rot. Verified on koala: resolves to the .agents link, and degrades to an
empty string plus a clear message where the skill is not installed.

The brain entry headless-browser-via-k3s-playwright-job carried the same stale
path in three places and referenced the retired wire-skills.sh wiring; updated
to match.

Closes #26
Bump-Type: patch
v0.8.3
2026-08-29 09:53:33 +02:00
mathias 087685f2e3 chore(release): plugin.json version 0.8.2
Skip-Release: true
2026-08-29 07:52:16 +00:00
mathias 83b069afba docs(index): add dream + experiment-spec rows, and gate the index
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Successful in 1s
Both skills shipped without an index row. The rows existed — on the feature
branches deleted in #22 — and were dropped by the squash-merge that followed
a directory reorg: the SKILL.md landed under skills/, the row did not. Nothing
errored, the index just under-reported the library to every harness reading it.

Adding two rows by hand fixes today and not tomorrow, so the assertion goes in
`task check` via bin/check-index, following the same pattern as
bin/should-skip-release: a real script with a real test, not untested logic
buried in a Taskfile. It checks both directions — a skill with no row, and a
row naming a skill that no longer exists.

Verified by mutation: deleting the `dream` row turns `task check` red and the
report names it.

Closes #15
Bump-Type: patch
v0.8.2
2026-08-29 09:52:03 +02:00