v0.19.3
62
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4edf42ce9f |
fix(test): run the secret guard via its own shebang, not through sh
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>
|
||
|
|
2f53346b8c |
docs(check): say plainly that CI can never verify the MCP env wiring
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> |
||
|
|
9fd5a054b3 |
fix(check): tolerate an unverifiable check instead of dying on it
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>
|
||
|
|
da76a6b3d2 |
feat(mcp): unpark the gitea server — it needs no AgentSecrets token
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> |
||
|
|
fc6d5628a7 |
fix(adr): template emitted invalid YAML frontmatter
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.
|
||
|
|
bedb6d278d |
fix(test): stop the Antigravity cases failing on a host without jq
main has been red since
|
||
|
|
e9ad692418 |
feat(install): re-add Antigravity as a wiring target
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> |
||
|
|
d6e892ac4c |
feat(skill): traceability — a chain of checked links, and a tool that says what it cannot check
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 |
||
|
|
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 |
||
|
|
40405cd95a |
fix(check): make the escalation ratchet actually ratchet
"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 |
||
|
|
39ac61c411 |
feat(skill): lineage — backward decision archaeology, with a tool
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 |
||
|
|
2cb6b54a5e |
feat(skill): adr — the house format, and how to record a decision you cannot make yet
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
|
||
|
|
0c7fac3686 |
fix(debug): close the intake gate properly — and the contradiction it exposed
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 |
||
|
|
cba0fe4337 |
docs(tdd): fix ten defects the GREEN run found in the language references
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 |
||
|
|
1d140ec525 |
feat(check): every skill states when it stops and hands back
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 |
||
|
|
b2af4e1129 |
feat(doctor): --online, for the release a host has never fetched
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 |
||
|
|
1d368d4b5d |
docs(skills): drop the Mode 2 routing notes, fix the contributing steps
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
|
||
|
|
b616c51a2c |
fix(skills): document a session_log call the tool would actually accept
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 |
||
|
|
c377c23e21 |
fix(mcp): correct the parked reason — the tokens work, the delivery does not
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 |
||
|
|
7ba01abdbe |
chore(mcp): park cad and gitea-mcp entries, blocked on vault visibility
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 |
||
|
|
5070a738e8 |
docs(review): Python adaptation files for clean-code, solid and code-review
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 |
||
|
|
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 |
||
|
|
62cfe46927 |
docs(superpowers): point the vendored cluster at the tools this harness has
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 |
||
|
|
51a2d396fd |
docs(spec): EARS-lite behavioural criteria and a context snapshot
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 |
||
|
|
08c935ba8a |
chore(debug): fold in two techniques, delete systematic-debugging
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 |
||
|
|
e12ac99465 |
docs(review): cross-link the three review skills, drop a false citation
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 |
||
|
|
d7775aac57 |
fix(ci): tag and bump separately — a queued run pushed a stale main
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 |
||
|
|
c6787e04d7 |
feat(index): generate the skills table from frontmatter
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 |
||
|
|
90a58f0b9e |
feat(skills): declare which surfaces each skill ships to
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 |
||
|
|
15737321ac |
fix(check): fail when the manifest is behind the tag, not merely different
`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 |
||
|
|
025f01aa80 |
feat(install): serve several sources instead of displacing them
~/.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 |
||
|
|
a2afb35d1d |
feat(doctor): report the installed plugin generation, not just its presence
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
|
||
|
|
21dbb35526 |
chore(skills): delete superpowers-test-driven-development
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 |
||
|
|
b188965161 |
docs(index): place experiment-spec in the relationships diagram
Missed in
|
||
|
|
e7e514390b |
docs(web-shot): resolve shot.sh from the install, not a checkout path
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 |
||
|
|
83b069afba |
docs(index): add dream + experiment-spec rows, and gate the index
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 |
||
|
|
a42ec18f40 |
fix(secret-guard): treat env as a dump only when no command follows
`env -u VAR cmd` and `env FOO=bar cmd` exec a command with a modified environment and print nothing themselves. The old rule matched any `env` token followed anywhere later by a pipe into a filter, so it blocked the documented 1Password write path and forced a `(unset VAR; cmd)` workaround. False positives only — never a leak — but a guard people route around is a guard on its way out. The rule now denies only when everything after `env` is exhausted by options and VAR=value assignments. That is strictly the dumping shape, and it also closes a hole the two previous rules left open: `env FOO=bar` with no command word prints the whole environment and was allowed. Three new deny/allow pairs pin it, including the 1Password write shape that motivated the report. 20 cases now, up from 14, all passing. Closes #25 Bump-Type: patch |
||
|
|
3112b4ce92 |
chore(ci): gate the release tagger on check + test
The repo had a `task check` structure gate and a 28-case shell test suite that nothing ever ran. The only workflow auto-tagged every push to main, so CI reported green having tested nothing — a gate that fails open, and the reason `mathias/skills` shows up as repository-blocked in the estate backlog classification. release.yml becomes ci.yml with two jobs: `gate` runs check + test on every push and PR; `tag` keeps the existing semver-from-trailer logic but declares `needs: gate`, so a red check cannot ship a tag. The pull_request trigger also gives `tbd_ship` a CI run to read on a branch head — without one it fails closed and never merges (#22). The tagger now pushes the manifest bump commit and the tag in a single `git push`. Two pushes left a window where the gate ran `task check` on the bump commit before the tag existed, and the manifest/tag parity assertion would fail on a commit that was in fact correct. tests/ci_gate_test.sh pins the wiring: PR trigger present, both tasks run, tag needs gate, no second ungated workflow file. Refs: #22 Bump-Type: minor |
||
|
|
d40169f2b7 |
feat(skill): cad-babysit — human-in-the-loop session over cad-stuck (skills#31)
release / tag (push) Successful in 1s
Wraps mathias/cad#14's `cad stuck` query surface: five-phase diagnostic order (re-derive the worklist, read issue comments FIRST, then Actions job logs, separate infrastructure fault from task outcome, decide-or-say DECISION-NEEDED), read-only by Iron Law — no dispatch, no label writes, no issue close. Every diagnosis cites a locator; every operator-only call surfaces as a structured DECISION NEEDED block instead of being decided for them. Built after cad#14 passed its live-estate acceptance test (7 real findings, locators verified) — the skill is the ritual around a query already proven to produce actionable rows, not a checklist with no input. Bump-Type: minor Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
abd81f939a |
fix(doctor): report a real harness directory as unmanaged, not as "outside"
release / tag (push) Successful in 1s
Found on flamingo during the first Mac install. ~/.config/crush/skills held real directories from a pre-plugin install, and doctor said: FAIL outside: ~/.config/crush/skills resolves to ~/.config/crush/skills, outside the active checkout ~/.local/share/skills/skills Naming a directory as its own resolution target is true and useless. Worse, it pointed at the wrong fix: "outside" means a link into a foreign checkout, which you repair by re-pointing the link. A real directory is repaired by moving it aside and re-running install.sh — install.sh already refuses to delete it, correctly, since it did not create it and cannot know what is in there. Now reported as its own class, with the skill count so it is obvious whether the directory holds a stale copy of the library or something unique that belongs in the repo: FAIL unmanaged: <dir> is a real directory, not a link into the checkout — it holds N unmanaged skill(s); move it aside and re-run install.sh The distinction matters because content in that directory reaches exactly one harness and cannot receive new skills — the failure mode is silent divergence, which is the same family as everything else this tool exists to catch. Bump-Type: patch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
68cebc0bbb |
fix(gitea-ci): hand out the call, not a copy of the build implementation (infra#309)
release / tag (push) Successful in 1s
This skill is the third factory. Two repo templates were fixed when the buildah->buildctl migration went through, and this one kept handing out the broken step for weeks afterwards -- because it is what gets consulted when adding CI to an EXISTING repo, which the templates never touch. Pasting an implementation is the actual defect. The estate now has one (mathias/infra ci-templates/build-image.sh) with byte-identical copies held in place by a drift gate, so the skill teaches the four adoption steps and the input list instead of a block to copy. Step 1 of those four -- registering in adopters.txt -- is called out explicitly, because a copy nobody registered works today and falls silently behind on the next canonical change. That is the failure this whole issue exists to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf |
||
|
|
1906a774b3 |
fix(gitea-ci): teach buildctl, not buildah (infra#241)
release / tag (push) Successful in 1s
This skill was the source of the buildah build step that infra#241 has spent a sprint removing from nine repos: rootless buildah cannot run in the containerized gitea-runner (drop ALL capabilities, allowPrivilegeEscalation false, no OCI runtime), and the failure is latent -- warm base-image layers hide it until the first cold pull. Two templates were migrated for exactly this reason (stop new repos inheriting the trap); this skill is the third factory and was still handing out the broken pattern. Also drops the `sudo k3s ctr` smoke test it recommended: written for the Arch host runner, impossible in a pod (`sudo: command not found`). The deploy job's rollout verification covers the same ground. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf |
||
|
|
7db7e0779b |
feat(mcp): serve brain and infra over Tailscale Serve with real TLS
release / tag (push) Successful in 1s
Replaces the raw NodePort URLs with Tailscale Serve endpoints on koala: brain https://koala.tail30472e.ts.net:8443/mcp -> 127.0.0.1:30330 infra https://koala.tail30472e.ts.net:8444/mcp -> 127.0.0.1:30342 Why this beats both alternatives that were tried: The public d-ma.be path exits to Cloudflare and NPM to reach a k3s service on the machine the session is running on, and Cloudflare rate-limits and size-caps tool results — the original complaint. The raw NodePort path fixed that but only worked from koala: every namespace carries default-deny-ingress and an allow policy naming the traefik namespace, so kube-proxy's KUBE-EXTERNAL-SERVICES chain RSTs external callers. Admitting tailnet clients there would need externalTrafficPolicy: Local on each service plus an ipBlock per policy, because the default Cluster policy SNATs the source and an ipBlock can never match. Serve sidesteps all of it. tailscaled terminates TLS with a genuine cert and proxies over loopback, so the request reaches the NodePort from koala itself — the case the NetworkPolicy already permits. No policy change, no externalTrafficPolicy change, no firewall rule, no cert-manager, no DNS record. Port-per-service rather than path-per-service: the mapping is 1:1 with no prefix rewriting, so what the client requests and what the server sees cannot desync. Verified: 200 authenticated in 7.6ms from koala, and `claude mcp list` reports Connected against the Serve URL. An earlier apparent OAuth resource-metadata mismatch turned out to be an artifact of testing without an Authorization header — with a valid bearer the client never enters OAuth discovery. Client requirement: `tailscale set --accept-dns=true`. Confirmed on iguana that 100.100.100.100 resolves the MagicDNS name correctly while the system resolver does not, so the name fails to resolve until Tailscale is accepted as a DNS provider. Tracked in #24 as the Mac prerequisite. Note Serve matches on SNI, so substituting the raw tailnet IP is not a workaround. Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
6151b973bc |
perf(mcp): reach brain and infra over tailnet NodePorts, not the public edge
release / tag (push) Successful in 1s
Every MCP call from Claude Code was going out through claude.ai's connector infrastructure, across the public internet, through Cloudflare and NPM on piguard, back into koala's k3s ingress — to reach a service on the same machine the session is running on. For brain that means a two-cloud round trip to read /var/lib/supervisor/brain, a file on koala's local disk. Cloudflare rate-limits and size-caps tool results on that path, which is the actual user-facing symptom. Measured from koala: brain koala:30330 200 in 6.8ms infra koala:30342 200 in 3.6ms brain public edge 200 in 27.7ms infra starts working again as a side effect: its only fault was an SNI/cert gap at the public edge (TLS "unrecognized name"), which the NodePort path does not traverse. No NPM change needed for local use. Note this is not a relocation of existing config. The mcpServers block that lived in ~/.claude/settings.json was inert — Claude Code reads server definitions from ~/.claude.json, project .mcp.json, plugins and connectors, not from settings.json. Verified: a uniquely-named server placed there is invisible to both `claude mcp list` and `claude mcp get`. So before the plugin existed, koala had no direct path at all and every call went via Cloudflare by construction. That also explains how a stale GITEA_MCP_TOKEN went unnoticed for five days: nothing consumed it. Safe to land now because koala is the only host with the plugin installed (#24). The koala firewall must allow 30330/30342 on the Tailscale interface — NOT on the LAN broadly, so tailnet membership stays the perimeter — before the Macs get the plugin. Tracked as a prerequisite in #24. Measured today: those ports are refused from iguana over both LAN and tailnet while :22 answers, so the rule does not exist yet. Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
5c8341c797 |
fix(mcp): drop the gitea server — it was a redundant, silently stale copy
release / tag (push) Successful in 2s
Every gitea__* call from Claude Code on koala already resolves through the claude.ai connector, not through this env-var-authenticated HTTP server. The entry was a second path to the same service that nothing actually used. That redundancy is exactly why it rotted unnoticed. GITEA_MCP_TOKEN on koala resolves ~/.op-env -> op://HomeLab/GITEA_MCP_TOKEN, an item orphaned by the 2026-07-30 consolidation that moved gitea-mcp, agentsquad-serve and crush-runner onto a single shared item in the AgentSecrets vault. The host env-file was never migrated and cannot be: koala's 1Password service account has no access to AgentSecrets, while onepassword-connect in-cluster maps both vaults. So the host copy diverged and stayed diverged for five days. The cluster was never affected — zero 401s from the pod CIDR in 30 days, every rejection came from the gateway. Deleting the divergent copy beats maintaining a second source of truth for a token this host does not need. brain stays: it authenticates, and it is genuinely used from the plugin. infra stays but is currently unreachable for an unrelated reason — infra-mcp.d-ma.be fails TLS with "unrecognized name", an SNI/cert gap at the NPM edge, tracked separately. Bump-Type: patch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
e2a312d05e |
fix(ci): anchor the release-skip marker to a trailer, and test it
release / tag (push) Successful in 1s
The tagger searched the entire commit message for a bracketed marker as a substring. The commit that introduced the manifest-bump step described that marker in its own body — so the tagger read its own documentation as an instruction and skipped the release. Run 15 reported success with no tag, no manifest bump and no error. Same failure family as everything else in this sweep: the wrong thing happened silently. The rule is now a git trailer matched anchored to its own line, consistent with Bump-Type, and it lives in bin/should-skip-release rather than untested inside YAML. tests/release_skip_test.sh pins six cases, including the exact regression: a message that spells the marker out in prose must still tag. A marker you cannot mention without triggering is a footgun, not a mechanism. Bump-Type: patch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
78b9b59eca |
docs+ci: document the plugin path, bump plugin.json version on release
release / tag (push) Successful in 1s
CI now writes the computed semver into .claude-plugin/plugin.json before tagging, committed with [skip-release] so it does not re-trigger the tagger. Claude Code gates plugin updates on that field: without the bump, installed hosts never see a new tag no matter how many releases go out. `task check` enforces manifest/tag equality so the two cannot silently diverge. Docs rewritten around the two delivery paths, and the checkout location is now stated as load-bearing rather than incidental: SKILLS_CHECKOUT_DIR defaults to ~/.local/share/skills because that is host-local and outside any synced directory. The previous docs told you to clone into ~/dev, which on this fleet is an iCloud folder shared by two Macs spanning two path roots — a git working tree with concurrent writers, and absolute links that are broken on one side by construction. That advice is what #23 is about; it is now inverted with the reason attached. Antigravity and Vibe removed from the docs along with their targets. Bump-Type: patch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
88c54399fe |
feat(doctor): fail loud on skills drift, wired to SessionStart
Every failure this fleet actually hit was silent. 243 dangling symlinks were
invisible to git because **/.claude/skills/ is in the global gitignore. A
checkout 20 skills behind still resolved, so nothing complained. A loose
secret-hygiene.md loaded in no harness and errored in none. Two machines had
the library missing entirely. Exit status is the signal that was absent.
bin/doctor reports, with a non-zero exit:
- dangling links, anywhere under the harness dirs
- links that RESOLVE but land outside the active checkout — the dangerous
case, because it works while serving stale content
- skill-count mismatch between a harness and the checkout
- stray entries where <name>/SKILL.md was expected
- leftover per-skill Claude symlinks (plugin skills do not override
same-named user skills, so leftovers mean everything loads twice)
- divergence between the plugin copy and the copy the other harnesses link to
Wired as a SessionStart hook with --quiet, so a healthy host prints nothing and
a drifting one says so at the top of the next session. That beats a timer:
no launchd/systemd wiring, and it reaches every host that installs the plugin.
27ms on koala, no network, no git fetch.
Two design points found by the tests, both worth keeping:
- The canonical checkout is whatever ~/.agents/skills resolves to, NOT the
copy doctor runs from. As a plugin hook $0 lives in the plugin cache, which
is legitimately a second copy; judging harness links against it flagged
every one of them as "outside".
- "cd X/.." resolves ".." lexically against the link path and lands back in
~/.agents. Resolve the link first, then take the parent.
tests/doctor_test.sh drives seven fixtures through $HOME and $DOCTOR_CHECKOUT,
so no test touches the real host. Each asserts both the non-zero exit and that
the report names the fault — an exit-code-only assertion would pass while the
message said nothing useful.
Bump-Type: minor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
|
||
|
|
747c6c7f4c |
feat(plugin): ship the secret-guard and pre-task-ritual hooks + MCP servers
The plugin now carries the two hooks Mathias owns and the brain/gitea/infra
MCP server definitions, not just skills.
Why this matters more than tidiness: both hooks existed ONLY in koala's
~/.claude/, which is host-local and unsynced. flamingo and iguana have been
running with no secret-guard at all — that is the control which blocks the
decrypt-and-print shape behind the 2026-08-05 ntfy-token leak, the op-item-get
shape behind 2026-07-30, and the base64-bypass shape behind 2026-06-11. Its
absence on two of three machines is a security gap, not a config gap. Shipping
it in the plugin means installing the plugin installs the guard.
.mcp.json references tokens as ${BRAIN_MCP_TOKEN} / ${GITEA_MCP_TOKEN} only —
env indirection, never literal values, so the manifest is safe to commit.
tests/secret_guard_test.sh pins 14 cases. It asserts on
hookSpecificOutput.permissionDecision, NOT exit status: the guard always exits
0 by contract, so an exit-status assertion would pass vacuously while the guard
silently allowed everything. Each deny case names the incident that produced it.
gsd-* hooks are deliberately NOT migrated — GSD owns and updates those in place.
Bump-Type: minor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
|
||
|
|
56a947b2a0 |
refactor(plugin)!: ship as a Claude Code plugin, one dir link per harness
Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so it is now a marketplace hosting exactly one plugin whose root is the repo root (source: "."). Claude Code installs it from git; no symlinks involved. Why: install.sh wired the library into every consuming repo as an ABSOLUTE symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated the links — 243 dangling links across 12 repos, hidden from git by the **/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots (/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an absolute link stored there is broken on one side by construction. Re-running the installer cannot fix that; only removing the mechanism can. install.sh keeps the harnesses with no plugin system and drops from 42 links per harness to one directory link, via the convergent ~/.agents/skills convention that Pi and OpenCode both read: ~/.agents/skills -> $CHECKOUT_DIR/skills ~/.config/crush/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills wire_claude_repo is deleted outright rather than gated behind a flag — on a mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go too; both unused. CHECKOUT_DIR keeps its host-local default deliberately: pointing it into ~/dev would put a git working tree inside iCloud with two concurrent writers. Taskfile drops five near-identical wiring loops (they had already drifted) and delegates to install.sh. `task check` gains frontmatter validation, a stray-root-entry gate, and manifest/tag version parity — the loose secret-hygiene.md sat unloadable for months because nothing checked layout. Verified: claude plugin validate . passes clean; a probe skill loaded as skills:zzprobe through --plugin-dir; task check green at 42 skills. Refs: #23 Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
21be3ea245 |
feat(skills): add secret-hygiene skill
release / tag (push) Successful in 0s
Was a loose secret-hygiene.md at the root of ~/.claude/skills — not dir/SKILL.md, so no harness ever loaded it. Promoted to a real skill so it installs to Claude Code, Crush, Antigravity and Vibe alike. Frontmatter description carries load-before triggers (kubectl exec/env, whole-file SOPS decrypt, token rotation) so it fires ahead of the command that would leak, not after. Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m |
||
|
|
85e125703e |
feat(own-the-task): add delegation-ownership skill (#21)
release / tag (push) Successful in 1s
|
||
|
|
1256653319 |
fix(ci): release.yml tag job dies whenever a commit has no Bump-Type footer (skills#8)
release / tag (push) Successful in 1s
set -euo pipefail + `... | grep -iE '^Bump-Type:' | head -1 | awk ...` inside `bump=$(...)`: when the last commit has no Bump-Type footer (the normal default-to-patch case), grep exits 1 on no-match. Under pipefail, that propagates as the whole pipeline's exit status even though head/awk both succeed -- set -e then kills the script before it ever reaches the case statement. Broke on every push since 2026-06-22 once a footer-less commit landed after the last one that happened to carry a Bump-Type line. Appended `|| true` to swallow the expected no-match case. |
||
|
|
7292e50d12 |
docs(index): recommend install.sh — task install is broken
release / tag (push) Failing after 1s
Verified this session: `task install` (and its sub-targets) wipe/skip the per-skill symlinks (0/40 across all harnesses), while install.sh wires 40/40 reliably. Point the wiring docs at install.sh until the Taskfile bug is fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cf19fc52ce |
docs(index): rewrite "Wiring into tools" for the repo-native installer
release / tag (push) Failing after 1s
The index still described local-dev's old wire-skills.sh / ~/dev/.skills mechanism. This repo is now the canonical home; wiring is `task install` (claude global+repo, crush, antigravity, vibe) or the curl|bash install.sh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef72982489 |
feat(install): add Mistral Vibe wiring target (parity with local-dev)
release / tag (push) Failing after 1s
Ports the Vibe wiring from local-dev's wire-skills.sh so this repo's installer covers all four harnesses. Vibe uses a flat layout — ~/.vibe/skills/<name>.md symlinked to the skill's SKILL.md file (not the directory, unlike Claude/Crush/ Antigravity). Added to both `task install` and install.sh, idempotent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e29c6d1af |
feat(skills): import 17 skills from local-dev; fix stale host
release / tag (push) Failing after 1s
Consolidating the divergence between this repo and local-dev's embedded ~/dev/.skills/ (the two had drifted; the 19 overlapping skills were byte-identical). - Import the 17 skills that existed only in local-dev: discovery-framing, stage-gate-review, web-shot, and the 14 superpowers-* skills. This repo is now the superset / single source of truth. - SKILLS_INDEX.md: add rows for the 17. - install.sh + Taskfile.yml: git.d-ma.be host (was stale gitea.d-ma.be, which broke the one-line curl|bash installer post-rename). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8f14c572b9 |
fix(skills): add YAML frontmatter to telos-load + regulatory-risk-assessment
release / tag (push) Successful in 1s
The two new skills shipped without the name/description frontmatter every other skill carries, so harnesses surfaced them as a bare directory name with no description to match against intent. Add frontmatter (body unchanged). Bump-Type: patch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7d8cfc3cc |
feat: add telos-load and regulatory-risk-assessment skills
release / tag (push) Successful in 1s
- telos-load: harness-agnostic TELOS session context loading (closes #4) - regulatory-risk-assessment: CAD compliance gate + risk register (closes #3) Bump-Type: minor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5a2bef35c1 |
fix(ci): runs-on self-hosted to match koala runner labels
release / tag (push) Successful in 1s
The koala act_runner has labels [self-hosted, linux, amd64]. The default ubuntu-latest label doesn't match, so the release workflow silently produced zero runs on every push. Matches the convention used by hyperguild + infra workflows. Runner is offline at time of this commit — bring it up on koala via the act_runner systemd unit before expecting tags to appear. [skip-release] |
||
|
|
b1ff71a7fd |
feat: wire Crush + Antigravity harnesses
release / tag (push) Has been cancelled
Phase 2 of mathias/skills extraction. Adds two new per-tool wirers alongside Claude Code: - Crush — ~/.config/crush/skills/<name>/SKILL.md (charmbracelet/crush, successor to opencode-ai/opencode which is archived) - Antigravity — ~/.gemini/antigravity/skills/<name>/SKILL.md (Google VS Code extension). Our SKILL.md frontmatter (name + description) is already in the format Antigravity expects — no manifest translation step needed. Both wirers added to Taskfile.yml (install:crush + install:antigravity) and install.sh (wire_crush + wire_antigravity). The aggregate `install` target now calls all four targets (claude:global, claude:repo, crush, antigravity). Idempotent symlinks, same shape as the Claude Code wirer. Per-host env overrides documented in README: SKILLS_REF for tag pinning, CRUSH_SKILLS_DIR + ANTIGRAVITY_SKILLS_DIR for non-default paths. Skipped: - opencode (opencode-ai/opencode): archived, succeeded by Crush; its Custom Commands surface is user prompt templates, not system skills - gitea-resident agents: consume via per-repo .claude/skills or brain MCP, no special target needed Bump-Type: minor |
||
|
|
d6a71e370e |
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
release / tag (push) Has been cancelled
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill dirs + SKILLS_INDEX.md) and adds the installation surface: - Taskfile.yml — install / update / list / release / check targets - install.sh — bootstrap installer for hosts without Task. Idempotent symlink wirer; default checkout at ~/.local/share/skills/ on every host; SKILLS_REF env var pins a tag (default: main). - .gitea/workflows/release.yml — auto-tag every push to main by Bump-Type footer (major/minor/patch, default patch). Skipped when commit contains [skip-release]. - README — usage, versioning, contribution flow, secret-hygiene rule. Phase 1 wires Claude Code only (~/.claude/skills/<name> global + <repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode, antigravity, and gitea-resident agents (cobalt-dingo, agentsquad) once their skill conventions are researched. Public repo, markdown-only — no secrets, no client names. Verified via pre-push grep before initial push. [skip-release] |