v0.22.1
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c25175e4b3 |
feat(agents): ship pr-gatekeeper from the repo, not one host's ~/.claude (#51)
pr-gatekeeper blocked dispatch#75 five times. Every block was a real,
executable escape — twelve pathspec bypasses, arbitrary command
execution via `git -c alias`, `--upload-pack='rm …'`, a write primitive
through `--output=`, `${IFS}` word-splitting past the policy, and a path
denylist that had been a no-op in the live flow since it was written.
task check was green on all five.
The agent definition existed in no repository. A code search across the
estate found only references to it — comments in dispatch source, a
local-dev sprint doc — while the file itself lived at
~/.claude/agents/pr-gatekeeper.md on koala alone: unversioned, on one
host, one rm from gone. local-dev AGENT.md rule 8 now requires routing
control code through it, so doctrine depended on an artifact with no
history.
PORTED, PLUS WHAT THE FIVE ROUNDS ACTUALLY TAUGHT
The file already had flip-testing. Three habits that did the finding were
implied at best, and are now explicit (§4b):
- Verify by EXECUTION, not by reading the implementation. Every finding
came from running the command in a scratch repo and looking at the
filesystem. The matcher said one thing and the filesystem another,
five times.
- Read the SURVIVORS. After an allow/deny sweep, read what is still
permitted, one at a time. This caught `git commit -F /etc/passwd` —
exfiltration wearing a message flag — which neither the review nor
the differential found, because both implementations agreed and both
were wrong. Agreement is not correctness.
- Grade a tool on its EXIT CODE, never by grepping output. A crashed
linter greps as clean.
Plus rule 9 in §4: a test whose name claims more than it proves is worse
than no test, because the name wins in the reader's head.
AND THE INCIDENT THIS AGENT CAUSED (§4c)
On 2026-09-19 a probe of this agent's own making printed /proc/$PPID/environ
and `ps e` raw, and a `git credential fill` consulted the host's real
store. Seven live credentials reached the transcript and need rotation
(infra#445). The finding was correct; the method was not. Versioning the
file without that lesson would have shipped the incident along with the
agent. So: a probe that reads an environment prints a count, never the
buffer; invented sentinels only; isolated HOME for anything touching
git's credential subsystem; prove a primitive and stop.
DOCTOR
New `shadow` check: a real file in ~/.claude/agents with the same name as
a shipped agent wins over the shipped one and nothing says so. Same
silent-drift shape as the existing `outside` and `unmanaged` checks — it
works, so nothing complains, while serving content nobody updates. A
symlink is managed wiring and is not flagged.
The host-local copy on koala has been moved aside, so the shipped file is
what runs once the plugin updates.
TESTS
14 assertions, each named for what it observes rather than what it
guarantees — a grep proves a phrase is present, not that a procedure is
followed, and naming it otherwise would be the defect rule 9 exists for.
Flip-tested both directions: removing the doctor check, and altering the
probe-safety phrase, each turn the suite red.
`agents` added to task check's root-entry allowlist. Leaving it out made
check_tolerates_unverifiable_test.sh fail, which is #23's stray-entry
guard working exactly as designed — an undeclared root entry loads in no
harness and errors in none either.
Closes #51
Bump-Type: minor
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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] |