Read handles PDF, images and notebooks natively; it does not open .docx,
.xlsx, .pptx, .epub, .msg or .zip. One uvx command does, with no install step
and no service.
Deliberately does NOT call docmark, which is the counter-intuitive part.
Measured 2026-09-24 on the same .docx, tables included: local markitdown and
the docmark service return byte-identical Markdown. Conversion of a
text-bearing document involves no LLM at all — the client is used only to
describe images. A network hop, a bearer token and an ingress for the same
bytes is not a trade worth making, and docmark is unreachable from a session
on koala anyway (ClusterIP, NetworkPolicy admits supervisor and dispatch only).
The boundary is measured, not asserted. The light extras register 18
converters; docmark registers 22. The difference is exactly DocxWithOCR,
PdfWithOCR, PptxWithOCR and XlsxWithOCR — the vision path, which is the one
case that must go through docmark because its sovereignty gate is the estate's
only audited route from an image to a model.
Two hazards the skill names because both bite silently:
- markitdown[all] pulls az-doc-intel and az-content-understanding, Azure
cloud converters. The extras list avoids having them on the path.
- A blank template and a scanned PDF with no text layer both convert to zero
characters and exit 0. Check the byte count; an empty success is the
failure mode, and a confident summary of an unread document is the worst
output this skill could produce.
Refs mathias/docmark#3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found from the other end: the koala cluster's CIHeadUncovered{repo=skills}
alert had been firing for 85 hours, and it is a true positive.
mathias/skills main HEAD f031c2b
workflow runs for that sha 0
Not a one-off. The previous release-bump commit has the same property:
6cc224f chore(release): plugin.json version 0.19.4 -> 0 runs
f031c2b chore(release): plugin.json version 0.19.5 -> 0 runs
The `tag` job pushes the manifest bump itself with secrets.GITEA_TOKEN, and a
push authenticated with the Actions token does not trigger workflows -- that is
deliberate platform behaviour to stop CI recursing. The `Skip-Release: true`
trailer stops the TAGGER recursing and works; nothing stopped the whole run,
`gate` included, from never firing on that commit.
So after every release, main's HEAD is a commit no CI has seen, until an
unrelated human push moves it. CIHeadUncovered therefore cannot clear on its
own -- and an alert that can only be cleared by doing something unrelated is
worse than a noisy one, because it teaches the reader that red is the normal
state of that rule.
The commit is not cosmetic either. plugin.json's version is what Claude Code
gates plugin updates on, `task check` asserts it is not behind the latest tag,
and the sed-and-retry logic that writes it already shipped a mismatched
manifest once -- v0.10.0 tagged with the manifest still reading 0.9.0, per the
comment above that very loop.
Fix: run `task check` on the bump commit before pushing it. The commit is
produced in that step, so it can be validated in that step -- one check instead
of a second full CI run, and no recursion risk to reintroduce.
Considered and rejected: re-triggering CI with a different credential after the
push (costs a full run per release and brings back what the token behaviour
exists to prevent), and teaching CIHeadUncovered to ignore manifest-only
commits (narrowing the measure to fit the system, and it would hide a real gap
too).
The assertion in tests/ci_gate_test.sh had to be anchored with ^[^#]*: the tag
job's own prose says "`task check` asserts the manifest is not behind the
latest tag", so a bare grep matched the comment explaining the problem and
reported the problem fixed.
Closes#46.
Second recurrence of the same host fault (infra#375; first hit
openbanking-api 2026-09-03, then cobalt-dingo 2026-09-08). golangci-lint
embeds the go/types export-data reader from the Go it was BUILT with, so a
newer GOROOT makes it unable to decode the stdlib:
panic: file requires newer Go version go1.27 (application built with go1.26)
An earlier form printed a typecheck error naming the user's own source file,
which sends people to debug code that is fine. It cost time in two repos before
anyone recognised it as a host fault rather than a repo fault.
CI never sees it — the runner image has matching versions — so it breaks
exactly the local `task check` that AGENT.md requires before every push, and
nothing else. The fix was always one reinstall; the missing thing was the
signal, which is what doctor is for.
Fails open by design: an unparseable version, or no Go at all, is a normal host
and not a fault doctor may assert. Version comparison is major.minor in POSIX
sh rather than sort -V, since this runs on macOS too and the export-data format
is minor-versioned.
Also makes the suite hermetic. The check reads the real host, so on a machine
whose golangci-lint is genuinely stale every unrelated fixture started failing.
The harness now prepends a matching fake pair for the whole file, and only the
four cases that opt in exercise the check.
Verified against koala, which is currently in exactly this state:
FAIL toolchain: golangci-lint built with go1.26.1 but GOROOT is go1.27.0
— lint panics on every Go repo; reinstall it under the current
toolchain (infra#375)
doctor: 25 passed, 0 failed. Full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes#44.
That issue overstated the risk and this corrects it. The hook is three lines
and echoes a string -- no bash-isms, so it cannot fail the way secret-guard
appeared to. The real exposure is narrower and duller: it can stop working
silently, and its entire job is to be visible.
THE ASSERTION THAT MATTERS IS REGISTRATION. A hook file that exists but is
absent from hooks.json does nothing at all and looks identical on disk to one
that works. So the test derives the command from hooks.json and runs it exactly
as registered, substituting CLAUDE_PLUGIN_ROOT the way the harness does, rather
than inventing its own invocation.
That choice is the lesson from #44's sibling. secret_guard_test.sh piped its
hook into `sh` while hooks.json runs it with `bash`; on Debian /bin/sh is dash,
which has no `set -o pipefail`, so the suite failed 20/20 in CI against a hook
that was FINE in production. The test was wrong, not the hook -- and the
earlier commit message for that fix said the shebang decides the interpreter,
which hooks.json shows is not true. Deriving the invocation removes the whole
class rather than fixing one instance of it.
No red phase to report: the hook was already correct, so this is regression
pinning, not TDD. Teeth proven by mutation against a throwaway copy instead:
- hook runs but prints nothing -> 6 failures
- hook removed from hooks.json -> caught, suite stops early
- ONE ritual step dropped, rule 0
still named -> caught (9 passed, 1 failed)
The third is the one worth having. Losing the whole line is obvious; quietly
losing "failing test" from the ritual while the banner still reads correctly is
the drift that would actually happen.
Bump-Type: patch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite piped hook input into `sh "$GUARD"`, which overrides the hook's
`#!/usr/bin/env bash` and tests an interpreter production never uses. Claude
Code executes the hook as a program.
On Debian /bin/sh is dash, which has no `set -o pipefail`, so the guard died
on line 23 before reading a byte of input:
secret-guard.sh: 23: set: Illegal option -o pipefail
secret_guard: 0 passed, 20 failed
while passing 20/20 on a host where /bin/sh happens to be bash. A security
gate that tests green and cannot start is the worst of both, and the test was
the only thing standing between those two states.
Found because baked-v16 added jq to the runner: this suite needs jq to build
its hook JSON, so it had been silently doing nothing in CI and started running
for the first time. Same shape as installing zip and immediately learning the
gitea backup check had never run -- a missing tool does not announce that it
is suppressing a whole suite.
Also asserts the hook is executable, since direct invocation depends on the
exec bit, and that assertion is what stops this regressing to `sh "$GUARD"`.
Checked the rest of the suite: every other `sh "$SUT"` targets a POSIX script,
which is correct. hooks/pre-task-ritual.sh is the other bash-shebang hook and
has no test at all -- filed separately rather than fixed here.
Bump-Type: patch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
infra#368 added jq to the gitea-runner image on the stated grounds that
check-mcp-env would then verify in CI instead of skipping. Verified after
rolling baked-v16: jq IS present now, the `SKIP: jq not installed` branch is
gone -- and the check still reports unverified, because CI has no ~/.op-env.
The env-file branch was always the binding one. It holds live credentials, so
it is not on the runner and must not be, which means this check can only ever
verify on a host that has one. Putting ~/.op-env on the runner to turn the
NOTE green would be trading a real secret for a green tick.
So the message now says so, at the point someone reads it. The risk otherwise
is not the skip -- it is a future reader seeing a passing pipeline and
concluding the MCP wiring is covered. It is covered on developer hosts only.
jq was still worth adding: it is a genuine dependency of this checker, the
runner's tool-contract.txt now asserts it, and a future image built without it
is a red pod at roll time rather than a silently degraded check.
Bump-Type: patch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bin/check-mcp-env exits 2 for "could not run" -- no jq, or no env-file --
and the gate was written to report that as a NOTE rather than fail:
"$DIR/check-mcp-env" "$DIR/.mcp.json" "$HOME/.op-env"
case $? in 0) ;; 2) echo "NOTE: ..." ;; *) fail=1 ;; esac
That tolerance never executed. Task runs each cmds block under `sh -e`, so
a bare command exiting non-zero aborts the shell before `case $?` is
reached. main went red on the very commit that added the check (run #57),
with `task: Failed to run task "check": exit status 2`.
It passed locally because this host has both jq and ~/.op-env, so the
check exited 0 and the untested branch never ran. CI has neither. A
tolerated exit code that is only tolerated where it never fires is not
tolerated at all.
`|| rc=$?` puts the invocation in a compound command, which `set -e`
exempts, so `case` is reached. Both exit-2 paths verified: no env-file
(the new test's precondition) and no jq (PATH sandbox, same SKIP message
CI printed).
The test pins the tolerance itself rather than the checker: it points HOME
at an empty directory, which is exactly CI's condition, and asserts `task
check` exits 0 AND still says it could not verify -- silence would mean
skipped rather than tolerated.
Bump-Type: patch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gitea-mcp was parked alongside cad on the stated reason that nothing puts its
token into the session environment: GITEA_MCP_STATIC_TOKEN lives in the
AgentSecrets vault, which the session identity deliberately cannot read
(infra#224), so enabling it was blocked behind #32/#33.
That reason never applied to gitea. gitea-mcp authenticates by PASS-THROUGH:
the bearer is a Gitea PAT, which the server validates against Gitea and then
acts as. No dedicated MCP token exists to be missing. DMABE_GITEA_API_TOKEN is
op://HomeLab and already in the session environment.
Verified on koala 2026-09-07 against tailscale serve 8446 -> NodePort 30340:
initialize returns 200 with serverInfo gitea-mcp, tools/list returns 41 tools.
Two DIFFERENT PATs both authenticate while GITEA_MCP_TOKEN 401s -- that is what
proves pass-through validation rather than a static-token comparison, and it
also shows GITEA_MCP_TOKEN is stale (it 401s against git-mcp.d-ma.be too).
cad stays parked; its blocker is real and unchanged.
Also adds bin/check-mcp-env and tests/mcp_config_test.sh, wired into task check.
The fault it gates is silent: a header interpolating an undefined variable does
not error, it sends an empty Authorization, and Claude Code reports "requires
authentication" -- indistinguishable from a revoked credential. That cost a full
investigation on koala today before the credential turned out to be fine. The
check exits 2 rather than 0 when it cannot run (no jq, no env-file, as in CI),
so an unverified config is never reported as a clean one. Servers under _parked
are exempt: referencing an unprovided variable is what parked means.
Bump-Type: minor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The frontmatter exists to be machine-read. Two shapes this template
suggested do not parse, so any ADR that copied them dropped silently out of
every "what is still open?" query while looking correct:
supersedes: - a bare dash is a YAML sequence entry
deciders: pending: Mathias the unquoted second colon reads as a
nested mapping
Both were found live during an estate-wide ADR survey on 2026-09-05:
openbanking-api ADR-0001 (the one genuinely open decision in the estate --
invisible to the survey until fixed) and infra ADR-0019, each copied
verbatim from here.
Template corrected to `null` and a quoted `"pending: <name>"`, plus a short
section after the fence explaining why and giving a one-line yaml.safe_load
check to run before committing. The explanation is deliberately OUTSIDE the
fenced block so it is not copied into the ADR being written.
main has been red since e9ad692. The gate passed; the shell suite failed on
three Antigravity cases.
Not a bug in the wiring. install.sh treats jq as optional by design — every
other target is dependency-free, so wire_antigravity warns and skips when jq is
missing. It did exactly that on the runner:
[skills] Antigravity: jq not found — skipping
The tests were the problem: they inherited whatever jq the host happened to
have. koala has it and reported 12/12; the runner does not and reported 3
failures on the same commit. Works-on-my-machine, caught by CI doing its job.
Two changes:
- The three merge-behaviour cases now run only where jq exists, and print a SKIP
naming what is not covered. A case that fails because the host lacks an
optional dependency is a false red, and false reds are how a suite stops being
read.
- The skip path gets a real test of its own, because it is a contract rather than
a fallback. install.sh gains SKILLS_JQ so the jq-absent branch is exercisable
on a host that has jq — the same seam pattern as DOCTOR_LATEST_TAG_CMD. It
asserts no skills.json is written, the skip is reported, and the other
harnesses still wire.
Verified both ways on koala by building a jq-free PATH:
with jq: install: 13 passed, 0 failed, 0 skipped
without jq: install: 10 passed, 0 failed, 3 skipped
Consequence worth naming: CI now SKIPS the merge assertions rather than running
them, so idempotency and entry-preservation are untested there until the runner
gets jq. Filed as #42.
Refs #41
Bump-Type: patch
Antigravity discovers skills via a path-entries JSON
(~/.gemini/config/skills.json), not a symlink directory. One entry points
at $AGENTS_DIR — the same managed, surfaces-filtered, collision-resolved
directory Crush/OpenCode/Pi already read — so Antigravity sees exactly the
same skill set as every other harness, for free.
Requires jq; every other target in this script stays dependency-free on
purpose, so wire_antigravity skips with a warning (not a hard failure) if
jq is missing. Merges into skills.json rather than overwriting it: the
file is documented as user-editable (json_configs.md — e.g. a personal
skills dir), so a hand-added entry must survive a re-run.
Was previously removed as "unused" (mathias/skills#41 — Antigravity usage
picked back up). Mistral Vibe stays removed.
Closes#41
Bump-Type: minor
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
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
"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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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