Compare commits

...
23 Commits
Author SHA1 Message Date
mathias 21dbb35526 chore(skills): delete superpowers-test-driven-development
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 0s
Verified duplicate rather than assumed one. Section for section it is the same
skill as `tdd` — Overview, When to Use, Iron Law, Red-Green-Refactor with the
same dot graph, Good Tests, Common Rationalizations, Red Flags, Verification
Checklist, When Stuck, Final Rule — with TypeScript examples and obra's "your
human partner" phrasing instead of ours. `tdd` carries all of it plus the
Go-specific material (table-driven tests, t.Run, testify, go test -race), the
brain MCP integration and the Mode 2 routing note.

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

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

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

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

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

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

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

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

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

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

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

Closes #15
Bump-Type: patch
2026-08-29 09:52:03 +02:00
mathias a699ad8cbd chore(release): plugin.json version 0.8.1
Skip-Release: true
2026-08-29 07:50:52 +00:00
mathias a42ec18f40 fix(secret-guard): treat env as a dump only when no command follows
CI / Tag release (push) Successful in 1s
CI / Check / Test (push) Successful in 1s
`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
2026-08-29 09:50:08 +02:00
mathias 46ff1acdfe chore(release): plugin.json version 0.8.0
Skip-Release: true
2026-08-29 07:47:07 +00:00
mathias 3112b4ce92 chore(ci): gate the release tagger on check + test
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Successful in 0s
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
2026-08-29 09:46:10 +02:00
mathias 51a8c25b4d chore(release): plugin.json version 0.7.0
Skip-Release: true
2026-08-28 11:09:50 +00:00
mathiasandClaude Sonnet 5 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>
2026-08-28 13:09:31 +02:00
mathias d4f0416dc5 chore(release): plugin.json version 0.6.3
Skip-Release: true
2026-08-26 05:54:12 +00:00
mathiasandClaude Opus 5 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
2026-08-26 07:53:14 +02:00
mathias e42d2ba9cb chore(release): plugin.json version 0.6.2
Skip-Release: true
2026-08-25 08:54:05 +00:00
mathiasandClaude Opus 5 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
2026-08-25 10:54:02 +02:00
mathias f50dd6be6a chore(release): plugin.json version 0.6.1
Skip-Release: true
2026-08-25 05:04:23 +00:00
mathiasandClaude Opus 5 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
2026-08-25 07:00:50 +02:00
mathias 38f905e705 chore(release): plugin.json version 0.6.0
Skip-Release: true
2026-08-24 19:16:00 +00:00
mathiasandClaude Opus 5 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
2026-08-24 21:11:49 +02:00
mathias 0be3506799 chore(release): plugin.json version 0.5.0
Skip-Release: true
2026-08-24 16:01:14 +00:00
mathiasandClaude Opus 5 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
2026-08-24 18:00:17 +02:00
17 changed files with 605 additions and 740 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "skills", "name": "skills",
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.", "description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
"version": "0.4.2", "version": "0.8.4",
"author": { "author": {
"name": "Mathias", "name": "Mathias",
"url": "https://git.d-ma.be/mathias" "url": "https://git.d-ma.be/mathias"
@@ -1,23 +1,52 @@
name: release name: CI
# Auto-tag every push to main with a semver bump derived from the latest # Two jobs, in order:
# commit footer:
# Bump-Type: major → vX.0.0
# Bump-Type: minor → v0.Y.0
# (default) → v0.0.Z (patch)
# #
# Skips when the latest commit is itself the auto-tagger (commit message # gate — `task check` (structure, frontmatter, manifest/tag parity) plus
# contains "[skip-release]") to prevent recursion. Tag is pushed back # `task test` (hooks, doctor, release-skip). Runs on every push and
# to the repo using the GITEA_TOKEN provided by act_runner. # every PR. Before this existed the suite ran nowhere: CI reported
# green having tested nothing (#22).
#
# tag — auto-tags a push to main with a semver bump derived from the latest
# commit footer:
# Bump-Type: major → vX.0.0
# Bump-Type: minor → v0.Y.0
# (default) → v0.0.Z (patch)
# Skips when the commit carries the `Skip-Release: true` trailer (the
# tagger's own manifest bump) to prevent recursion. Gated on `gate`,
# so a red check cannot ship a tag.
on: "on":
push: push:
branches: branches: ["**"]
- main pull_request:
branches: [main]
jobs: jobs:
tag: gate:
name: Check / Test
runs-on: self-hosted runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
# `task check` compares plugin.json's version to `git describe`;
# a shallow checkout has no tags and would skip that assertion.
fetch-depth: 0
- name: Verify toolchain
run: task --version
- name: Structure gate
run: task check
- name: Shell test suite
run: task test
tag:
name: Tag release
needs: gate
runs-on: self-hosted
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -72,16 +101,19 @@ jobs:
manifest=.claude-plugin/plugin.json manifest=.claude-plugin/plugin.json
version="${TAG#v}" version="${TAG#v}"
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
git tag -a "$TAG" -m "release $TAG"
if [ "$current" != "$version" ]; then if [ "$current" != "$version" ]; then
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest" sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
git add "$manifest" git add "$manifest"
# The trailer below stops this push from re-triggering the tagger. # The trailer below stops this push from re-triggering the tagger.
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true" git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
git push origin HEAD:main # Commit and tag go up in ONE push: the gate job runs `task check`
# on the bump commit, and that asserts manifest == latest tag. Two
# separate pushes leave a window where it legitimately fails.
git push origin HEAD:main "$TAG"
echo "bumped $manifest to $version" echo "bumped $manifest to $version"
else
git push origin "$TAG"
fi fi
git tag -a "$TAG" -m "release $TAG"
# gitea-actions checkout uses an x-access-token URL; reuse it.
git push origin "$TAG"
echo "pushed $TAG" echo "pushed $TAG"
+4 -3
View File
@@ -1,15 +1,16 @@
{ {
"_comment": "Tailscale Serve on koala, not the public d-ma.be hostnames. The public path exits to Cloudflare and NPM to reach a k3s service on the same machine most sessions run on, which imposes rate limits and message-size caps on tool results. Serve terminates TLS with a real Tailscale cert and proxies to the NodePort over loopback, so traffic reaches the service from koala itself and satisfies the default-deny-ingress NetworkPolicy with no cluster change. Clients need 'tailscale set --accept-dns=true' for the MagicDNS name to resolve; see mathias/skills#24.",
"mcpServers": { "mcpServers": {
"brain": { "brain": {
"type": "http", "type": "http",
"url": "https://brain-mcp.d-ma.be/mcp", "url": "https://koala.tail30472e.ts.net:8443/mcp",
"headers": { "headers": {
"Authorization": "Bearer ${BRAIN_MCP_TOKEN}" "Authorization": "Bearer ${BRAIN_MCP_TOKEN}"
} }
}, },
"infra": { "infra": {
"type": "http", "type": "http",
"url": "https://infra-mcp.d-ma.be/mcp" "url": "https://koala.tail30472e.ts.net:8444/mcp"
} }
} }
} }
+13 -9
View File
@@ -19,9 +19,11 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | | `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious | | `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | | `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature |
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | | `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | | `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation | | `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index |
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | | `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists | | `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry | | `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
@@ -30,13 +32,13 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | | `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | | `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists | | `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present |
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | | `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding | | `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | | `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | | `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents |
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | | `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically | | `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
| `superpowers-test-driven-development` | TDD discipline (superpowers variant) | Red → green → refactor on a change |
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete | | `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | | `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | | `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments |
@@ -106,14 +108,16 @@ problem-analysis
│ │
├── spec-driven-dev (PM-kickoff lifecycle) ├── spec-driven-dev (PM-kickoff lifecycle)
│ │ │ │
│ └── feature-spec (per-feature, mid-implementation) │ ├── feature-spec (per-feature, mid-implementation)
│ │ │ │ │
│ └── planning │ │ └── planning
│ │ │ │ │
│ └── atdd ──→ tdd (foundation) │ │ └── atdd ──→ tdd (foundation)
│ ├──→ debug (when tests fail) │ │ ├──→ debug (when tests fail)
│ ├──→ clean-code (REFACTOR phase) │ │ ├──→ clean-code (REFACTOR phase)
│ └──→ test-design (review) │ │ └──→ test-design (review)
│ │
│ └── experiment-spec (research/ML phase, instead of feature-spec)
│ │
└── planning └── planning
+6 -1
View File
@@ -94,7 +94,12 @@ tasks:
count=$((count + 1)) count=$((count + 1))
done done
# 3. Plugin manifest version must match the latest tag, or installers # 3. SKILLS_INDEX.md lists exactly the skills that exist. Delegated
# to bin/check-index so the rule is covered by
# tests/check_index_test.sh rather than living untested here.
sh '{{.TASKFILE_DIR}}/bin/check-index' '{{.TASKFILE_DIR}}' || fail=1
# 4. Plugin manifest version must match the latest tag, or installers
# never see the update (Claude Code gates updates on this field). # never see the update (Claude Code gates updates on this field).
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1) manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# check-index — asserts SKILLS_INDEX.md lists exactly the skills that exist.
#
# The index is hand-maintained and drifts silently in both directions: a skill
# ships with no row (`dream` and `experiment-spec` did — their rows lived on
# branches squash-merged after a directory reorg, so the SKILL.md landed and
# the row did not, #15), or a row outlives the skill it names. Neither errors
# anywhere; the library just misreports itself to every harness that reads it.
#
# Usage: check-index [checkout-root] (defaults to this script's repo root)
set -u
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
INDEX="$ROOT/SKILLS_INDEX.md"
fail=0
if [ ! -f "$INDEX" ]; then
echo "ERROR: missing SKILLS_INDEX.md" >&2
exit 1
fi
# A row is a table line whose first cell is the skill name in backticks.
listed=$(sed -n 's/^|[[:space:]]*`\([^`]*\)`.*/\1/p' "$INDEX" | sort -u)
for dir in "$ROOT"/skills/*/; do
name=$(basename "$dir")
[ -f "$dir/SKILL.md" ] || continue
printf '%s\n' "$listed" | grep -qx "$name" || {
echo "ERROR: skills/$name has no row in SKILLS_INDEX.md" >&2
fail=1
}
done
for name in $listed; do
[ -f "$ROOT/skills/$name/SKILL.md" ] || {
echo "ERROR: SKILLS_INDEX.md lists '$name', which has no skills/$name/SKILL.md" >&2
fail=1
}
done
exit $fail
+9
View File
@@ -109,6 +109,15 @@ do
continue continue
fi fi
# A real directory where a link belongs holds content nothing manages: it
# cannot receive new skills and silently shadows the checkout. Distinguished
# from the foreign-checkout case below because the fix differs — move it
# aside and re-run install.sh, rather than re-point a link.
if [ ! -L "$dir" ] && [ -d "$dir" ]; then
add FAIL unmanaged "$dir is a real directory, not a link into the checkout — it holds $(count_skills "$dir") unmanaged skill(s); move it aside and re-run install.sh"
continue
fi
# Resolve to a physical path and confirm it lands inside THIS checkout. # Resolve to a physical path and confirm it lands inside THIS checkout.
# A link that resolves to a different checkout is the dangerous case: it # A link that resolves to a different checkout is the dangerous case: it
# works, so nothing complains, while serving stale content. # works, so nothing complains, while serving stale content.
+12 -5
View File
@@ -50,11 +50,18 @@ if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
fi fi
# Raw env/secret-file dumps (infra#228/#229 root cause). # Raw env/secret-file dumps (infra#228/#229 root cause).
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)env(\s|$).*\|.*grep'; then #
deny "Blocked: 'env | grep' prints matching values in full. Use an existence-only check: [ -n \"\$VAR\" ] && echo set" # `env` is a dump only when nothing but options and VAR=value assignments
fi # follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
if printf '%s' "$CMD" | grep -Eq '(^|[;&|])[[:space:]]*env[[:space:]]*($|[;&|])'; then # modified environment and print nothing themselves — piping those into grep
deny "Blocked: bare 'env' prints every variable's value, including secrets. Use an existence-only check: [ -n \"\$VAR\" ] && echo set" # filters the command's own output, not the environment (#25). The earlier
# pattern matched any `env` token followed anywhere later by a pipe into
# grep, which blocked the documented 1Password write path
# (`env -u OP_SERVICE_ACCOUNT_TOKEN op item create`) and cost a workaround.
# Denying only the "no command word follows" shape also catches `env FOO=bar`
# with no command, which the two previous rules let through.
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|[[:space:]])env([[:space:]]+-[iu][[:space:]]*[A-Za-z_][A-Za-z0-9_]*|[[:space:]]+-[iu]|[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)*[[:space:]]*($|[|;&])'; then
deny "Blocked: this 'env' prints every variable's value, including secrets (no command word follows it, so it dumps rather than execs). Use an existence-only check: [ -n \"\$VAR\" ] && echo set. Note 'env -u VAR cmd' and 'env FOO=bar cmd' are allowed — they run a command."
fi fi
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set" deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
+263
View File
@@ -0,0 +1,263 @@
---
name: cad-babysit
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
---
# CAD Babysit
## Overview
A deliberately narrow session: pick up whatever CAD has run into trouble on, work it with a
human present, and stop. This is not an autonomous supervisor — it is a babysitter with a
person in the room, and that is the point, not a limitation to route around later.
**Core principle:** an unattended supervisor reproduces the exact failure it exists to catch.
`dispatch` ran dead for eight weeks because it exited 0 and nobody noticed (`dispatch#33`); a
long-running watcher process that dies quietly has the same property one level up, and
manufactures false confidence while doing it. With a human present, **the human is the
liveness check** — that removes the objection entirely and needs no new machinery.
This skill is the ritual around `cad stuck` (`mathias/cad#14`), not a replacement for it.
Without that query it would be a checklist with no input; the query without this skill is
data nobody reads carefully. Neither closes the loop alone.
## When to Use
- Picking up CAD's stuck queue with a human present and time set aside to work it
- After `dispatch` (or any executor) reports a failure and it is not immediately obvious
whether it is an infrastructure fault or a real task outcome
- Reviewing the "awaiting human" queue (issues CI has verified but nobody has closed)
**Do not use for:**
- Unattended/scheduled runs. This skill assumes a human is present for its entire duration —
if nobody is watching, its central safety property (human-as-liveness-check) does not hold.
- Dispatching, arming a redispatch label, or closing an issue. This skill only ever diagnoses;
see Iron Laws.
## Iron Laws
1. **Read-only, always.** No dispatching, no arming labels (including `status:redispatch`), no
closing or reopening an issue. A reviewer that can act on its own findings has no
independent check — control stays where it already is.
2. **Every finding cites a locator.** A run id, commit sha, comment permalink, or a specific
log line. A finding without one is an unfalsifiable opinion about a machine — do not emit
it, and do not soften this into "probably" language instead of dropping it.
3. **Re-derive, never recall.** State lives in Gitea and Actions, not in this session. Start
cold every time: run `cad stuck` fresh, do not carry forward a prior session's worklist.
Sessions get compacted and lose exactly the cross-run history that makes a stale worklist
dangerous to trust.
4. **Distinguish "blocked" from "awaiting human".** `cad stuck` already makes this split
(`stuck` vs `awaiting_human`); preserve it in every summary. The `awaiting_human` class is
the human gate working, not a bottleneck — do not report it as one.
5. **Anything that is the operator's call surfaces as `DECISION NEEDED`, never gets decided
for them.** See Output Format.
## Process
### Phase 1 — Query (re-derive the worklist)
Run `cad stuck` against the estate (the `dispatch-repos.txt` scope, or whatever repo set the
operator names). This is the only input. Do not substitute a memory of a prior run, a brain
note, or this issue's own worked examples — all three can be stale within the same day (a
taxonomy row's cited live example can stop reproducing between when it was written and when
you run the query, because the underlying state genuinely changed; that is the design working,
not a bug to paper over).
Group the output by class exactly as `cad stuck` returns it: `stuck` first, `awaiting_human`
second. Pick ONE row to work per pass — this skill is deliberately narrow, not a full-estate
sweep.
### Phase 2 — Read the issue's own comments FIRST
Before anything else, before touching Actions or pod logs: read every comment on the issue,
oldest first. This step exists because it was skipped once and it cost a session — a confident
wrong root cause was produced from pod logs while the correct diagnosis sat in the issue's own
comment thread from nine days earlier (`cad-atlas#10`). The rule "read issue comments before
working" was already written down elsewhere and was not followed; it is written down again
here because that is exactly the kind of already-known thing a rushed session skips.
What to look for: the bot's own report comments (routing decision, ground-truth verdict,
attempt count), and any human comment that already contains the diagnosis.
### Phase 3 — THEN the Actions job logs
Only after Phase 2 turns up nothing conclusive, read the CI job logs:
```
GET /repos/{owner}/{repo}/actions/runs # find the run, match on .run_number
GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs # returns 200, full text, secrets masked
```
This endpoint works. A prior note claiming it 404s was wrong and cost a diagnosis the day this
skill was written — do not rediscover that. Get the run id from `/actions/runs`, not from the
`run_number` shown in a comment (they are different fields; match on `.run_number` to find the
internal `id` the jobs endpoint needs).
This is where the two most valuable findings of the day this skill is drawn from lived: an
agent issuing the identical refused command five times and stopping (never trying the tools it
was actually permitted to use), and a different model, same denial, writing a Go program to
route around it. Neither is visible from the issue comments alone — no metric produces either
of them. They need a reader of the raw transcript.
### Phase 4 — Separate infrastructure fault from task outcome
A run that died before turn 1 says nothing about the issue itself. Before concluding anything
about the WORK, check whether the run reached the model at all:
- Zero turns, a 2-3 record transcript, a "could not start" comment, or a claim label with no
attempt comment behind it → infrastructure fault. This is not evidence the issue is hard, ill
specified, or that the assigned model is unsuitable — it is evidence something upstream
(credit, a key, MCP connectivity, the executor binary) is broken.
- A run that reached the model, drove tools, and stopped on a ceiling, a denial loop, or a
genuine task failure → a real task outcome, worth diagnosing on its own terms.
Four claim labels were burned this exact way in one estate sweep and the tracker read them as
four handled issues (`dispatch#33`). Get this distinction wrong and everything downstream is
about the wrong thing.
### Phase 5 — Decide, or say `DECISION NEEDED`
For each row worked, produce one of:
- **A closed diagnosis** — cause identified, cited to a locator, no operator judgment call
required (e.g. "infrastructure fault, credit exhausted, see comment #N — retry is automatic
on the next cycle, no action needed").
- **A `DECISION NEEDED`** — anything that is genuinely the operator's call: whether to close a
verified-but-open issue, whether to raise a ceiling, whether to abandon an issue that keeps
hitting the same denial. Never make this call on the operator's behalf.
This skill's OWN last act, every time, is to stop — not to dispatch, not to redispatch, not to
close. Ending a pass is itself the enforcement of Iron Law 1.
## Output Format
For a closed diagnosis:
```
DIAGNOSIS: <repo>#<issue> — <category from cad stuck, e.g. ceiling_hit>
CAUSE: <one sentence, cites the mechanism>
LOCATOR: <url or run id / log line>
ACTION: <what happens automatically, or "none — informational">
```
For anything requiring a human call:
```
DECISION NEEDED: <repo>#<issue>
QUESTION: <the exact thing only the operator can decide>
OPTIONS:
1. <option> — <consequence>
2. <option> — <consequence>
RECOMMENDATION: <your pick, with reasoning — but the operator decides>
BLOCKS: <what stays stuck until this is answered, if anything>
```
End every pass with a one-line summary: `N diagnosed, M decisions needed, stopping.`
## Worked Example
`cad stuck` returns `mathias/cad-atlas#10 [near_attempt_cap] 4 dispatch attempts recorded, at
or above the near-cap threshold (4 of MaxAttempts=5).`
Phase 2 (issue comments): the issue's own thread already explains the pattern — repeated
`status:redispatch` re-arming without the label being cleared after a completed attempt.
Phase 3: not needed — Phase 2 was conclusive.
Phase 4: each of the 4 attempts reached the model and ran (not an infrastructure fault) — a
real re-fire pattern, not a claim-label artifact.
Phase 5 output:
```
DECISION NEEDED: mathias/cad-atlas#10
QUESTION: one more automatic attempt remains before MaxAttempts=5 gives up permanently. Let
it fire, or intervene now (fix the underlying cause, or manually close/redirect the issue)?
OPTIONS:
1. Let it fire — costs one more headless run; if it fails the same way, dispatch gives up
automatically and posts a terminal comment. No action needed until then.
2. Intervene now — diagnose why redispatch keeps re-arming (likely: the watcher isn't
clearing RedispatchLabel after a serviced run) and fix that before the cap is spent on
a symptom rather than the cause.
RECOMMENDATION: option 2. The pattern (redispatch not self-clearing after a completed run) is
already documented as a known operational hazard (dispatch#54) — spending the last attempt
on it again teaches nothing new.
BLOCKS: nothing else; this issue's own progress.
```
`1 diagnosed, 1 decision needed, stopping.`
## Anti-Patterns
| Anti-Pattern | Why It Fails |
|---|---|
| Diagnosing from pod/Actions logs before reading issue comments | The exact mistake that cost a session — the answer was already written down nine days earlier. |
| Reporting `awaiting_human` rows as a backlog or bottleneck | It is the human gate working. Misreporting it manufactures urgency that isn't there and trains the operator to distrust the queue. |
| Treating a zero-turn, could-not-start run as evidence about the issue | It is an infrastructure fault. Four claim labels were burned exactly this way and read as four handled issues. |
| Deciding a `DECISION NEEDED` case on the operator's behalf "to save time" | Removes the one property that makes this skill safe to run unattended-adjacent: the human decides, this skill only prepares the decision. |
| Applying `status:redispatch`, closing an issue, or adding any label from this skill | Control stays where it is. This skill has no write path and should never be given one. |
| Carrying forward a worklist from an earlier session instead of re-running `cad stuck` | State changes between sessions (see the worked note on `brain-gardener#190` in `cad#14`'s own acceptance run — its trigger label was gone hours later). A stale worklist gives false confidence, which is the specific failure mode this whole skill exists to avoid. |
## Honest Limitation
This skill encodes the *repeatable* diagnostic sequence: read comments, then logs, separate
fault from outcome, verify by exit code, re-derive rather than recall. It reliably catches the
taxonomy `cad stuck` already knows about. It will not, by itself, produce a genuinely novel
finding the way an unhurried human-plus-agent reading session did the day this skill was
written — that came from noticing things (an agent editing the file that authorizes it, an
issue body recommending a command the policy forbids) that no checklist anticipates. Expect
this skill to be reliable on known failure modes and to miss the next new one. That is an
acceptable trade as long as nobody mistakes a clean pass through this skill for "nothing is
wrong" — it means "nothing on the known list is wrong."
## Brain MCP Integration
**At session start:**
- Run `brain_query` for the domain (`cad`, `dispatch`, the specific repo) to check whether a
finding this session is about to produce has already been diagnosed and written up. Do not
re-diagnose a known pattern from scratch every session.
**After a genuinely novel finding (not already in the taxonomy or the brain):**
- `brain_write` the pattern, generalized beyond the specific issue — the failure mode, not the
issue number. Do not write routine, already-known-taxonomy findings; that is what `cad
stuck`'s own output already records, durably, in Gitea.
**Never:**
- Write a `DECISION NEEDED` outcome to the brain before the operator has actually decided —
that would record a guess as a resolution.
### Logging
Call `session_log` once at the end of every phase to record the outcome.
**At end of each phase:**
- `session_log` with `{skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
"fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root:
"<absolute path>"}`
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
decide
**Status semantics:**
- `pass` — the phase's intended outcome was reached (worklist re-derived; comments read;
fault/outcome separated; a diagnosis or `DECISION NEEDED` was produced).
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
## Mode 2 Routing Note
Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should
stay Mode 1. Phase 1 (running `cad stuck` and grouping its output) is mechanical and a Mode 2
routing candidate once Plan 6 ships the routing pod.
## Cross-References
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
replacement.
- Load `debug` for the hypothesis-generation discipline if Phase 4's fault/outcome split is
itself ambiguous.
- Load `secret-hygiene` before reading raw Actions job logs if the repo is client-tagged —
logs are secret-masked by Gitea but a pasted excerpt into a summary is not.
- `mathias/cad-atlas#11` renders the same `cad stuck` output as a pipeline overlay — read that
instead of this skill's text output when a visual "where is everything stuck" view is wanted
over working one row in depth.
+40 -29
View File
@@ -1,6 +1,6 @@
--- ---
name: gitea-ci name: gitea-ci
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, buildah image build, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere. description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
--- ---
# Gitea CI/CD Pipeline Setup # Gitea CI/CD Pipeline Setup
@@ -31,7 +31,7 @@ Before writing any YAML, collect the following. Use AskUserQuestion for anything
| Container registry | Ask or check existing config | `localhost:5000` on koala, or external | | Container registry | Ask or check existing config | `localhost:5000` on koala, or external |
| k3s namespace | k8s/ manifests or ask | Deploy target | | k3s namespace | k8s/ manifests or ask | Deploy target |
| GitHub mirror repo | Ask | Target for the mirror job | | GitHub mirror repo | Ask | Target for the mirror job |
| Image name | Ask or derive from repo name | Used in buildah + k3s steps | | Image name | Ask or derive from repo name | Used in buildctl + k3s steps |
Check the project's `CLAUDE.md` for infra notes first — often this answers most questions without needing to ask. Check the project's `CLAUDE.md` for infra notes first — often this answers most questions without needing to ask.
@@ -133,35 +133,46 @@ jobs:
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT" echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi fi
# infra#309: do NOT paste a build implementation here. The estate has
# exactly one, mathias/infra's ci-templates/build-image.sh, and each
# repo carries a byte-identical copy at .gitea/build-image.sh that a
# drift gate keeps current.
#
# Copying the implementation is what made the buildah->buildctl
# migration (infra#241) an eleven-repo sprint, and what left this very
# skill handing out the broken version for weeks after the app repos
# were fixed. Per-repo differences are INPUTS, not edits to the copy.
#
# To adopt in a new repo:
# 1. add the repo to mathias/infra ci-templates/adopters.txt
# 2. run `task ci:sync` in mathias/infra -- it writes the copy
# 3. commit .gitea/build-image.sh in your repo
# 4. call it as below
#
# Skipping step 1 leaves the copy unmanaged: it works today and falls
# silently behind the next time the canonical file changes.
#
# Inputs: IMAGE (required, usually the workflow-level env), SHA_TAG,
# VERSION_TAG, GIT_SHA, IMAGE_SOURCE, BUILD_ARGS, EXTRA_LABELS,
# CONTEXT, DOCKERFILE_DIR, DOCKERFILE_NAME, PUSH_LATEST. Read the
# canonical file's header for the full list.
#
# `bash <script>`, not a direct exec: a file written through the Gitea
# API lands 0644, and CI that depends on a file mode is a trap.
- name: Build and push to local registry - name: Build and push to local registry
run: | env:
REGISTRY="localhost:5000" SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" VERSION_TAG: ${{ steps.meta.outputs.version-tag }}
buildah build \ GIT_SHA: ${{ github.sha }}
--label "org.opencontainers.image.revision=${{ github.sha }}" \ IMAGE_SOURCE: ${{ github.repositoryUrl }}
--label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \ run: bash .gitea/build-image.sh
-t ${REF} \
-t ${REGISTRY}/${{ env.IMAGE }}:latest \
.
buildah push --tls-verify=false ${REF}
buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest
[[ -n "${{ steps.meta.outputs.version-tag }}" ]] && \
buildah push --tls-verify=false \
${REF} \
${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} || true
echo "✓ Image pushed to ${REF}"
- name: Smoke test # No smoke test step. The one this skill used to recommend ran
run: | # `sudo k3s ctr run` against the host's containerd -- that worked when
REGISTRY="localhost:5000" # the runner was the Arch host, and is impossible now that it is a pod:
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" # no sudo, no ctr, no containerd socket (`sudo: command not found`).
CNAME="smoke-${{ steps.meta.outputs.sha-tag }}" # The deploy job's `Verify rollout` + `Confirm pod running new image`
sudo k3s ctr images pull --plain-http ${REF} # below assert the same property against the real workload.
OUTPUT=$(timeout 5 sudo k3s ctr run --rm ${REF} ${CNAME} /<binary-name> 2>&1 || true)
sudo k3s ctr containers delete ${CNAME} 2>/dev/null || true
echo "$OUTPUT" | grep -q "<project-name>" \
&& echo "✓ Smoke test passed" \
|| echo "⚠ Smoke test inconclusive: $OUTPUT"
# ── 3. Deploy to k3s ──────────────────────────────────────────────────────── # ── 3. Deploy to k3s ────────────────────────────────────────────────────────
deploy: deploy:
@@ -1,371 +0,0 @@
---
name: test-driven-development
description: Use when implementing any feature or bugfix, before writing implementation code
---
# Test-Driven Development (TDD)
## Overview
Write the test first. Watch it fail. Write minimal code to pass.
**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
**Violating the letter of the rules is violating the spirit of the rules.**
## When to Use
**Always:**
- New features
- Bug fixes
- Refactoring
- Behavior changes
**Exceptions (ask your human partner):**
- Throwaway prototypes
- Generated code
- Configuration files
Thinking "skip TDD just this once"? Stop. That's rationalization.
## The Iron Law
```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```
Write code before the test? Delete it. Start over.
**No exceptions:**
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Implement fresh from tests. Period.
## Red-Green-Refactor
```dot
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify passes\nAll green", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
```
### RED - Write Failing Test
Write one minimal test showing what should happen.
<Good>
```typescript
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
```
Clear name, tests real behavior, one thing
</Good>
<Bad>
```typescript
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
```
Vague name, tests mock not code
</Bad>
**Requirements:**
- One behavior
- Clear name
- Real code (no mocks unless unavoidable)
### Verify RED - Watch It Fail
**MANDATORY. Never skip.**
```bash
npm test path/to/test.test.ts
```
Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
**Test passes?** You're testing existing behavior. Fix test.
**Test errors?** Fix error, re-run until it fails correctly.
### GREEN - Minimal Code
Write simplest code to pass the test.
<Good>
```typescript
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
if (i === 2) throw e;
}
}
throw new Error('unreachable');
}
```
Just enough to pass
</Good>
<Bad>
```typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}
```
Over-engineered
</Bad>
Don't add features, refactor other code, or "improve" beyond the test.
### Verify GREEN - Watch It Pass
**MANDATORY.**
```bash
npm test path/to/test.test.ts
```
Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)
**Test fails?** Fix code, not test.
**Other tests fail?** Fix now.
### REFACTOR - Clean Up
After green only:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green. Don't add behavior.
### Repeat
Next failing test for next feature.
## Good Tests
| Quality | Good | Bad |
|---------|------|-----|
| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
| **Clear** | Name describes behavior | `test('test1')` |
| **Shows intent** | Demonstrates desired API | Obscures what code should do |
## Why Order Matters
**"I'll write tests after to verify it works"**
Tests written after code pass immediately. Passing immediately proves nothing:
- Might test wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug
Test-first forces you to see the test fail, proving it actually tests something.
**"I already manually tested all the edge cases"**
Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive
Automated tests are systematic. They run the same way every time.
**"Deleting X hours of work is wasteful"**
Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (X more hours, high confidence)
- Keep it and add tests after (30 min, low confidence, likely bugs)
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
**"TDD is dogmatic, being pragmatic means adapting"**
TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
**"Tests after achieve the same goals - it's spirit not ritual"**
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
## Common Rationalizations
| Excuse | Reality |
|--------|---------|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
## Red Flags - STOP and Start Over
- Code before test
- Test after implementation
- Test passes immediately
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "It's about spirit not ritual"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."
**All of these mean: Delete code. Start over with TDD.**
## Example: Bug Fix
**Bug:** Empty email accepted
**RED**
```typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
```
**Verify RED**
```bash
$ npm test
FAIL: expected 'Email required', got undefined
```
**GREEN**
```typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
```
**Verify GREEN**
```bash
$ npm test
PASS
```
**REFACTOR**
Extract validation for multiple fields if needed.
## Verification Checklist
Before marking work complete:
- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Each test failed for expected reason (feature missing, not typo)
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Output pristine (no errors, warnings)
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered
Can't check all boxes? You skipped TDD. Start over.
## When Stuck
| Problem | Solution |
|---------|----------|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
## Debugging Integration
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
## Testing Anti-Patterns
When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Mocking without understanding dependencies
## Final Rule
```
Production code → test exists and failed first
Otherwise → not TDD
```
No exceptions without your human partner's permission.
@@ -1,299 +0,0 @@
# Testing Anti-Patterns
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
## Overview
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
**Core principle:** Test what the code does, not what the mocks do.
**Following strict TDD prevents these anti-patterns.**
## The Iron Laws
```
1. NEVER test mock behavior
2. NEVER add test-only methods to production classes
3. NEVER mock without understanding dependencies
```
## Anti-Pattern 1: Testing Mock Behavior
**The violation:**
```typescript
// ❌ BAD: Testing that the mock exists
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
```
**Why this is wrong:**
- You're verifying the mock works, not that the component works
- Test passes when mock is present, fails when it's not
- Tells you nothing about real behavior
**your human partner's correction:** "Are we testing the behavior of a mock?"
**The fix:**
```typescript
// ✅ GOOD: Test real component or don't mock it
test('renders sidebar', () => {
render(<Page />); // Don't mock sidebar
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// OR if sidebar must be mocked for isolation:
// Don't assert on the mock - test Page's behavior with sidebar present
```
### Gate Function
```
BEFORE asserting on any mock element:
Ask: "Am I testing real component behavior or just mock existence?"
IF testing mock existence:
STOP - Delete the assertion or unmock the component
Test real behavior instead
```
## Anti-Pattern 2: Test-Only Methods in Production
**The violation:**
```typescript
// ❌ BAD: destroy() only used in tests
class Session {
async destroy() { // Looks like production API!
await this._workspaceManager?.destroyWorkspace(this.id);
// ... cleanup
}
}
// In tests
afterEach(() => session.destroy());
```
**Why this is wrong:**
- Production class polluted with test-only code
- Dangerous if accidentally called in production
- Violates YAGNI and separation of concerns
- Confuses object lifecycle with entity lifecycle
**The fix:**
```typescript
// ✅ GOOD: Test utilities handle test cleanup
// Session has no destroy() - it's stateless in production
// In test-utils/
export async function cleanupSession(session: Session) {
const workspace = session.getWorkspaceInfo();
if (workspace) {
await workspaceManager.destroyWorkspace(workspace.id);
}
}
// In tests
afterEach(() => cleanupSession(session));
```
### Gate Function
```
BEFORE adding any method to production class:
Ask: "Is this only used by tests?"
IF yes:
STOP - Don't add it
Put it in test utilities instead
Ask: "Does this class own this resource's lifecycle?"
IF no:
STOP - Wrong class for this method
```
## Anti-Pattern 3: Mocking Without Understanding
**The violation:**
```typescript
// ❌ BAD: Mock breaks test logic
test('detects duplicate server', () => {
// Mock prevents config write that test depends on!
vi.mock('ToolCatalog', () => ({
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
}));
await addServer(config);
await addServer(config); // Should throw - but won't!
});
```
**Why this is wrong:**
- Mocked method had side effect test depended on (writing config)
- Over-mocking to "be safe" breaks actual behavior
- Test passes for wrong reason or fails mysteriously
**The fix:**
```typescript
// ✅ GOOD: Mock at correct level
test('detects duplicate server', () => {
// Mock the slow part, preserve behavior test needs
vi.mock('MCPServerManager'); // Just mock slow server startup
await addServer(config); // Config written
await addServer(config); // Duplicate detected ✓
});
```
### Gate Function
```
BEFORE mocking any method:
STOP - Don't mock yet
1. Ask: "What side effects does the real method have?"
2. Ask: "Does this test depend on any of those side effects?"
3. Ask: "Do I fully understand what this test needs?"
IF depends on side effects:
Mock at lower level (the actual slow/external operation)
OR use test doubles that preserve necessary behavior
NOT the high-level method the test depends on
IF unsure what test depends on:
Run test with real implementation FIRST
Observe what actually needs to happen
THEN add minimal mocking at the right level
Red flags:
- "I'll mock this to be safe"
- "This might be slow, better mock it"
- Mocking without understanding the dependency chain
```
## Anti-Pattern 4: Incomplete Mocks
**The violation:**
```typescript
// ❌ BAD: Partial mock - only fields you think you need
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' }
// Missing: metadata that downstream code uses
};
// Later: breaks when code accesses response.metadata.requestId
```
**Why this is wrong:**
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
- **Downstream code may depend on fields you didn't include** - Silent failures
- **Tests pass but integration fails** - Mock incomplete, real API complete
- **False confidence** - Test proves nothing about real behavior
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
**The fix:**
```typescript
// ✅ GOOD: Mirror real API completeness
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' },
metadata: { requestId: 'req-789', timestamp: 1234567890 }
// All fields real API returns
};
```
### Gate Function
```
BEFORE creating mock responses:
Check: "What fields does the real API response contain?"
Actions:
1. Examine actual API response from docs/examples
2. Include ALL fields system might consume downstream
3. Verify mock matches real response schema completely
Critical:
If you're creating a mock, you must understand the ENTIRE structure
Partial mocks fail silently when code depends on omitted fields
If uncertain: Include all documented fields
```
## Anti-Pattern 5: Integration Tests as Afterthought
**The violation:**
```
✅ Implementation complete
❌ No tests written
"Ready for testing"
```
**Why this is wrong:**
- Testing is part of implementation, not optional follow-up
- TDD would have caught this
- Can't claim complete without tests
**The fix:**
```
TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete
```
## When Mocks Become Too Complex
**Warning signs:**
- Mock setup longer than test logic
- Mocking everything to make test pass
- Mocks missing methods real components have
- Test breaks when mock changes
**your human partner's question:** "Do we need to be using a mock here?"
**Consider:** Integration tests with real components often simpler than complex mocks
## TDD Prevents These Anti-Patterns
**Why TDD helps:**
1. **Write test first** → Forces you to think about what you're actually testing
2. **Watch it fail** → Confirms test tests real behavior, not mocks
3. **Minimal implementation** → No test-only methods creep in
4. **Real dependencies** → You see what the test actually needs before mocking
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
## Quick Reference
| Anti-Pattern | Fix |
|--------------|-----|
| Assert on mock elements | Test real component or unmock it |
| Test-only methods in production | Move to test utilities |
| Mock without understanding | Understand dependencies first, mock minimally |
| Incomplete mocks | Mirror real API completely |
| Tests as afterthought | TDD - tests first |
| Over-complex mocks | Consider integration tests |
## Red Flags
- Assertion checks for `*-mock` test IDs
- Methods only called in test files
- Mock setup is >50% of test
- Test fails when you remove mock
- Can't explain why mock is needed
- Mocking "just to be safe"
## The Bottom Line
**Mocks are tools to isolate, not things to test.**
If TDD reveals you're testing mock behavior, you've gone wrong.
Fix: Test real behavior or question why you're mocking at all.
+14 -4
View File
@@ -22,18 +22,28 @@ Screenshot a locally-running web UI without installing a browser on the agent's
## Usage ## Usage
`shot.sh` ships beside this SKILL.md, so resolve it from wherever the harness
installed the skill rather than from a checkout path — an absolute `~/dev/...`
went stale the first time the repo moved and cost a caller three hand-rolled
Job attempts before they found the working script (#26).
```bash ```bash
# Claude Code installs the plugin; Crush/Pi/OpenCode get ~/.agents/skills.
SHOT=$(ls ~/.claude/plugins/marketplaces/*/skills/web-shot/shot.sh \
~/.agents/skills/web-shot/shot.sh 2>/dev/null | head -1 || true)
[ -n "$SHOT" ] || { echo "web-shot not installed on this host: run install.sh or the plugin install" >&2; exit 1; }
# auto-detect Tailscale hostname, screenshot port 8099 root # auto-detect Tailscale hostname, screenshot port 8099 root
~/dev/.skills/web-shot/shot.sh --port 8099 "$SHOT" --port 8099
# explicit URL # explicit URL
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173 "$SHOT" --url http://flamingo:5173
# capture a search state too (types into input[name=q]) # capture a search state too (types into input[name=q])
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad "$SHOT" --port 8181 --search agentsquad
# different output dir # different output dir
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots "$SHOT" --port 3000 --out /tmp/shots
``` ```
## Flags ## Flags
+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
# Tests for bin/check-index.
#
# The regression these exist for: SKILLS_INDEX.md is maintained by hand, and
# two skills (`dream`, `experiment-spec`) shipped without a row — the index
# rows lived on feature branches that were squash-merged after a directory
# reorg, so the SKILL.md landed and the row did not (#15, #22). Nothing
# errored; the index just quietly under-reported the library.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/check-index"
pass=0
fail=0
WORK=${TMPDIR:-/tmp}/check_index_test.$$
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM
# new_fixture <name> — a checkout with skills/alpha and skills/beta.
new_fixture() {
_f="$WORK/$1"
mkdir -p "$_f/skills/alpha" "$_f/skills/beta"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
printf -- '---\nname: beta\ndescription: b\n---\n' > "$_f/skills/beta/SKILL.md"
printf '%s' "$_f"
}
index() {
_dir=$1; shift
printf '# Engineering Skills Index\n\n| Skill | Description | Use when |\n|---|---|---|\n' > "$_dir/SKILLS_INDEX.md"
for _n in "$@"; do
printf '| `%s` | desc | when |\n' "$_n" >> "$_dir/SKILLS_INDEX.md"
done
}
# expect <0|1> <label> <fixture> [grep-for]
expect() {
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
if [ "$_rc" -ne "$_want" ]; then
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
fi
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
fail=$((fail + 1)); printf ' FAIL %s (rc ok, but report never names %s)\n%s\n' "$_label" "$_needle" "$_out"; return
fi
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
}
fx=$(new_fixture complete); index "$fx" alpha beta
expect 0 'every skill has a row' "$fx"
fx=$(new_fixture missing); index "$fx" alpha
expect 1 'a skill with no row is reported' "$fx" beta
fx=$(new_fixture stale); index "$fx" alpha beta gamma
expect 1 'a row for a skill that does not exist is reported' "$fx" gamma
fx=$(new_fixture noindex)
expect 1 'a missing index is reported' "$fx" SKILLS_INDEX.md
printf '\ncheck_index: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+57
View File
@@ -0,0 +1,57 @@
#!/bin/sh
# Tests for the CI gate workflow.
#
# The regression these exist for: this repo had a release workflow that
# auto-tagged every push to main, and a `task check` / `task test` suite that
# nothing ever ran. CI reported green having tested nothing — a gate that
# fails *open*. These assertions pin the wiring so it cannot silently regress.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/.gitea/workflows/ci.yml"
pass=0
fail=0
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
ok 'ci.yml exists'
# A pull_request trigger is what gives tbd_ship a CI run to read on the head
# commit of a branch; without one it fails closed and never merges (#22).
grep -qE '^ pull_request:' "$SUT" \
&& ok 'runs on pull_request' \
|| no 'runs on pull_request'
grep -qE '^ +push:' "$SUT" \
&& ok 'runs on push' \
|| no 'runs on push'
grep -qE '^ +- *(run: *)?task check$|task check' "$SUT" \
&& ok 'gate runs task check' \
|| no 'gate runs task check'
grep -qE 'task test' "$SUT" \
&& ok 'gate runs task test' \
|| no 'gate runs task test'
# The point of the gate: tagging must not happen on a red check.
tag_job=$(awk '/^ tag:/{f=1} f&&/^ [a-z][a-z_-]*:/&&!/^ tag:/{f=0} f' "$SUT")
printf '%s' "$tag_job" | grep -qE '^ +needs: *gate' \
&& ok 'tag job needs the gate' \
|| no 'tag job needs the gate'
# The old workflow file must be gone, or two workflows race on the same push
# and the ungated one still tags.
[ ! -f "$ROOT/.gitea/workflows/release.yml" ] \
&& ok 'ungated release.yml removed' \
|| no 'ungated release.yml removed'
printf '\nci_gate: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+13
View File
@@ -112,6 +112,19 @@ mkdir -p "$f/home/.claude/skills"
ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha" ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha"
assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported' assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
# --- 7b. harness dir is a REAL directory, not a link into the checkout -------
# Found on flamingo 2026-08-26: ~/.config/crush/skills held real directories
# from a pre-plugin install. The old message read "resolves to
# <path>, outside the active checkout" — naming the directory as its own
# resolution target, which is true but useless. It must say the dir is not a
# link at all, because the fix (move it aside, re-run install.sh) is different
# from the fix for a link into a foreign checkout.
f=$(new_fixture realdir)
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'
# --- 7. dangling link inside a harness dir (not the canonical link itself) -- # --- 7. dangling link inside a harness dir (not the canonical link itself) --
# Exercises the per-entry scan rather than the canonical-link check: this is # Exercises the per-entry scan rather than the canonical-link check: this is
# the shape the 243 dead links actually had. # the shape the 243 dead links actually had.
+11
View File
@@ -61,6 +61,17 @@ expect deny 'op read "op://vault/item/field"' 'op read'
expect deny 'env | grep -i gitea' 'env piped to grep' expect deny 'env | grep -i gitea' 'env piped to grep'
expect deny 'env' 'bare env' expect deny 'env' 'bare env'
expect deny 'printenv' 'printenv' expect deny 'printenv' 'printenv'
expect deny 'env -i | grep -i token' 'env -i with no command is still a dump'
expect deny 'env FOO=bar' 'env with only assignments and no command is a dump'
# --- allow: `env VAR=x cmd` / `env -u VAR cmd` RUN a command (#25) ---
# These print nothing themselves; a pipe into grep filters the command's own
# output, not the environment. Blocking them cost a documented workaround
# ((unset VAR; cmd)) on the 1Password write path, which is the shape below.
expect allow 'env -u ANTHROPIC_API_KEY claude --debug -p "ok" 2>&1 | grep -i plugin' 'env -u VAR runs a command'
expect allow 'env FOO=bar ./script.sh | grep x' 'env VAR=x runs a command'
expect allow 'env -u OP_SERVICE_ACCOUNT_TOKEN op item create - < item.json' 'env -u on the 1P write path'
expect deny 'env -u FOO ./run.sh; env | grep -i secret' 'a real dump later in the same line still denies'
# --- deny: kubernetes secret material --- # --- deny: kubernetes secret material ---
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml' expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'