Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e514390b | ||
|
|
087685f2e3 | ||
|
|
83b069afba | ||
|
|
a699ad8cbd | ||
|
|
a42ec18f40 | ||
|
|
46ff1acdfe | ||
|
|
3112b4ce92 | ||
|
|
51a8c25b4d | ||
|
|
d40169f2b7 | ||
|
|
d4f0416dc5 | ||
|
|
abd81f939a | ||
|
|
e42d2ba9cb | ||
|
|
68cebc0bbb | ||
|
|
f50dd6be6a | ||
|
|
1906a774b3 | ||
|
|
38f905e705 | ||
|
|
7db7e0779b |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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.",
|
||||
"version": "0.5.0",
|
||||
"version": "0.8.2",
|
||||
"author": {
|
||||
"name": "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
|
||||
# commit footer:
|
||||
# Bump-Type: major → vX.0.0
|
||||
# Bump-Type: minor → v0.Y.0
|
||||
# (default) → v0.0.Z (patch)
|
||||
# Two jobs, in order:
|
||||
#
|
||||
# Skips when the latest commit is itself the auto-tagger (commit message
|
||||
# contains "[skip-release]") to prevent recursion. Tag is pushed back
|
||||
# to the repo using the GITEA_TOKEN provided by act_runner.
|
||||
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
|
||||
# `task test` (hooks, doctor, release-skip). Runs on every push and
|
||||
# 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:
|
||||
branches:
|
||||
- main
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
gate:
|
||||
name: Check / Test
|
||||
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:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -72,16 +101,19 @@ jobs:
|
||||
manifest=.claude-plugin/plugin.json
|
||||
version="${TAG#v}"
|
||||
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
||||
git tag -a "$TAG" -m "release $TAG"
|
||||
|
||||
if [ "$current" != "$version" ]; then
|
||||
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
||||
git add "$manifest"
|
||||
# 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 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"
|
||||
else
|
||||
git push origin "$TAG"
|
||||
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"
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"_comment": "Tailnet-direct NodePorts, not the public d-ma.be hostnames. The public path goes out through Cloudflare and NPM to reach a k3s service on koala, which imposes rate limits and message-size caps on tool results and adds a full internet round trip - for a service on the same machine most sessions run on. Requires koala to allow these ports on the Tailscale interface; see mathias/skills#24.",
|
||||
"_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": {
|
||||
"brain": {
|
||||
"type": "http",
|
||||
"url": "http://koala:30330/mcp",
|
||||
"url": "https://koala.tail30472e.ts.net:8443/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${BRAIN_MCP_TOKEN}"
|
||||
}
|
||||
},
|
||||
"infra": {
|
||||
"type": "http",
|
||||
"url": "http://koala:30342/mcp"
|
||||
"url": "https://koala.tail30472e.ts.net:8444/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -30,6 +32,7 @@ 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 |
|
||||
| `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 |
|
||||
| `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-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 |
|
||||
|
||||
+6
-1
@@ -94,7 +94,12 @@ tasks:
|
||||
count=$((count + 1))
|
||||
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).
|
||||
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
||||
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
||||
|
||||
Executable
+42
@@ -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
|
||||
@@ -109,6 +109,15 @@ do
|
||||
continue
|
||||
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.
|
||||
# A link that resolves to a different checkout is the dangerous case: it
|
||||
# works, so nothing complains, while serving stale content.
|
||||
|
||||
+12
-5
@@ -50,11 +50,18 @@ if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
|
||||
fi
|
||||
|
||||
# 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"
|
||||
fi
|
||||
if printf '%s' "$CMD" | grep -Eq '(^|[;&|])[[:space:]]*env[[:space:]]*($|[;&|])'; then
|
||||
deny "Blocked: bare 'env' prints every variable's value, including secrets. Use an existence-only check: [ -n \"\$VAR\" ] && echo set"
|
||||
#
|
||||
# `env` is a dump only when nothing but options and VAR=value assignments
|
||||
# follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
|
||||
# modified environment and print nothing themselves — piping those into grep
|
||||
# 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
|
||||
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
||||
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
@@ -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 |
|
||||
| k3s namespace | k8s/ manifests or ask | Deploy target |
|
||||
| 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.
|
||||
|
||||
@@ -133,35 +133,46 @@ jobs:
|
||||
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
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
|
||||
run: |
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
buildah build \
|
||||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||
--label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \
|
||||
-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}"
|
||||
env:
|
||||
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||||
VERSION_TAG: ${{ steps.meta.outputs.version-tag }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
IMAGE_SOURCE: ${{ github.repositoryUrl }}
|
||||
run: bash .gitea/build-image.sh
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
CNAME="smoke-${{ steps.meta.outputs.sha-tag }}"
|
||||
sudo k3s ctr images pull --plain-http ${REF}
|
||||
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"
|
||||
# No smoke test step. The one this skill used to recommend ran
|
||||
# `sudo k3s ctr run` against the host's containerd -- that worked when
|
||||
# the runner was the Arch host, and is impossible now that it is a pod:
|
||||
# no sudo, no ctr, no containerd socket (`sudo: command not found`).
|
||||
# The deploy job's `Verify rollout` + `Confirm pod running new image`
|
||||
# below assert the same property against the real workload.
|
||||
|
||||
# ── 3. Deploy to k3s ────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
|
||||
@@ -22,18 +22,28 @@ Screenshot a locally-running web UI without installing a browser on the agent's
|
||||
|
||||
## 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
|
||||
# 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
|
||||
~/dev/.skills/web-shot/shot.sh --port 8099
|
||||
"$SHOT" --port 8099
|
||||
|
||||
# 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])
|
||||
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad
|
||||
"$SHOT" --port 8181 --search agentsquad
|
||||
|
||||
# different output dir
|
||||
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots
|
||||
"$SHOT" --port 3000 --out /tmp/shots
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
@@ -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 ]
|
||||
@@ -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 ]
|
||||
@@ -112,6 +112,19 @@ mkdir -p "$f/home/.claude/skills"
|
||||
ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha"
|
||||
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) --
|
||||
# Exercises the per-entry scan rather than the canonical-link check: this is
|
||||
# the shape the 243 dead links actually had.
|
||||
|
||||
@@ -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' 'bare env'
|
||||
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 ---
|
||||
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'
|
||||
|
||||
Reference in New Issue
Block a user