From 5879480cad3aa5daaf97088160a6fa3a5ba4fb38 Mon Sep 17 00:00:00 2001
From: satyaborg <satya.borg@gmail.com>
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] <spec.md> [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] <spec.md> [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-<slug>.md path if you write a file; otherwise return markdown on stdout."
   fi
   cat <<EOF
-Use this bundled devloop skill to produce one implementation spec.
+Use the installed devloop-spec skill to produce one implementation spec.
 
 Current date: $today
 $output_line
 
 If the source context is missing or too thin, follow the skill's interview path before drafting. Return only the final markdown spec. Do not wrap it in a code fence.
 
-Bundled skill:
-$skill
+Bundled skill path, for fallback only: $skill
 
 Context:
 $context
diff --git a/install.sh b/install.sh
index fbe3d26..301603b 100755
--- a/install.sh
+++ b/install.sh
@@ -12,9 +12,12 @@ while [ -L "$SCRIPT_PATH" ]; do
 done
 
 ROOT="$(cd -P "$(dirname "$SCRIPT_PATH")" >/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
