Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc6d5628a7 | ||
|
|
1f7f53e6c0 | ||
|
|
bedb6d278d | ||
|
|
e9ad692418 | ||
|
|
86529d2ac5 |
@@ -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.17.1",
|
"version": "0.18.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
@@ -100,9 +100,11 @@ default to point into `~/dev`.
|
|||||||
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
||||||
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
||||||
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
|
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
|
||||||
|
| `ANTIGRAVITY_SKILLS_JSON` | `$HOME/.gemini/config/skills.json` (registers `AGENTS_SKILLS_DIR` as a path entry; needs `jq`) |
|
||||||
| `SKILLS_EXTRA_SOURCES` | unset (colon-separated extra source directories) |
|
| `SKILLS_EXTRA_SOURCES` | unset (colon-separated extra source directories) |
|
||||||
|
|
||||||
Antigravity and Mistral Vibe targets were removed — unused.
|
The Mistral Vibe target was removed — unused. Antigravity's was removed for
|
||||||
|
the same reason and re-added once it came back into use.
|
||||||
|
|
||||||
## Checking a host
|
## Checking a host
|
||||||
|
|
||||||
|
|||||||
+51
-2
@@ -17,8 +17,8 @@
|
|||||||
# /plugin install skills@mathias
|
# /plugin install skills@mathias
|
||||||
#
|
#
|
||||||
# That path carries the hooks and MCP server definitions too, which symlinks
|
# That path carries the hooks and MCP server definitions too, which symlinks
|
||||||
# cannot. This script covers the harnesses with no plugin system: Crush, Pi
|
# cannot. This script covers the harnesses with no plugin system: Crush, Pi,
|
||||||
# and OpenCode.
|
# OpenCode, and Antigravity.
|
||||||
#
|
#
|
||||||
# Wiring model — one managed directory, one link per harness:
|
# Wiring model — one managed directory, one link per harness:
|
||||||
#
|
#
|
||||||
@@ -27,6 +27,13 @@
|
|||||||
# ~/.config/crush/skills -> ~/.agents/skills
|
# ~/.config/crush/skills -> ~/.agents/skills
|
||||||
# ~/.config/opencode/skills -> ~/.agents/skills
|
# ~/.config/opencode/skills -> ~/.agents/skills
|
||||||
#
|
#
|
||||||
|
# Antigravity has no symlink-directory discovery — it reads a path-entries
|
||||||
|
# JSON instead (~/.gemini/config/skills.json:
|
||||||
|
# {"entries":[{"path":"..."}]}). One entry points at ~/.agents/skills, so it
|
||||||
|
# sees exactly the same surfaces-filtered, collision-resolved set every other
|
||||||
|
# harness does. Requires jq; skipped with a warning if jq is unavailable —
|
||||||
|
# every other target in this script stays dependency-free on purpose.
|
||||||
|
#
|
||||||
# ~/.agents/skills was itself a link to this checkout until #27. That encoded
|
# ~/.agents/skills was itself a link to this checkout until #27. That encoded
|
||||||
# an assumption nobody had stated — that this checkout is the ONLY source of
|
# an assumption nobody had stated — that this checkout is the ONLY source of
|
||||||
# skills on the host — and on flamingo the only way to install this library
|
# skills on the host — and on flamingo the only way to install this library
|
||||||
@@ -53,7 +60,12 @@ CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
|
|||||||
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
||||||
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
||||||
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
||||||
|
ANTIGRAVITY_SKILLS_JSON="${ANTIGRAVITY_SKILLS_JSON:-$HOME/.gemini/config/skills.json}"
|
||||||
EXTRA_SOURCES="${SKILLS_EXTRA_SOURCES:-}"
|
EXTRA_SOURCES="${SKILLS_EXTRA_SOURCES:-}"
|
||||||
|
# Overridable so the jq-absent branch is testable on a host that has jq. That
|
||||||
|
# branch is a contract, not a fallback: jq is optional here while every other
|
||||||
|
# target in this script is dependency-free.
|
||||||
|
JQ="${SKILLS_JQ:-jq}"
|
||||||
# Recorded beside the managed directory rather than inside it: a stray file in
|
# Recorded beside the managed directory rather than inside it: a stray file in
|
||||||
# there would look like a skill to every harness reading the dir.
|
# there would look like a skill to every harness reading the dir.
|
||||||
SOURCES_MANIFEST="$(dirname "$AGENTS_DIR")/$(basename "$AGENTS_DIR")-sources"
|
SOURCES_MANIFEST="$(dirname "$AGENTS_DIR")/$(basename "$AGENTS_DIR")-sources"
|
||||||
@@ -213,6 +225,42 @@ link_dir() {
|
|||||||
log "linked $link → $target"
|
log "linked $link → $target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# wire_antigravity — register $AGENTS_DIR in Antigravity's skills.json.
|
||||||
|
# Merges rather than overwrites: skills.json is a documented user-editable
|
||||||
|
# config (json_configs.md), so a hand-added entry (e.g. a personal skills
|
||||||
|
# dir) must survive a re-run untouched.
|
||||||
|
wire_antigravity() {
|
||||||
|
if ! command -v "$JQ" >/dev/null 2>&1; then
|
||||||
|
log "Antigravity: jq not found — skipping (needed to merge $ANTIGRAVITY_SKILLS_JSON safely)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local existing already updated
|
||||||
|
mkdir -p "$(dirname "$ANTIGRAVITY_SKILLS_JSON")"
|
||||||
|
|
||||||
|
if [ -s "$ANTIGRAVITY_SKILLS_JSON" ]; then
|
||||||
|
existing="$(cat "$ANTIGRAVITY_SKILLS_JSON")"
|
||||||
|
else
|
||||||
|
existing='{}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
already="$(printf '%s' "$existing" | "$JQ" --arg p "$AGENTS_DIR" \
|
||||||
|
'[.entries // [] | .[] | select(.path == $p)] | length' 2>/dev/null)" || {
|
||||||
|
log "Antigravity: $ANTIGRAVITY_SKILLS_JSON is not valid JSON — leaving it untouched"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$already" -gt 0 ]; then
|
||||||
|
log "Antigravity: $AGENTS_DIR already registered in $ANTIGRAVITY_SKILLS_JSON"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
updated="$(printf '%s' "$existing" | "$JQ" --arg p "$AGENTS_DIR" \
|
||||||
|
'.entries = ((.entries // []) + [{path: $p}])')"
|
||||||
|
printf '%s\n' "$updated" > "$ANTIGRAVITY_SKILLS_JSON"
|
||||||
|
log "Antigravity: registered $AGENTS_DIR in $ANTIGRAVITY_SKILLS_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
ensure_checkout
|
ensure_checkout
|
||||||
|
|
||||||
@@ -224,6 +272,7 @@ main() {
|
|||||||
|
|
||||||
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
||||||
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
||||||
|
wire_antigravity
|
||||||
|
|
||||||
log "done — $LINKED_COUNT skill(s) linked from $(sources | wc -l | tr -d ' ') source(s), ref=$REF"
|
log "done — $LINKED_COUNT skill(s) linked from $(sources | wc -l | tr -d ' ') source(s), ref=$REF"
|
||||||
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
|
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
|
||||||
|
|||||||
+21
-2
@@ -58,8 +58,8 @@ adr: 0004
|
|||||||
title: <the decision, as a sentence — not "database choice">
|
title: <the decision, as a sentence — not "database choice">
|
||||||
status: proposed | accepted | superseded | amended
|
status: proposed | accepted | superseded | amended
|
||||||
date: 2026-08-29
|
date: 2026-08-29
|
||||||
deciders: <who accepted it, by name — or "pending: <name>" while proposed>
|
deciders: <who accepted it, by name — or "pending: <name>" (QUOTED) while proposed>
|
||||||
supersedes: <adr number, or ->
|
supersedes: <adr number, or null>
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-0004 — <same title>
|
# ADR-0004 — <same title>
|
||||||
@@ -97,6 +97,25 @@ the condition that fires a revisit. An ADR with no validation section is a prefe
|
|||||||
with a template around it.
|
with a template around it.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### The frontmatter must actually parse
|
||||||
|
|
||||||
|
It exists to be machine-read, so a block that does not load is worse than no block at
|
||||||
|
all: it drops that ADR silently out of every "what is still open?" query, while looking
|
||||||
|
correct. Two shapes this template used to suggest are invalid YAML, and both were found
|
||||||
|
live in the estate on 2026-09-05 — `openbanking-api` ADR-0001 and `infra` ADR-0019, each
|
||||||
|
copied verbatim from here:
|
||||||
|
|
||||||
|
| Wrong | Why | Right |
|
||||||
|
|---|---|---|
|
||||||
|
| `supersedes: -` | a bare dash is a YAML *sequence entry*, not a string | `supersedes: null` |
|
||||||
|
| `deciders: pending: Mathias` | the second, unquoted colon reads as a nested mapping | `deciders: "pending: Mathias"` |
|
||||||
|
|
||||||
|
Check before committing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -c "import yaml,sys;yaml.safe_load(open(sys.argv[1]).read().split('---')[1]);print('ok')" docs/adr/0004-*.md
|
||||||
|
```
|
||||||
|
|
||||||
## Preserving a productive tension
|
## Preserving a productive tension
|
||||||
|
|
||||||
Some tradeoffs are not unresolved because nobody thought hard enough. They are
|
Some tradeoffs are not unresolved because nobody thought hard enough. They are
|
||||||
|
|||||||
+91
-1
@@ -17,6 +17,7 @@ SUT="$ROOT/install.sh"
|
|||||||
|
|
||||||
pass=0
|
pass=0
|
||||||
fail=0
|
fail=0
|
||||||
|
skipped=0
|
||||||
WORK=${TMPDIR:-/tmp}/install_test.$$
|
WORK=${TMPDIR:-/tmp}/install_test.$$
|
||||||
|
|
||||||
if [ ! -f "$SUT" ]; then
|
if [ ! -f "$SUT" ]; then
|
||||||
@@ -55,6 +56,7 @@ run_install() {
|
|||||||
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
||||||
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
||||||
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
||||||
|
ANTIGRAVITY_SKILLS_JSON="$_f/home/.gemini/config/skills.json" \
|
||||||
SKILLS_EXTRA_SOURCES="$_extra" \
|
SKILLS_EXTRA_SOURCES="$_extra" \
|
||||||
bash "$SUT" 2>&1)
|
bash "$SUT" 2>&1)
|
||||||
rc=$?
|
rc=$?
|
||||||
@@ -156,6 +158,40 @@ else
|
|||||||
no 'configured sources are recorded for doctor' "$out"
|
no 'configured sources are recorded for doctor' "$out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Antigravity: jq is optional, so BOTH branches must be tested -----------
|
||||||
|
# These three cases inherited whatever jq the host happened to have. koala has
|
||||||
|
# it, the CI runner does not, so they passed locally and failed in CI — the
|
||||||
|
# classic works-on-my-machine gate. install.sh treats jq as optional by design,
|
||||||
|
# which means the skip is a contract and needs a test of its own.
|
||||||
|
|
||||||
|
# run_install_nojq <fixture> — same as run_install, with the jq lookup pointed
|
||||||
|
# at a binary that does not exist. Deterministic regardless of the host.
|
||||||
|
run_install_nojq() {
|
||||||
|
_f=$1
|
||||||
|
_out=$(HOME="$_f/home" \
|
||||||
|
SKILLS_SKIP_FETCH=1 \
|
||||||
|
SKILLS_CHECKOUT_DIR="$_f/ck" \
|
||||||
|
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
||||||
|
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
||||||
|
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
||||||
|
SKILLS_JQ="definitely-not-jq-$$" \
|
||||||
|
bash "$SUT" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
printf '%s' "$_out"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(new_fixture antigravity_nojq)
|
||||||
|
out=$(run_install_nojq "$f")
|
||||||
|
if [ -e "$f/home/.gemini/config/skills.json" ]; then
|
||||||
|
no 'without jq, no skills.json is written' "$out"
|
||||||
|
elif ! printf '%s' "$out" | grep -qi 'jq not found'; then
|
||||||
|
no 'without jq, the skip is reported' "$out"
|
||||||
|
elif [ ! -L "$f/home/.agents/skills/alpha" ]; then
|
||||||
|
no 'without jq, the other harnesses are still wired' "$out"
|
||||||
|
else
|
||||||
|
ok 'without jq, Antigravity is skipped and the rest still wires'
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 8. surfaces: the library link set is not "every skill" -----------------
|
# --- 8. surfaces: the library link set is not "every skill" -----------------
|
||||||
# claude.ai has no shell, no cluster and no checkout, so a chat-only skill
|
# claude.ai has no shell, no cluster and no checkout, so a chat-only skill
|
||||||
# linked into Crush is a skill that can never fire, competing for selection
|
# linked into Crush is a skill that can never fire, competing for selection
|
||||||
@@ -183,5 +219,59 @@ else
|
|||||||
no 'a source with no surfaces convention is linked as-is' "$out"
|
no 'a source with no surfaces convention is linked as-is' "$out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\ninstall: %d passed, %d failed\n' "$pass" "$fail"
|
# --- 9-11. Antigravity's merge behaviour, only where jq exists ---------------
|
||||||
|
# These three assert what jq DOES. install.sh treats jq as optional, so on a
|
||||||
|
# host without it there is nothing to assert — and failing there is what turned
|
||||||
|
# main red: they passed on koala (jq present) and failed on the runner (absent).
|
||||||
|
# A skipped case is honest; a case that fails because the host lacks an optional
|
||||||
|
# dependency is a false red, and false reds are how a suite stops being read.
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
skipped=$((skipped + 3))
|
||||||
|
printf ' SKIP jq absent — Antigravity merge behaviour not covered on this host (3 cases)\n'
|
||||||
|
else
|
||||||
|
# skills.json entries must point at $AGENTS_DIR — that carries the
|
||||||
|
# surfaces-filtering, collision handling and multi-source merge. Pointing at
|
||||||
|
# the checkout directly would give Antigravity a different skill set than
|
||||||
|
# every other harness.
|
||||||
|
f=$(new_fixture antigravity)
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
if [ ! -f "$AG" ]; then
|
||||||
|
no 'Antigravity skills.json is created' "$out"
|
||||||
|
else
|
||||||
|
registered=$(jq -r --arg p "$f/home/.agents/skills" '[.entries[]? | select(.path == $p)] | length' "$AG")
|
||||||
|
if [ "$registered" = "1" ]; then
|
||||||
|
ok 'Antigravity registers the managed agents dir, not the raw checkout'
|
||||||
|
else
|
||||||
|
no 'Antigravity registers the managed agents dir, not the raw checkout' "$(cat "$AG")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
f=$(new_fixture antigravity_idempotent)
|
||||||
|
run_install "$f" >/dev/null
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
count=$(jq '.entries | length' "$AG" 2>/dev/null || echo "?")
|
||||||
|
if [ "$count" = "1" ]; then
|
||||||
|
ok 'Antigravity wiring is idempotent (no duplicate entries)'
|
||||||
|
else
|
||||||
|
no 'Antigravity wiring is idempotent (no duplicate entries)' "entries=$count"$'\n'"$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A user may hand-edit skills.json to point at their own personal skills dir
|
||||||
|
# (json_configs.md documents this). Our merge must not clobber it.
|
||||||
|
f=$(new_fixture antigravity_preserve)
|
||||||
|
mkdir -p "$f/home/.gemini/config"
|
||||||
|
printf '{"entries":[{"path":"/Users/mathias/personal-skills"}]}' > "$f/home/.gemini/config/skills.json"
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
personal=$(jq -r '[.entries[]? | select(.path == "/Users/mathias/personal-skills")] | length' "$AG" 2>/dev/null || echo 0)
|
||||||
|
if [ "$personal" = "1" ]; then
|
||||||
|
ok 'Antigravity wiring preserves pre-existing hand-authored entries'
|
||||||
|
else
|
||||||
|
no 'Antigravity wiring preserves pre-existing hand-authored entries' "$(cat "$AG")"$'\n'"$out"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\ninstall: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skipped"
|
||||||
[ "$fail" -eq 0 ]
|
[ "$fail" -eq 0 ]
|
||||||
|
|||||||
Reference in New Issue
Block a user