@jesdi/skills 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/package.json +1 -1
- package/skills/backlog/SKILL.md +12 -6
- package/skills/backlog/references/graphql.md +2 -1
- package/skills/crap-gate/SKILL.md +38 -11
- package/skills/crap-gate/crap.py +13 -1
- package/skills/crap-gate/run.sh +87 -0
- package/skills/crap-gate/test_launcher.py +116 -0
- package/skills/prototype/LICENSE +21 -0
- package/skills/prototype/LOGIC.md +67 -0
- package/skills/prototype/SKILL.md +39 -0
- package/skills/prototype/UI.md +99 -0
- package/skills/to-spec/LICENSE +21 -0
- package/skills/to-spec/SKILL.md +119 -0
- package/skills/to-tickets/LICENSE +21 -0
- package/skills/to-tickets/SKILL.md +95 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +65 -0
- package/skills/wizard/template.sh +229 -0
- package/skills-manifest.json +30 -6
package/README.md
CHANGED
|
@@ -20,6 +20,24 @@ directory.
|
|
|
20
20
|
npx @jesdi/skills-cli sync
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
It looks like this:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 1,
|
|
28
|
+
"agents": ["claude", "opencode"],
|
|
29
|
+
"skills": {
|
|
30
|
+
"crap-gate": { "version": "1.2.0", "package": "1.4.0" },
|
|
31
|
+
"backlog": { "version": "0.3.0", "package": "1.4.0", "agents": ["claude"] }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The top-level `agents` is the default for every skill; a skill's own `agents`
|
|
37
|
+
overrides it. `install <skill>` inherits the default (the first interactive
|
|
38
|
+
install asks once and stores it), while `install --agent ...` writes a
|
|
39
|
+
per-skill override. In scripts, pass `--agent` or commit the top-level default.
|
|
40
|
+
|
|
23
41
|
## Commands
|
|
24
42
|
|
|
25
43
|
```
|
|
@@ -35,11 +53,12 @@ npx @jesdi/skills-cli uninstall <skill> [--global]
|
|
|
35
53
|
|
|
36
54
|
Skills authored by other people are **not** copied into this repo — they keep
|
|
37
55
|
their own authors, upstreams, and licenses. `external-skills.json` records
|
|
38
|
-
which ones are part of the standard setup
|
|
39
|
-
|
|
56
|
+
which ones are part of the standard setup, where they come from, and (under
|
|
57
|
+
`pins`) the upstream commit the box set is taken from. Install them from
|
|
58
|
+
upstream with the [skills.sh](https://skills.sh/) CLI:
|
|
40
59
|
|
|
41
60
|
```bash
|
|
42
|
-
npx skills add mattpocock/skills # grill-me, grill-with-docs, improve-codebase-architecture
|
|
61
|
+
npx skills add mattpocock/skills # grill-me, grill-with-docs, grilling, improve-codebase-architecture, to-questionnaire, tdd, code-review, codebase-design, diagnosing-bugs, resolving-merge-conflicts
|
|
43
62
|
npx skills add JuliusBrussee/caveman # caveman suite
|
|
44
63
|
npx skills add vercel-labs/skills # find-skills
|
|
45
64
|
npx skills add vercel-labs/agent-skills # vercel-react-best-practices
|
|
@@ -49,6 +68,23 @@ npx skills add anthropics/skills # frontend-design (skip if using the C
|
|
|
49
68
|
The skills.sh lockfile (`~/.agents/.skill-lock.json`) tracks installed
|
|
50
69
|
versions; `npx skills update` refreshes them.
|
|
51
70
|
|
|
71
|
+
### Forks (the one exception)
|
|
72
|
+
|
|
73
|
+
`to-spec`, `to-tickets`, `prototype` and `wizard` are modified copies of the
|
|
74
|
+
mattpocock/skills originals, vendored under `skills/<name>/` with the upstream
|
|
75
|
+
MIT `LICENSE` and a header naming the upstream commit. They keep their upstream
|
|
76
|
+
names on purpose, so **never install the upstream copies of those four names
|
|
77
|
+
alongside them** (`npx skills add mattpocock/skills` installs the whole set —
|
|
78
|
+
deselect those four, or uninstall them afterwards). `external-skills.json`
|
|
79
|
+
lists them under `forks`.
|
|
80
|
+
|
|
81
|
+
### The box set
|
|
82
|
+
|
|
83
|
+
`external-skills.json` → `sets.box` names the skills the agent-ops box
|
|
84
|
+
receives: `own` from this package, `external` from the pinned upstream.
|
|
85
|
+
agent-ops vendors that set into its claude-home seed with its own refresh
|
|
86
|
+
command; this repo's job is to publish the set and keep the pins resolvable.
|
|
87
|
+
|
|
52
88
|
## Authoring skills
|
|
53
89
|
|
|
54
90
|
Add `skills/<name>/SKILL.md` (frontmatter `name` must match the directory,
|
package/package.json
CHANGED
package/skills/backlog/SKILL.md
CHANGED
|
@@ -71,16 +71,19 @@ these labels (`setup` provisions them all) — never invent new ones.
|
|
|
71
71
|
| `testing` | Test coverage, test infrastructure, flaky tests |
|
|
72
72
|
| `dependencies` | Upgrading or managing third-party dependencies |
|
|
73
73
|
|
|
74
|
-
**Pipeline —
|
|
74
|
+
**Pipeline — orthogonal to Type and Area:**
|
|
75
75
|
|
|
76
|
-
Whether an issue is ready for an unattended agent to pick up
|
|
76
|
+
Whether an issue is ready for an unattended agent to pick up, and what it
|
|
77
|
+
carries. `auto` and `human-required` are mutually exclusive; leave both off
|
|
77
78
|
when it is genuinely unclear — an unlabelled issue is simply not routed, which
|
|
78
|
-
is the safe default.
|
|
79
|
+
is the safe default. `spec-ready` may sit alongside `auto`. Dispatchers filter
|
|
80
|
+
on `auto` and read `spec-ready`; nothing else reads these.
|
|
79
81
|
|
|
80
82
|
| Label | Apply when |
|
|
81
83
|
|-------|-----------|
|
|
82
84
|
| `auto` | Trivial, or close enough to previously automated work — suitable for the unattended agents pipeline |
|
|
83
85
|
| `human-required` | Substantial fog of war remains; needs heavy human interaction before an agent can take it |
|
|
86
|
+
| `spec-ready` | The issue body already carries a settled design (Problem, Solution, Implementation and Testing Decisions), typically written by `to-spec` after a grilling session — the pipeline skips its own interview and reconciles the design against current code |
|
|
84
87
|
|
|
85
88
|
## `setup` — create the board once (idempotent)
|
|
86
89
|
|
|
@@ -92,7 +95,7 @@ dir.
|
|
|
92
95
|
1. Ensure the `project` scope: `gh auth status`; if Projects calls 403, run
|
|
93
96
|
`gh auth refresh -s project`.
|
|
94
97
|
2. Follow `references/graphql.md` → **Setup**: create the Project, link the repo,
|
|
95
|
-
add the six fields, create the `inbox` label plus the
|
|
98
|
+
add the six fields, create the `inbox` label plus the 15 taxonomy labels
|
|
96
99
|
(idempotent — `--force` reconciles color/description on re-runs). On a
|
|
97
100
|
brand-new board the `Area`
|
|
98
101
|
single-select is created with the generic default options
|
|
@@ -171,7 +174,9 @@ Reuse the **dedup discipline** from the `file-bug-issue` skill, but take the
|
|
|
171
174
|
3. Otherwise infer labels from the idea text per the **Label taxonomy** above,
|
|
172
175
|
then create it (see `references/graphql.md` → Capture): `--label inbox` plus
|
|
173
176
|
one `--label` per inferred label, body = the idea as-is (one loose sentence
|
|
174
|
-
is fine; no required structure).
|
|
177
|
+
is fine; no required structure). Add `spec-ready` only when the body being
|
|
178
|
+
filed is a settled design (the `to-spec` skill does this itself); a one-line
|
|
179
|
+
idea is never spec-ready.
|
|
175
180
|
4. Report the new issue URL and the labels applied. If mid-task, treat as a
|
|
176
181
|
side-quest and return.
|
|
177
182
|
|
|
@@ -182,7 +187,8 @@ Reuse the **dedup discipline** from the `file-bug-issue` skill, but take the
|
|
|
182
187
|
(skip = leave in inbox). Offer label corrections per the **Label taxonomy**
|
|
183
188
|
above; apply via `gh issue edit N --add-label … --remove-label …`
|
|
184
189
|
(graphql.md → Triage). Labels stay independent of the board `Area` gathered
|
|
185
|
-
next.
|
|
190
|
+
next. If the issue carries `spec-ready`, confirm the body really is a
|
|
191
|
+
settled design with its sections intact; remove the label when it is not.
|
|
186
192
|
3. For a promoted issue, gather `Impact` (1–5), `Effort` (1–5), `Area` (**pick one
|
|
187
193
|
from the options present in `.backlog/project-meta.json`** → `fields.Area.options`),
|
|
188
194
|
and optional blockers.
|
|
@@ -47,9 +47,10 @@ gh label create security --repo <repo> --description "Vulnerabilities, auth, per
|
|
|
47
47
|
gh label create performance --repo <repo> --description "Speed, memory, efficiency, scalability" --color f9d0c4 --force
|
|
48
48
|
gh label create testing --repo <repo> --description "Test coverage, test infrastructure, flaky tests" --color 0e8a16 --force
|
|
49
49
|
gh label create dependencies --repo <repo> --description "Upgrading or managing third-party dependencies" --color 0366d6 --force
|
|
50
|
-
# Pipeline labels (
|
|
50
|
+
# Pipeline labels (auto/human-required mutually exclusive; spec-ready may co-exist; read by dispatchers)
|
|
51
51
|
gh label create auto --repo <repo> --description "Suitable for the unattended agents pipeline" --color 5319E7 --force
|
|
52
52
|
gh label create human-required --repo <repo> --description "Needs heavy human interaction before automation" --color B60205 --force
|
|
53
|
+
gh label create spec-ready --repo <repo> --description "Issue body carries a settled design; the pipeline skips its interview" --color 006B75 --force
|
|
53
54
|
|
|
54
55
|
# 5. Discover IDs to build .backlog/project-meta.json
|
|
55
56
|
gh project view <projectNumber> --owner <owner> --format json # -> id (projectId)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crap-gate
|
|
3
3
|
description: >-
|
|
4
4
|
Score every function a branch changed with CRAP (cyclomatic complexity ×
|
|
5
|
-
untested fraction), fail on existing functions
|
|
5
|
+
untested fraction), fail on existing functions above 15 and new functions above 9,
|
|
6
6
|
and drive a fixer + independent reviewer loop until the gate passes. Use
|
|
7
7
|
when implementation is done and before pushing, when the user says "run the
|
|
8
8
|
crap gate", "check CRAP", "is this change tested enough", or when a push
|
|
@@ -21,16 +21,19 @@ is the human approval gate at push time.
|
|
|
21
21
|
|
|
22
22
|
## Running the engine
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Use the bundled launcher on macOS or Linux (x86_64/ARM64; Windows via WSL).
|
|
25
|
+
It downloads checksum-verified uv 0.12.13 and provisions private Python 3.13.7
|
|
26
|
+
on first use, then reuses them across repositories. No global Python, uv,
|
|
27
|
+
virtualenv activation, or pip installation is needed. The engine uses only
|
|
28
|
+
Python's standard library and supports Python source syntax through 3.13.
|
|
29
|
+
Run from anywhere inside the repo — it walks up to `.crap-gate.json`:
|
|
27
30
|
|
|
28
31
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
sh "<skill-dir>/run.sh" # human table, exit 1 on violations
|
|
33
|
+
sh "<skill-dir>/run.sh" --json # same data for a subagent prompt
|
|
34
|
+
sh "<skill-dir>/run.sh" --no-run # reuse the last coverage reports
|
|
35
|
+
sh "<skill-dir>/run.sh" --all # whole-repo baseline (exploration only)
|
|
36
|
+
sh "<skill-dir>/run.sh" --base main --threshold-new 7 # local what-ifs
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
`<skill-dir>` is wherever this skill is installed (`.agents/skills/crap-gate`,
|
|
@@ -40,13 +43,37 @@ wrap it (`make crap`); prefer the wrapper when one exists.
|
|
|
40
43
|
When you point a wrapper at an agent symlink, that symlink only exists after
|
|
41
44
|
`skills-cli sync` if the matching agent is declared for the skill in
|
|
42
45
|
`.my-skills.json` — `claude` → `.claude/skills/crap-gate`, `opencode` →
|
|
43
|
-
`.agents/skills/crap-gate`. A wrapper that runs `.agents/skills/crap-gate/
|
|
46
|
+
`.agents/skills/crap-gate`. A wrapper that runs `.agents/skills/crap-gate/run.sh`
|
|
44
47
|
therefore needs `"agents": ["opencode"]` (or `["claude", "opencode"]`) on the
|
|
45
48
|
`crap-gate` entry, or a fresh checkout/worktree will lack that path after `sync`.
|
|
46
|
-
The store copy `.my-skills/crap-gate/
|
|
49
|
+
The store copy `.my-skills/crap-gate/run.sh` is always materialized regardless
|
|
47
50
|
of declared agents, so wrapping against it avoids the agent/path coupling
|
|
48
51
|
entirely.
|
|
49
52
|
|
|
53
|
+
Existing `make crap` / `make crap-gate` wrappers must replace `python3 .../crap.py`
|
|
54
|
+
with `sh .../run.sh` (quote the path). Prefer the store path, for example:
|
|
55
|
+
|
|
56
|
+
```makefile
|
|
57
|
+
crap crap-gate:
|
|
58
|
+
sh "$(CURDIR)/.my-skills/crap-gate/run.sh"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The launcher needs standard shell utilities, `curl`, `tar`, and `sha256sum`
|
|
62
|
+
or `shasum`. Its cache defaults to `${XDG_CACHE_HOME:-$HOME/.cache}/crap-gate`;
|
|
63
|
+
set `CRAP_GATE_CACHE_DIR` to an absolute path to relocate it. To pre-provision,
|
|
64
|
+
run the launcher with `--help` while online. `CRAP_GATE_OFFLINE=1` then requires
|
|
65
|
+
that populated cache and prevents runtime downloads. Runtime setup messages go
|
|
66
|
+
to stderr; `--json` returns a structured error on setup failure, with exit 2.
|
|
67
|
+
Do not bypass the gate or repair global Python when setup fails; resolve the
|
|
68
|
+
reported download/cache problem and rerun the launcher.
|
|
69
|
+
|
|
70
|
+
TypeScript targets still need `node` and the project's own `typescript` package.
|
|
71
|
+
Coverage commands still need the project's test dependencies; configure them
|
|
72
|
+
with its normal runner (for example, `cd backend && pipenv run pytest ...`).
|
|
73
|
+
The launcher preserves PATH and the caller's environment for these commands;
|
|
74
|
+
its private Python is only for the gate engine. It does not install project
|
|
75
|
+
dependencies or edit project environments.
|
|
76
|
+
|
|
50
77
|
By default the engine runs each target's coverage command only when that
|
|
51
78
|
target has changed files, so a backend-only change never runs the frontend
|
|
52
79
|
suite. Coverage command output goes to `.crap/<target>.log`.
|
package/skills/crap-gate/crap.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""crap-gate engine: CRAP score for every function a branch changed.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
sh run.sh [--config PATH] [--base REF] [--threshold-existing N]
|
|
5
5
|
[--threshold-new N] [--run | --no-run] [--all] [--json]
|
|
6
6
|
|
|
7
7
|
Exit 0 = no violations, 1 = violations, 2 = configuration/tool error.
|
|
@@ -11,12 +11,22 @@ project's own `typescript` package.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import json
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
import tempfile
|
|
17
18
|
import time
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
21
|
+
# Direct invocation remains useful for development, but should fail clearly
|
|
22
|
+
# before importing an analyzer that requires newer AST nodes.
|
|
23
|
+
if sys.version_info < (3, 10):
|
|
24
|
+
message = "Use the bundled run.sh launcher; direct execution requires Python >= 3.10"
|
|
25
|
+
print(f"crap: {message}", file=sys.stderr)
|
|
26
|
+
if "--json" in sys.argv:
|
|
27
|
+
print(json.dumps({"error": {"kind": "tooling", "message": message}}))
|
|
28
|
+
sys.exit(2)
|
|
29
|
+
|
|
20
30
|
import crap_git
|
|
21
31
|
import crap_python
|
|
22
32
|
import crap_typescript
|
|
@@ -201,6 +211,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
201
211
|
result = run(cfg, run_mode=args.run_mode, all_functions=args.all)
|
|
202
212
|
except ToolError as exc:
|
|
203
213
|
print(f"crap: {exc}", file=sys.stderr)
|
|
214
|
+
if args.json:
|
|
215
|
+
print(json.dumps({"error": {"kind": "tooling", "message": str(exc)}}))
|
|
204
216
|
return 2
|
|
205
217
|
sys.stdout.write(render_json(result) if args.json else render_text(result))
|
|
206
218
|
return 1 if result.summary["fail"] else 0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Keep bootstrap dependencies to POSIX tools, curl, tar, and SHA-256.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
json=false
|
|
6
|
+
for arg in "$@"; do
|
|
7
|
+
[ "$arg" != --json ] || json=true
|
|
8
|
+
done
|
|
9
|
+
fail() {
|
|
10
|
+
printf 'crap: %s\n' "$1" >&2
|
|
11
|
+
if "$json"; then
|
|
12
|
+
printf '%s\n' '{"error":{"kind":"tooling","message":"Runtime setup failed; see stderr. Run the launcher again after resolving the setup error."}}'
|
|
13
|
+
fi
|
|
14
|
+
exit 2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Resolve file symlinks as well as symlinked skill directories.
|
|
18
|
+
entry=$0
|
|
19
|
+
while [ -L "$entry" ]; do
|
|
20
|
+
parent=$(CDPATH= cd -- "$(dirname -- "$entry")" && pwd)
|
|
21
|
+
entry=$(readlink "$entry")
|
|
22
|
+
case "$entry" in /*) ;; *) entry=$parent/$entry ;; esac
|
|
23
|
+
done
|
|
24
|
+
skill_dir=$(CDPATH= cd -- "$(dirname -- "$entry")" && pwd)
|
|
25
|
+
cache=${CRAP_GATE_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/crap-gate}
|
|
26
|
+
case "$cache" in /*) ;; *) fail 'CRAP_GATE_CACHE_DIR must be an absolute path' ;; esac
|
|
27
|
+
uv_version=0.12.13
|
|
28
|
+
python_version=3.13.7
|
|
29
|
+
case "$(uname -s):$(uname -m)" in
|
|
30
|
+
Darwin:arm64|Darwin:aarch64)
|
|
31
|
+
target=aarch64-apple-darwin
|
|
32
|
+
digest=7e6ddb9316acc00f2296c82ff4d99977870ee34b2f0ddcae9444d714db9364ed ;;
|
|
33
|
+
Darwin:x86_64)
|
|
34
|
+
target=x86_64-apple-darwin
|
|
35
|
+
digest=5e287ef61cb6a9b61b3a83fef124fd143e400468a7dac794230147a810e17119 ;;
|
|
36
|
+
Linux:aarch64|Linux:arm64)
|
|
37
|
+
target=aarch64-unknown-linux-musl
|
|
38
|
+
digest=f44bc1037a17889fe562fffd2002d4ed108e499fbe68b4f022af244dc7b8244f ;;
|
|
39
|
+
Linux:x86_64)
|
|
40
|
+
target=x86_64-unknown-linux-musl
|
|
41
|
+
digest=4e2bfd0c9007b1032a50e539e965fd0a6037d87ad93ae1580d220a92d4c94098 ;;
|
|
42
|
+
*) fail 'Unsupported platform: use macOS or Linux on x86_64 or ARM64 (Windows: WSL)' ;;
|
|
43
|
+
esac
|
|
44
|
+
uv_dir=$cache/uv/$uv_version/$target
|
|
45
|
+
uv=$uv_dir/uv
|
|
46
|
+
if [ ! -x "$uv" ]; then
|
|
47
|
+
[ "${CRAP_GATE_OFFLINE:-0}" != 1 ] || fail 'uv is not cached; provision this cache online first'
|
|
48
|
+
mkdir -p "$uv_dir" || fail 'Cannot create runtime cache'
|
|
49
|
+
stage=$(mktemp -d "$uv_dir/.download.XXXXXX") || fail 'Cannot create download directory'
|
|
50
|
+
trap 'rm -rf "$stage"' EXIT
|
|
51
|
+
trap 'exit 2' HUP INT TERM
|
|
52
|
+
printf 'crap: downloading uv %s\n' "$uv_version" >&2
|
|
53
|
+
curl --fail --location --silent --show-error --connect-timeout 15 --max-time 300 \
|
|
54
|
+
"https://github.com/astral-sh/uv/releases/download/$uv_version/uv-$target.tar.gz" \
|
|
55
|
+
-o "$stage/uv.tar.gz" || fail 'Cannot download uv; check network access and retry'
|
|
56
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
57
|
+
actual=$(sha256sum "$stage/uv.tar.gz")
|
|
58
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
59
|
+
actual=$(shasum -a 256 "$stage/uv.tar.gz")
|
|
60
|
+
else
|
|
61
|
+
fail 'SHA-256 verification requires sha256sum or shasum'
|
|
62
|
+
fi
|
|
63
|
+
[ "${actual%% *}" = "$digest" ] || fail 'uv archive checksum mismatch'
|
|
64
|
+
tar -xzf "$stage/uv.tar.gz" -C "$stage" || fail 'Cannot extract uv'
|
|
65
|
+
# Atomic publication: concurrent first runs never see a partial executable.
|
|
66
|
+
mv -f "$stage/uv-$target/uv" "$uv" || fail 'Cannot install cached uv'
|
|
67
|
+
rm -rf "$stage"
|
|
68
|
+
trap - EXIT HUP INT TERM
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Limit uv settings to this subprocess. Do not activate a venv or change PATH:
|
|
72
|
+
# coverage commands must inherit the caller's project environment unchanged.
|
|
73
|
+
managed_uv() (
|
|
74
|
+
unset UV_NO_MANAGED_PYTHON UV_PYTHON UV_PYTHON_DOWNLOADS UV_PYTHON_BIN_DIR
|
|
75
|
+
export UV_PYTHON_INSTALL_DIR="$cache/python"
|
|
76
|
+
export UV_CACHE_DIR="$cache/uv-cache"
|
|
77
|
+
export UV_MANAGED_PYTHON=true
|
|
78
|
+
"$uv" --no-config "$@"
|
|
79
|
+
)
|
|
80
|
+
python=$(managed_uv python find --no-project --system --managed-python --no-python-downloads "$python_version" 2>/dev/null) || {
|
|
81
|
+
[ "${CRAP_GATE_OFFLINE:-0}" != 1 ] || fail 'Python is not cached; provision this cache online first'
|
|
82
|
+
managed_uv python install --no-bin "$python_version" >&2 || fail 'Cannot provision Python; check network access and retry'
|
|
83
|
+
python=$(managed_uv python find --no-project --system --managed-python --no-python-downloads "$python_version") || fail 'Cannot locate managed Python'
|
|
84
|
+
}
|
|
85
|
+
# Ignore Python-specific ambient settings for the engine, but preserve them for
|
|
86
|
+
# child coverage processes. -I would also remove the sibling module directory.
|
|
87
|
+
exec "$python" -E -s "$skill_dir/crap.py" "$@"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Launcher regressions. Real-runtime cases use a pre-provisioned cache in CI."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from test_cli import BASE_SRC, repo # noqa: F401 — shared git repository fixture
|
|
11
|
+
|
|
12
|
+
LAUNCHER = Path(__file__).with_name("run.sh").resolve()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def launch(env, *args, cwd=None, script=LAUNCHER):
|
|
16
|
+
return subprocess.run(
|
|
17
|
+
["/bin/sh", str(script), *args], env=env, cwd=cwd,
|
|
18
|
+
text=True, capture_output=True, timeout=60,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_empty_offline_cache_is_json_tool_error(tmp_path):
|
|
23
|
+
env = dict(os.environ, CRAP_GATE_CACHE_DIR=str(tmp_path / "cache"), CRAP_GATE_OFFLINE="1")
|
|
24
|
+
proc = launch(env, "--json")
|
|
25
|
+
assert proc.returncode == 2
|
|
26
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
27
|
+
assert "not cached" in proc.stderr
|
|
28
|
+
assert not (tmp_path / "cache").exists()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_bad_download_is_rejected_before_execution(tmp_path):
|
|
32
|
+
bin_dir = tmp_path / "bin"
|
|
33
|
+
bin_dir.mkdir()
|
|
34
|
+
curl = bin_dir / "curl"
|
|
35
|
+
curl.write_text('#!/bin/sh\nwhile [ "$1" != -o ]; do shift; done\nprintf corrupt > "$2"\n')
|
|
36
|
+
curl.chmod(0o755)
|
|
37
|
+
env = dict(os.environ, PATH=f"{bin_dir}:{os.environ['PATH']}",
|
|
38
|
+
CRAP_GATE_CACHE_DIR=str(tmp_path / "cache"), CRAP_GATE_OFFLINE="0")
|
|
39
|
+
proc = launch(env, "--json")
|
|
40
|
+
assert proc.returncode == 2
|
|
41
|
+
assert "checksum mismatch" in proc.stderr
|
|
42
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
43
|
+
assert not list((tmp_path / "cache").rglob("uv.tar.gz"))
|
|
44
|
+
assert not list((tmp_path / "cache").rglob(".download.*"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def runtime_env():
|
|
49
|
+
cache = os.environ.get("CRAP_GATE_TEST_CACHE")
|
|
50
|
+
if not cache:
|
|
51
|
+
pytest.skip("Set CRAP_GATE_TEST_CACHE to a cache provisioned by run.sh --help")
|
|
52
|
+
return dict(os.environ, CRAP_GATE_CACHE_DIR=cache, CRAP_GATE_OFFLINE="1")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("python_on_path", ["absent", "incompatible"])
|
|
56
|
+
def test_runtime_ignores_host_python_and_preserves_coverage_environment(
|
|
57
|
+
repo, tmp_path, runtime_env, python_on_path,
|
|
58
|
+
):
|
|
59
|
+
bin_dir = tmp_path / "host bin"
|
|
60
|
+
bin_dir.mkdir()
|
|
61
|
+
for name in ("uname", "dirname", "git", "sh", "env"):
|
|
62
|
+
(bin_dir / name).symlink_to(shutil.which(name))
|
|
63
|
+
if python_on_path == "incompatible":
|
|
64
|
+
for name in ("python", "python3"):
|
|
65
|
+
executable = bin_dir / name
|
|
66
|
+
executable.write_text("#!/bin/sh\necho wrong-python >&2\nexit 99\n")
|
|
67
|
+
executable.chmod(0o755)
|
|
68
|
+
runtime_env.update(PATH=str(bin_dir), VIRTUAL_ENV=str(tmp_path / "wrong venv"),
|
|
69
|
+
PYTHONHOME="/nonexistent-python", PYTHONPATH="/nonexistent-modules")
|
|
70
|
+
# Both files must be ignored by runtime discovery.
|
|
71
|
+
(repo / ".python-version").write_text("3.9.6\n")
|
|
72
|
+
(repo / "uv.toml").write_text("deliberately invalid configuration\n")
|
|
73
|
+
(repo / "pkg/m.py").write_text(BASE_SRC + "\ndef added():\n return 1\n")
|
|
74
|
+
(repo / ".crap").mkdir()
|
|
75
|
+
cfg = json.loads((repo / ".crap-gate.json").read_text())
|
|
76
|
+
cfg["targets"][0]["coverage"]["command"] = (
|
|
77
|
+
"env > coverage-env.txt; printf '%s' '{\"files\":{}}' > .crap/coverage.json"
|
|
78
|
+
)
|
|
79
|
+
(repo / ".crap-gate.json").write_text(json.dumps(cfg))
|
|
80
|
+
proc = launch(runtime_env, "--json", cwd=repo)
|
|
81
|
+
assert proc.returncode == 0, proc.stderr
|
|
82
|
+
assert json.loads(proc.stdout)["functions"][0]["name"] == "added"
|
|
83
|
+
inherited = dict(line.split("=", 1) for line in (repo / "coverage-env.txt").read_text().splitlines())
|
|
84
|
+
for name in ("PATH", "VIRTUAL_ENV", "PYTHONHOME", "PYTHONPATH"):
|
|
85
|
+
assert inherited[name] == runtime_env[name]
|
|
86
|
+
assert "UV_PYTHON_INSTALL_DIR" not in inherited or inherited["UV_PYTHON_INSTALL_DIR"] == os.environ.get("UV_PYTHON_INSTALL_DIR")
|
|
87
|
+
assert not (repo / ".venv").exists()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_symlink_spaces_and_exit_codes(repo, tmp_path, runtime_env):
|
|
91
|
+
link = tmp_path / "launcher link"
|
|
92
|
+
link.symlink_to(LAUNCHER)
|
|
93
|
+
(repo / "pkg/m.py").write_text(BASE_SRC + "\ndef unnew(a, b):\n return a if b else (b if a else 0)\n")
|
|
94
|
+
(repo / ".crap").mkdir()
|
|
95
|
+
(repo / ".crap/coverage.json").write_text('{"files":{}}')
|
|
96
|
+
proc = launch(runtime_env, "--json", "--no-run", cwd=repo, script=link)
|
|
97
|
+
assert proc.returncode == 1, proc.stderr
|
|
98
|
+
assert json.loads(proc.stdout)["summary"]["fail"] == 1
|
|
99
|
+
proc = launch(runtime_env, "--json", "--base", "missing-ref", cwd=repo, script=link)
|
|
100
|
+
assert proc.returncode == 2
|
|
101
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_offline_cache_missing_python_is_tool_error(tmp_path, runtime_env):
|
|
105
|
+
original = Path(runtime_env["CRAP_GATE_CACHE_DIR"])
|
|
106
|
+
cache = tmp_path / "uv only cache"
|
|
107
|
+
for executable in (original / "uv").rglob("uv"):
|
|
108
|
+
if executable.is_file():
|
|
109
|
+
target = cache / executable.relative_to(original)
|
|
110
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
target.symlink_to(executable)
|
|
112
|
+
runtime_env["CRAP_GATE_CACHE_DIR"] = str(cache)
|
|
113
|
+
proc = launch(runtime_env, "--json")
|
|
114
|
+
assert proc.returncode == 2
|
|
115
|
+
assert "Python is not cached" in proc.stderr
|
|
116
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Pocock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Logic Prototype
|
|
2
|
+
|
|
3
|
+
A single, self-contained HTML file (a **shareable demo**) that lets anyone drive a state model by clicking buttons. Use this when the question is about **business logic, state transitions, or data shape**: the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases.
|
|
4
|
+
|
|
5
|
+
Because it's one file with nothing to install, you can hand it to a non-developer (a designer, a PM, a domain expert) and let them feel the model for themselves. So it speaks their language, not the code's.
|
|
6
|
+
|
|
7
|
+
## When this is the right shape
|
|
8
|
+
|
|
9
|
+
- "I'm not sure if this state machine handles the edge case where X then Y."
|
|
10
|
+
- "Does this data model actually let me represent the case where..."
|
|
11
|
+
- "I want to feel out what the API should look like before writing it."
|
|
12
|
+
- Anything where someone wants to **press buttons and watch state change**.
|
|
13
|
+
|
|
14
|
+
If the question is "what should this look like," this is the wrong branch. Use [UI.md](UI.md).
|
|
15
|
+
|
|
16
|
+
## Process
|
|
17
|
+
|
|
18
|
+
### 1. State the question
|
|
19
|
+
|
|
20
|
+
Before writing code, write down what state model and what question you're prototyping. One paragraph, at the top of the demo (in a visible intro, not just a comment). A logic prototype that answers the wrong question is pure waste, so make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK.
|
|
21
|
+
|
|
22
|
+
### 2. Isolate the logic in a portable module
|
|
23
|
+
|
|
24
|
+
Put the actual logic (the bit that's answering the question) in a single `<script>` block written as a small, pure module that could be lifted out and dropped into the real codebase later. The page around it is throwaway; this module isn't.
|
|
25
|
+
|
|
26
|
+
The right shape depends on the question:
|
|
27
|
+
|
|
28
|
+
- **A pure reducer**: `(state, action) => state`. Good when actions are discrete events and state is a single value.
|
|
29
|
+
- **A state machine**: explicit states and transitions. Good when "which actions are even legal right now" is part of the question.
|
|
30
|
+
- **A small set of pure functions** over a plain data type. Good when there's no implicit current state, just transformations.
|
|
31
|
+
- **A class or module with a clear method surface** when the logic genuinely owns ongoing internal state.
|
|
32
|
+
|
|
33
|
+
Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a page. Keep it pure: no DOM, no `document`, no button handlers reaching inside it. The page calls into it; nothing flows the other direction. This is what makes the prototype useful past its own lifetime: once the question's answered, the validated reducer / machine / function set lifts into the real module on its own.
|
|
34
|
+
|
|
35
|
+
### 3. Build the shareable HTML file
|
|
36
|
+
|
|
37
|
+
One file, plain HTML/CSS/JS: no framework, no bundler, no server, everything inline so it opens by double-click and survives being emailed around. Anyone should be able to run it by opening it. Write it to the path the invoking prompt names (see [SKILL.md](SKILL.md) rule 1).
|
|
38
|
+
|
|
39
|
+
Write it for a non-developer. Every label is in **domain language**, not code: buttons and state read like the business, not the reducer. Explain in plain words what's happening.
|
|
40
|
+
|
|
41
|
+
Lay it out with a clean hierarchy, top to bottom:
|
|
42
|
+
|
|
43
|
+
1. **Title and one-line explanation** of what this demo lets you explore (the question from step 1).
|
|
44
|
+
2. **Current state**: the full relevant state, rendered as a readable panel (labelled fields, not a raw JSON dump), re-rendered after every click so the change is visible. Where it helps a non-developer follow, call out what just changed.
|
|
45
|
+
3. **Free-play buttons**: one button per action, always available, so anyone can poke at the model in any order. Each click dispatches its action and re-renders the state.
|
|
46
|
+
4. **Guided walkthroughs**: a set of **scenarios**, one per tab. Each tab holds a short plain-language description of the scenario (the situation it sets up and what to watch for) and underneath it, the ordered **buttons to press** for that scenario. Each step is a real button: clicking it performs that action and moves to the next step. Starting a walkthrough resets to a known initial state so the scenario runs the same way every time.
|
|
47
|
+
|
|
48
|
+
Choose scenarios that demonstrate the awkward cases, the ones hard to reason about on paper: the happy path, a tricky edge case, an attempt at something that should be illegal.
|
|
49
|
+
|
|
50
|
+
Keep it beautiful but restrained: clean typography, generous spacing, one accent colour. No animations, no gimmicks: nothing that competes with the state and the buttons. It must read well on a phone: a single column, buttons at least 44px tall.
|
|
51
|
+
|
|
52
|
+
### 4. Hand it over
|
|
53
|
+
|
|
54
|
+
Send them the file, or open it for them. They'll click through the walkthroughs and free-play whenever they get to it; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different"; those are the bugs in the _idea_, which is the whole point. If they want new actions or a new scenario, add them. Prototypes evolve.
|
|
55
|
+
|
|
56
|
+
### 5. Capture the answer
|
|
57
|
+
|
|
58
|
+
Once the prototype has answered its question, capture the verdict the way [SKILL.md](SKILL.md) rule 6 describes (an issue comment when there is an issue). The validated reducer / machine / function set lifts into the real module; the HTML shell is thrown away.
|
|
59
|
+
|
|
60
|
+
## Anti-patterns
|
|
61
|
+
|
|
62
|
+
- **Don't add tests.** A prototype that needs tests is no longer a prototype.
|
|
63
|
+
- **Don't wire it to the real database.** Use in-memory state unless the question is specifically about persistence.
|
|
64
|
+
- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question.
|
|
65
|
+
- **Don't blur the logic and the page together.** If the pure module references the DOM, `document`, or button handlers, it's no longer liftable. Keep the page as a thin shell over a pure module.
|
|
66
|
+
- **Don't reach for a framework, bundler, or server.** One file the recipient double-clicks; a React app or a dev server defeats "shareable".
|
|
67
|
+
- **Don't ship the HTML shell into production.** The page is optimised for being clicked through by hand. The logic module behind it is the bit worth keeping.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prototype
|
|
3
|
+
description: Build a throwaway prototype to answer a design question, delivered as one self-contained HTML file. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
Modified copy of https://github.com/mattpocock/skills/tree/3cca18b368ae95cdbdebbff572ccafa662551015/skills/engineering/prototype
|
|
8
|
+
(mattpocock-skills v1.2.3). Copyright (c) 2026 Matt Pocock, MIT — see LICENSE.
|
|
9
|
+
Changes: the UI branch produces one self-contained HTML file holding every
|
|
10
|
+
variation (no routes, no dev server) instead of routes in the running app; the
|
|
11
|
+
file is written to the path the invoking prompt names; the verdict is captured as
|
|
12
|
+
an issue comment instead of a throwaway branch.
|
|
13
|
+
-->
|
|
14
|
+
|
|
15
|
+
# Prototype
|
|
16
|
+
|
|
17
|
+
A prototype is **throwaway code that answers a question**. The question decides the shape, but the delivery is always the same: **one self-contained HTML file** that opens by double-click, with no build step and no server. That is what lets a machine with no browser serve it as a file and a person open it on a phone.
|
|
18
|
+
|
|
19
|
+
## Pick a branch
|
|
20
|
+
|
|
21
|
+
Identify which question is being answered, using the user's prompt, the surrounding code, or by asking if the user is around:
|
|
22
|
+
|
|
23
|
+
- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Free-play buttons plus tabbed guided walkthroughs that push the state machine through cases that are hard to reason about on paper, and that a non-developer can drive.
|
|
24
|
+
- **"What should this look like?"** → [UI.md](UI.md). Several radically different UI variations in one page, switchable inside the page from a floating bottom bar.
|
|
25
|
+
|
|
26
|
+
The two branches produce very different pages, so getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype.
|
|
27
|
+
|
|
28
|
+
## Rules that apply to both
|
|
29
|
+
|
|
30
|
+
1. **One file, clearly a prototype.** Everything inline: HTML, CSS, JS, sample data. No framework, no bundler, no server, no network requests. Write it to the path the invoking prompt names; with no path given, put it next to the module or page it prototypes for, named `<name>.prototype.html`, so a casual reader sees it is a prototype and not production.
|
|
31
|
+
2. **Trivial to run.** Opening the file is the whole procedure. No thinking required to start it.
|
|
32
|
+
3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on.
|
|
33
|
+
4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast.
|
|
34
|
+
5. **Surface the state.** After every action (logic) or on every variant switch (UI), render the full relevant state so the user can see what changed.
|
|
35
|
+
6. **Capture the verdict where the next reader looks.** When the work has a GitHub issue, post the verdict as a comment on it: the question, the answer (which variant, which model, or "none of these"), and the reason. The next stage is often a fresh session that reads the issue thread and has never seen the file. With no issue, tell the user the verdict and where the file is. Fold any validated decision into the real code (a validated reducer lifts into the real module; a winning layout is rebuilt properly, not copied). The prototype file itself never lands on `main`: leave it uncommitted, or delete it once the verdict is recorded.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gh issue comment <number> --repo <owner/repo> --body-file /tmp/verdict.md
|
|
39
|
+
```
|