From 5879480cad3aa5daaf97088160a6fa3a5ba4fb38 Mon Sep 17 00:00:00 2001 From: satyaborg Date: Fri, 29 May 2026 12:53:35 +1000 Subject: [PATCH] chore: install devloop skills --- AGENTS.md | 2 +- CLAUDE.md | 1 + README.md | 13 +- devloop | 25 ++- install.sh | 7 +- skill_helpers.sh | 212 ++++++++++++++++++ skills/devloop-review/SKILL.md | 2 + skills/devloop-spec/SKILL.md | 4 + .../devloop-spec/references/spec-template.md | 0 tests/devloop_test.sh | 38 +++- 10 files changed, 289 insertions(+), 15 deletions(-) create mode 120000 CLAUDE.md create mode 100644 skill_helpers.sh rename templates/spec.md => skills/devloop-spec/references/spec-template.md (100%) diff --git a/AGENTS.md b/AGENTS.md index 601620b..c54c24d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization -This is a Bash CLI project. The active runtime is the root `devloop` executable. `install.sh` links it into a local bin directory, `tests/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation prompt asset, `skills/devloop-review/SKILL.md` is the review prompt asset, and `templates/spec.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here. +This is a Bash CLI project. The active runtime is the root `devloop` executable. `install.sh` links it into a local bin directory and installs bundled skills into `~/.agents/skills`, `tests/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation skill, `skills/devloop-review/SKILL.md` is the review skill, and `skills/devloop-spec/references/spec-template.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here. ## Build, Test, and Development Commands diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 71162b7..230a59c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ Run without installing: ./devloop --help ``` +`install.sh` also installs the bundled Agent Skills globally under `~/.agents/skills`. +After install or update, verify the local setup: + +```sh +devloop doctor +``` + ## Quick Start Create a spec: @@ -44,7 +51,7 @@ devloop --create-pr .specs/change.md ## Specs -A good spec is short, concrete, and verifiable. Start from [`templates/spec.md`](templates/spec.md), or generate one: +A good spec is short, concrete, and verifiable. Start from [`skills/devloop-spec/references/spec-template.md`](skills/devloop-spec/references/spec-template.md), or generate one: ```sh devloop spec @@ -94,7 +101,9 @@ devloop [options] [max=5] ```sh bash -n devloop install.sh ./devloop --help -DEVLOOP_BIN_DIR="$(mktemp -d)/bin" ./install.sh +tmp="$(mktemp -d)" +DEVLOOP_BIN_DIR="$tmp/bin" DEVLOOP_SKILLS_DIR="$tmp/skills" ./install.sh +PATH="$tmp/bin:$PATH" DEVLOOP_SKILLS_DIR="$tmp/skills" devloop doctor ``` The supported runtime is the root [`devloop`](devloop) Bash script. diff --git a/devloop b/devloop index d2e94df..2232368 100755 --- a/devloop +++ b/devloop @@ -14,6 +14,7 @@ while [ -L "$SCRIPT_PATH" ]; do esac done ROOT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)" +source "$ROOT_DIR/skill_helpers.sh" USE_TUI=false if [ -t 1 ]; then USE_TUI=true; fi @@ -52,6 +53,16 @@ main() { return $? fi + if [ "${1:-}" = "doctor" ]; then + shift + if [ "$#" -gt 0 ]; then + printf '%s\n' "usage: devloop doctor" >&2 + return 2 + fi + devloop_doctor "$ROOT_DIR" + return $? + fi + if [ "$#" -eq 0 ] || has_arg "-h" "$@" || has_arg "--help" "$@"; then welcome return 0 @@ -75,6 +86,7 @@ Usage: devloop [options] [max=5] Common commands: + devloop doctor devloop spec "add retry behavior to the chat sender" devloop .specs/change.md devloop --tui .specs/change.md @@ -1081,7 +1093,7 @@ review_prompt() { local criteria priors review_skill strict_rule criteria="$(criteria_block "$criteria_file")" priors="$(list_reviews "$slug" "$pass" "$max")" - review_skill="$(cat "$ROOT_DIR/skills/devloop-review/SKILL.md")" + review_skill="$ROOT_DIR/skills/devloop-review/SKILL.md" strict_rule="" if [ "$strict" = true ]; then strict_rule=" @@ -1099,8 +1111,8 @@ $priors Acceptance criteria: $criteria Output path: $output -Bundled review skill: -$review_skill +Skill: use the installed devloop-review skill. +Bundled skill path, for fallback only: $review_skill Steps: 1. Read the spec and track. @@ -1644,7 +1656,7 @@ spec_command() { local context today prompt cmd_args=() context="$(resolve_spec_context "${context_items[@]}")" today="$(date +%F)" - prompt="$(spec_prompt "$context" "$output" "$(cat "$skill")" "$today")" + prompt="$(spec_prompt "$context" "$output" "$skill" "$today")" if [ "$agent" = "codex" ]; then run_with_prompt "$PWD" "" "" "$prompt" codex exec "${CODEX_REASONING_ARGS[@]}" -s read-only -C "$PWD" - elif [ "$agent" = "claude" ]; then @@ -1705,15 +1717,14 @@ spec_prompt() { output_line="Output path: choose a .specs/YYYY-MM-DD-.md path if you write a file; otherwise return markdown on stdout." fi cat </dev/null 2>&1 && pwd)" +source "$ROOT/skill_helpers.sh" + BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}" TARGET="$BIN_DIR/devloop" SOURCE="$ROOT/devloop" +SKILL_STATUS=0 if [ ! -f "$SOURCE" ]; then echo "missing devloop executable: $SOURCE" >&2 @@ -26,6 +29,7 @@ chmod +x "$SOURCE" ln -sfn "$SOURCE" "$TARGET" echo "installed devloop -> $SOURCE" +devloop_install_skills "$ROOT" || SKILL_STATUS=$? case ":${PATH:-}:" in *":$BIN_DIR:"*) ;; @@ -37,4 +41,5 @@ case ":${PATH:-}:" in esac echo -echo "try: devloop --help" +echo "try: devloop doctor" +exit "$SKILL_STATUS" diff --git a/skill_helpers.sh b/skill_helpers.sh new file mode 100644 index 0000000..bd8c700 --- /dev/null +++ b/skill_helpers.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash + +devloop_skills_dir() { + printf '%s\n' "${DEVLOOP_SKILLS_DIR:-$HOME/.agents/skills}" +} + +devloop_checksum_file() { + local file="$1" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + else + cksum "$file" | awk '{print $1}' + fi +} + +devloop_skill_tree_checksum() { + local root="$1" + local manifest checksum + if [ ! -d "$root" ]; then return 1; fi + manifest="$(mktemp "${TMPDIR:-/tmp}/devloop-skill-manifest.XXXXXX")" + ( + cd "$root" || exit 1 + find . -type f ! -name ".devloop-checksum" | LC_ALL=C sort | while IFS= read -r file; do + printf '%s %s\n' "$(devloop_checksum_file "$file")" "${file#./}" + done + ) > "$manifest" || { + rm -f "$manifest" + return 1 + } + checksum="$(devloop_checksum_file "$manifest")" + rm -f "$manifest" + printf '%s\n' "$checksum" +} + +devloop_skill_name() { + sed -n 's/^name: *//p' "$1" | head -n 1 +} + +devloop_valid_skill_name() { + printf '%s\n' "$1" | grep -Eq '^[a-z0-9]+(-[a-z0-9]+)*$' +} + +devloop_can_replace_skill() { + local dest="$1" + local force="${DEVLOOP_FORCE:-0}" + local marker recorded current + if [ "$force" = "1" ]; then return 0; fi + if [ ! -e "$dest" ] && [ ! -L "$dest" ]; then return 0; fi + if [ -L "$dest" ]; then return 1; fi + + marker="$dest/.devloop-checksum" + if [ ! -f "$marker" ]; then return 1; fi + recorded="$(cat "$marker")" + current="$(devloop_skill_tree_checksum "$dest")" || return 1 + [ "$recorded" = "$current" ] +} + +devloop_install_skills() { + local root="$1" + local skills_dir mode source name dest checksum status + skills_dir="$(devloop_skills_dir)" + mode="${DEVLOOP_SKILL_INSTALL:-copy}" + status=0 + + case "$mode" in + copy|link) ;; + *) + printf 'unknown DEVLOOP_SKILL_INSTALL: %s\n' "$mode" >&2 + return 2 + ;; + esac + + if ! mkdir -p "$skills_dir"; then + printf 'failed to create skills directory: %s\n' "$skills_dir" >&2 + return 1 + fi + for source in "$root"/skills/*; do + if [ ! -d "$source" ]; then continue; fi + name="$(basename "$source")" + dest="$skills_dir/$name" + checksum="$(devloop_skill_tree_checksum "$source")" || { + printf 'failed to checksum bundled skill: %s\n' "$source" >&2 + status=1 + continue + } + + if ! devloop_can_replace_skill "$dest"; then + printf 'skipping modified skill: %s (set DEVLOOP_FORCE=1 to overwrite)\n' "$dest" >&2 + continue + fi + + if ! rm -rf "$dest"; then + printf 'failed to replace skill: %s\n' "$dest" >&2 + status=1 + continue + fi + + if [ "$mode" = "link" ]; then + if ! ln -s "$source" "$dest"; then + printf 'failed to link skill: %s\n' "$dest" >&2 + status=1 + continue + fi + printf 'installed skill %s -> %s\n' "$name" "$source" + else + if ! mkdir -p "$dest"; then + printf 'failed to create skill directory: %s\n' "$dest" >&2 + status=1 + continue + fi + if ! cp -R "$source/." "$dest/"; then + printf 'failed to copy skill: %s\n' "$dest" >&2 + status=1 + continue + fi + if ! printf '%s\n' "$checksum" > "$dest/.devloop-checksum"; then + printf 'failed to write skill checksum: %s\n' "$dest/.devloop-checksum" >&2 + status=1 + continue + fi + printf 'installed skill %s -> %s\n' "$name" "$dest" + fi + done + + return "$status" +} + +devloop_doctor_command() { + local command="$1" + local resolved + resolved="$(command -v "$command" 2>/dev/null || true)" + if [ -n "$resolved" ]; then + printf '[ok] %s: %s\n' "$command" "$resolved" + return 0 + fi + printf '[fail] missing command: %s\n' "$command" >&2 + return 1 +} + +devloop_doctor_skills() { + local root="$1" + local skills_dir source name dest declared bundled installed status + skills_dir="$(devloop_skills_dir)" + status=0 + + for source in "$root"/skills/*; do + if [ ! -d "$source" ]; then continue; fi + name="$(basename "$source")" + dest="$skills_dir/$name" + + if [ ! -f "$dest/SKILL.md" ]; then + printf '[fail] missing skill: %s\n' "$dest/SKILL.md" >&2 + status=1 + continue + fi + + declared="$(devloop_skill_name "$dest/SKILL.md")" + if [ "$declared" != "$name" ]; then + printf '[fail] skill name mismatch: %s declares %s\n' "$dest/SKILL.md" "$declared" >&2 + status=1 + continue + fi + + if ! devloop_valid_skill_name "$declared"; then + printf '[fail] invalid skill name: %s\n' "$declared" >&2 + status=1 + continue + fi + + bundled="$(devloop_skill_tree_checksum "$source")" || { + printf '[fail] failed to checksum bundled skill: %s\n' "$source" >&2 + status=1 + continue + } + installed="$(devloop_skill_tree_checksum "$dest")" || { + printf '[fail] failed to checksum installed skill: %s\n' "$dest" >&2 + status=1 + continue + } + + if [ "$bundled" != "$installed" ]; then + printf '[fail] stale skill: %s (run ./install.sh)\n' "$dest" >&2 + status=1 + continue + fi + + printf '[ok] skill %s: %s\n' "$name" "$dest" + done + + return "$status" +} + +devloop_doctor() { + local root="$1" + local status=0 + + printf 'devloop doctor\n' + devloop_doctor_command devloop || status=1 + devloop_doctor_command git || status=1 + devloop_doctor_command codex || status=1 + devloop_doctor_command claude || status=1 + devloop_doctor_skills "$root" || status=1 + + if [ "$status" -eq 0 ]; then + printf 'devloop doctor: ready\n' + else + printf 'devloop doctor: not ready\n' >&2 + fi + return "$status" +} diff --git a/skills/devloop-review/SKILL.md b/skills/devloop-review/SKILL.md index 7a80241..4f418c1 100644 --- a/skills/devloop-review/SKILL.md +++ b/skills/devloop-review/SKILL.md @@ -1,6 +1,8 @@ --- name: devloop-review description: Use this skill when reviewing a devloop implementation pass against a spec, track, diff, prior reviews, acceptance criteria, or engineering quality gates. Decide ACCEPT, REJECT, or UNCLEAR with concrete evidence and fix instructions. +metadata: + devloop-managed: "true" --- # Devloop Review diff --git a/skills/devloop-spec/SKILL.md b/skills/devloop-spec/SKILL.md index 8b11a58..00cfa65 100644 --- a/skills/devloop-spec/SKILL.md +++ b/skills/devloop-spec/SKILL.md @@ -1,6 +1,8 @@ --- name: devloop-spec description: Use this skill when the user wants a devloop-ready implementation spec from a rough idea, notes, file, URL, issue, research, or conversation context. Interview from a cold start when source material is too thin; otherwise distill the provided material into one concrete spec. +metadata: + devloop-managed: "true" --- # Devloop Spec @@ -12,6 +14,8 @@ Produce one implementation spec that conforms to the devloop standard. This skil The output is the spec that `devloop` will use as its implementation input. +When a starter document is needed, read `references/spec-template.md`. + ## Scope Guard Write exactly one spec sized for one worktree and one PR. Push back before drafting when the request mixes multiple logical changes, depends on an unresolved preparatory refactor, or would plausibly exceed about 300 meaningful changed lines. diff --git a/templates/spec.md b/skills/devloop-spec/references/spec-template.md similarity index 100% rename from templates/spec.md rename to skills/devloop-spec/references/spec-template.md diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh index 72947c5..5f16719 100755 --- a/tests/devloop_test.sh +++ b/tests/devloop_test.sh @@ -26,7 +26,7 @@ equals() { [[ "$actual" == "$expected" ]] || fail "$label expected [$expected], got [$actual]" } -bash -n "$ROOT/devloop" "$ROOT/install.sh" +bash -n "$ROOT/devloop" "$ROOT/install.sh" "$ROOT/skill_helpers.sh" ok "bash syntax" DEVLOOP_LIB=1 @@ -35,6 +35,7 @@ unset DEVLOOP_LIB help="$("$ROOT/devloop" --help)" contains "$help" "Common commands:" "help" +contains "$help" "devloop doctor" "help" contains "$help" "--create-pr" "help" ok "help output" @@ -47,10 +48,15 @@ for skill in "$ROOT"/skills/*/SKILL.md; do name="$(sed -n 's/^name: *//p' "$skill" | head -n 1)" description="$(sed -n 's/^description: *//p' "$skill" | head -n 1)" dirname="$(basename "$(dirname "$skill")")" + reference_nesting="" [[ "$name" == "$dirname" ]] || fail "skill name mismatch: $skill declares $name" [[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || fail "invalid skill name: $name" [[ -n "$description" ]] || fail "missing skill description: $skill" [[ "${#description}" -le 1024 ]] || fail "skill description too long: $skill" + if [ -d "$(dirname "$skill")/references" ]; then + reference_nesting="$(find "$(dirname "$skill")/references" -mindepth 2 -type f -print)" + [[ -z "$reference_nesting" ]] || fail "nested skill references: $reference_nesting" + fi done ok "skill metadata" @@ -104,8 +110,8 @@ sed 's/| Maintainability | PASS |/| Maintainability | FAIL |/' "$review_file" > if has_passing_quality_matrix "$work/review-quality-fail.md"; then fail "has_passing_quality_matrix accepted failing matrix"; fi review_prompt_text="$(review_prompt codex "$criteria_file" ".codex/tracks/test.md" main 1 ".codex/reviews/test-r1.md" test 5 "$criteria_file" true)" -contains "$review_prompt_text" "Bundled review skill:" "review prompt" -contains "$review_prompt_text" "Devloop Review" "review prompt" +contains "$review_prompt_text" "Skill: use the installed devloop-review skill." "review prompt" +contains "$review_prompt_text" "Bundled skill path, for fallback only: $ROOT/skills/devloop-review/SKILL.md" "review prompt" contains "$review_prompt_text" "Engineering quality matrix" "review prompt" findings_a="$work/findings-a.md" @@ -179,13 +185,37 @@ contains "$uuid_one" "00000000-0000-4000-8000-" "new_uuid fallback format" ok "pure helpers" bin_dir="$work/bin" -DEVLOOP_BIN_DIR="$bin_dir" "$ROOT/install.sh" >/tmp/devloop-install-test.out +skills_dir="$work/skills" +DEVLOOP_BIN_DIR="$bin_dir" DEVLOOP_SKILLS_DIR="$skills_dir" "$ROOT/install.sh" >/tmp/devloop-install-test.out [[ -x "$ROOT/devloop" ]] || fail "devloop is not executable" [[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink" +[[ -f "$skills_dir/devloop-spec/SKILL.md" ]] || fail "installer did not install spec skill" +[[ -f "$skills_dir/devloop-spec/references/spec-template.md" ]] || fail "installer did not install spec template reference" +[[ -f "$skills_dir/devloop-review/SKILL.md" ]] || fail "installer did not install review skill" +[[ -f "$skills_dir/devloop-review/.devloop-checksum" ]] || fail "installer did not write checksum" "$bin_dir/devloop" --help >/tmp/devloop-help-test.out contains "$(cat /tmp/devloop-help-test.out)" "Spec-driven code and review loop." "installed help" ok "installer" +printf '%s\n' "user edit" >> "$skills_dir/devloop-review/SKILL.md" +DEVLOOP_BIN_DIR="$bin_dir" DEVLOOP_SKILLS_DIR="$skills_dir" "$ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1 +contains "$(cat /tmp/devloop-install-skip.out)" "skipping modified skill" "installer modified skill guard" +contains "$(cat /tmp/devloop-install-skip.out)" "try: devloop doctor" "installer guidance after skill skip" +contains "$(cat "$skills_dir/devloop-review/SKILL.md")" "user edit" "installer modified skill preserved" +DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" DEVLOOP_SKILLS_DIR="$skills_dir" "$ROOT/install.sh" >/tmp/devloop-install-force.out +if grep -q "user edit" "$skills_dir/devloop-review/SKILL.md"; then fail "installer force did not restore skill"; fi +ok "installer skill updates" + +fake_bin="$work/fake-bin" +mkdir -p "$fake_bin" +printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/codex" +printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/claude" +chmod +x "$fake_bin/codex" "$fake_bin/claude" +doctor_output="$(DEVLOOP_SKILLS_DIR="$skills_dir" PATH="$bin_dir:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)" +contains "$doctor_output" "devloop doctor: ready" "doctor" +contains "$doctor_output" "[ok] skill devloop-spec" "doctor" +ok "doctor" + agent="$work/spec-agent" cat > "$agent" <<'AGENT' #!/usr/bin/env bash