Commit Graph
47 Commits
Author SHA1 Message Date
mathias 61c68c01b0 chore(release): plugin.json version 0.22.1
Skip-Release: true
2026-10-01 20:22:24 +00:00
mathiasandClaude Opus 5 55b2e811d1 chore(release): 0.22.0 — pr-gatekeeper pre-push mode
CI / Check / Test (push) Successful in 3s
CI / Tag release (push) Successful in 0s
pr-gatekeeper gains a second mode: reviewing a staged or unpushed diff in a
GitOps repo where a branch cannot be reconciled, recording its verdict on the
issue instead of merging. New capability, so minor.

Refs mathias/infra#470

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 10:32:30 +02:00
mathias c25175e4b3 feat(agents): ship pr-gatekeeper from the repo, not one host's ~/.claude (#51)
CI / Check / Test (push) Successful in 4s
CI / Tag release (push) Successful in 1s
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
2026-09-28 23:30:37 +02:00
mathias e2a93c1e99 chore(release): plugin.json version 0.20.1
Skip-Release: true
2026-09-24 17:44:54 +00:00
mathiasandClaude Opus 5 2399ce68e5 chore(release): plugin.json version 0.20.0
CI / Check / Test (push) Successful in 9s
CI / Tag release (push) Successful in 4s
New skill (document-to-markdown) is a minor bump; the gate asserts
manifest/tag parity, so this lands with the tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 19:44:28 +02:00
mathias cf3abafc66 chore(release): plugin.json version 0.19.6
Skip-Release: true
2026-09-11 09:11:36 +00:00
mathias f031c2b77d chore(release): plugin.json version 0.19.5
Skip-Release: true
2026-09-08 19:35:09 +00:00
mathias 6cc224f151 chore(release): plugin.json version 0.19.4
Skip-Release: true
2026-09-07 20:34:58 +00:00
mathias 400d2b9b6b chore(release): plugin.json version 0.19.3
Skip-Release: true
2026-09-07 20:23:31 +00:00
mathias dcc0677270 chore(release): plugin.json version 0.19.2
Skip-Release: true
2026-09-07 18:14:53 +00:00
mathiasandClaude Opus 5 9fd5a054b3 fix(check): tolerate an unverifiable check instead of dying on it
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
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>
2026-09-07 20:14:19 +02:00
mathiasandClaude Opus 5 da76a6b3d2 feat(mcp): unpark the gitea server — it needs no AgentSecrets token
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
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>
2026-09-07 07:24:23 +02:00
mathias 1f7f53e6c0 chore(release): plugin.json version 0.18.1
Skip-Release: true
2026-08-30 09:49:22 +00:00
mathias 86529d2ac5 chore(release): plugin.json version 0.18.0
Skip-Release: true
2026-08-29 13:34:27 +00:00
mathias 88a4fa407b chore(release): plugin.json version 0.17.1
Skip-Release: true
2026-08-29 13:27:06 +00:00
mathias 808af7b020 chore(release): plugin.json version 0.17.0
Skip-Release: true
2026-08-29 13:27:05 +00:00
mathias 1448963823 chore(release): plugin.json version 0.16.0
Skip-Release: true
2026-08-29 13:19:26 +00:00
mathias 58687c68a6 chore(release): plugin.json version 0.15.1
Skip-Release: true
2026-08-29 13:19:25 +00:00
mathias 06592805b6 chore(release): plugin.json version 0.15.0
Skip-Release: true
2026-08-29 12:56:44 +00:00
mathias 2fcc15e9e4 chore(release): plugin.json version 0.14.0
Skip-Release: true
2026-08-29 12:53:49 +00:00
mathias 3c8d2f5613 chore(release): plugin.json version 0.13.4
Skip-Release: true
2026-08-29 12:50:31 +00:00
mathias a565b04e91 chore(release): plugin.json version 0.13.3
Skip-Release: true
2026-08-29 12:49:56 +00:00
mathias b8bacfa7b3 chore(release): plugin.json version 0.13.2
Skip-Release: true
2026-08-29 12:22:35 +00:00
mathias 8f9d5dd8bd chore(release): plugin.json version 0.13.1
Skip-Release: true
2026-08-29 12:19:31 +00:00
mathias 9a81a707af chore(release): plugin.json version 0.13.0
Skip-Release: true
2026-08-29 12:10:37 +00:00
mathias 6e5024d498 chore(release): plugin.json version 0.12.1
Skip-Release: true
2026-08-29 09:48:12 +00:00
mathias de84800892 chore(release): plugin.json version 0.12.0
Skip-Release: true
2026-08-29 09:41:44 +00:00
mathias bb912e5594 chore(release): plugin.json version 0.11.0
Skip-Release: true
2026-08-29 09:34:36 +00:00
mathias 84a012cd0f chore(release): plugin.json version 0.10.2
Skip-Release: true
2026-08-29 09:28:32 +00:00
mathias dc97fd4a36 chore(release): plugin.json version 0.10.1
Skip-Release: true
2026-08-29 09:25:28 +00:00
mathias d7775aac57 fix(ci): tag and bump separately — a queued run pushed a stale main
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Three runs failed with `! [rejected] HEAD -> main (non-fast-forward)`. The
runner is maxParallel=1 and shared with mathias/infra, so those jobs started
five minutes and three commits after their own push. The tagger checked out
its commit and pushed that stale HEAD to main, which main had long left behind.

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

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

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

Refs #22
Bump-Type: patch
2026-08-29 11:25:20 +02:00
mathias eee62e4fc0 chore(release): plugin.json version 0.9.0
Skip-Release: true
2026-08-29 08:01:01 +00:00
mathias ad17a7c6c0 chore(release): plugin.json version 0.8.5
Skip-Release: true
2026-08-29 07:56:02 +00:00
mathias 8ad2079f86 chore(release): plugin.json version 0.8.4
Skip-Release: true
2026-08-29 07:54:53 +00:00
mathias ae66840a03 chore(release): plugin.json version 0.8.3
Skip-Release: true
2026-08-29 07:53:48 +00:00
mathias 087685f2e3 chore(release): plugin.json version 0.8.2
Skip-Release: true
2026-08-29 07:52:16 +00:00
mathias a699ad8cbd chore(release): plugin.json version 0.8.1
Skip-Release: true
2026-08-29 07:50:52 +00:00
mathias 46ff1acdfe chore(release): plugin.json version 0.8.0
Skip-Release: true
2026-08-29 07:47:07 +00:00
mathias 51a8c25b4d chore(release): plugin.json version 0.7.0
Skip-Release: true
2026-08-28 11:09:50 +00:00
mathias d4f0416dc5 chore(release): plugin.json version 0.6.3
Skip-Release: true
2026-08-26 05:54:12 +00:00
mathias e42d2ba9cb chore(release): plugin.json version 0.6.2
Skip-Release: true
2026-08-25 08:54:05 +00:00
mathias f50dd6be6a chore(release): plugin.json version 0.6.1
Skip-Release: true
2026-08-25 05:04:23 +00:00
mathias 38f905e705 chore(release): plugin.json version 0.6.0
Skip-Release: true
2026-08-24 19:16:00 +00:00
mathias 0be3506799 chore(release): plugin.json version 0.5.0
Skip-Release: true
2026-08-24 16:01:14 +00:00
mathias 6c1b6ce9db chore(release): plugin.json version 0.4.2
Skip-Release: true
2026-08-24 15:31:15 +00:00
mathias 5ca44c9c6b chore(release): plugin.json version 0.4.1
Skip-Release: true
2026-08-23 12:39:55 +00:00
mathiasandClaude Opus 5 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
2026-08-23 14:29:51 +02:00