#!/usr/bin/env bash # Install the DeepSeek Harness side of claude-harness. # # This script never installs the Cordis bundle into a profile: the Desktop # profile is Electron-managed, so the bundle goes through the GUI Plugins page # or the model-facing `plugin_manager` tool. The script prepares the global # instructions, the user skill root, and the role files, then prints the bundle # path. # # Usage: # bash dsh/install.sh --check # bash dsh/install.sh --global # bash dsh/install.sh --skills [--superpowers-from ] # bash dsh/install.sh --roles # bash dsh/install.sh --all # bash dsh/install.sh --openspec # # Run it from a terminal, not from inside a DSH session: --global, --skills and # --roles write outside the session workspace and would require approval. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" BUNDLE="$ROOT/dsh/bundle" # O-2: remember whether DSH_HOME came from the environment. Isolating HOME # alone is not enough: an explicit DSH_HOME still writes outside the isolated # tree (a temp HOME with the real DSH_HOME, typically a test or a detached # shell), so the install steps warn about that mixed state. DSH_HOME_FROM_ENV=0 if [ -n "${DSH_HOME:-}" ]; then DSH_HOME_FROM_ENV=1 fi DSH_HOME="${DSH_HOME:-$HOME/.dsh}" DSH_SUPERPOWERS_REF="${DSH_SUPERPOWERS_REF:-v7.0.1}" DSH_SUPERPOWERS_REPO="${DSH_SUPERPOWERS_REPO:-https://github.com/obra/superpowers}" DO_CHECK=0 DO_GLOBAL=0 DO_SKILLS=0 DO_ROLES=0 DO_OPENSPEC=0 SUPERPOWERS_FROM="" SUPERPOWERS_FROM_SET=0 CLONE_HINT="git clone --depth 1 --branch $DSH_SUPERPOWERS_REF $DSH_SUPERPOWERS_REPO $DSH_HOME/superpowers" # Synced staging trees, per source kind: $DSH_HOME/superpowers is reserved for # the pinned clone (T3.1), so explicit sources stage beside it. PINNED_OUT="$DSH_HOME/superpowers/skills.synced" EXPLICIT_OUT="$DSH_HOME/.skills.synced" usage() { cat < --check verify python3, git, node, hooks, bundle, skills, and roles (offline) --global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md --skills sync + verify + copy superpowers and local dsh/skills into \$DSH_HOME/skills --roles copy roles/ into \$DSH_HOME/roles --all --check + --global + --skills + --roles, then print the bundle step --openspec add the agents skill adapter to the current OpenSpec project --superpowers-from DIR sync superpowers skills from DIR (the repo root or its skills/) Superpowers source, first match wins: 1. --superpowers-from DIR 2. \$DSH_SUPERPOWERS_SOURCE 3. the pinned clone \$DSH_HOME/superpowers at \$DSH_SUPERPOWERS_REF (default v7.0.1), cloned or updated from \$DSH_SUPERPOWERS_REPO (default https://github.com/obra/superpowers); offline it is reused only when already on the ref 4. legacy: the Claude Code plugin cache, then ~/.claude/skills (raw copy, warns; removal is #66) Explicit sources are synced in place (no network, no pin enforcement); the synced tree is verified before anything is copied. Get the default source once: $CLONE_HINT Bundle installation itself is done in DSH: Plugins -> install a bundle from $BUNDLE or ask the agent to call plugin_manager install_bundle with that target. EOF } if [ "$#" -eq 0 ]; then DO_CHECK=1 fi while [ "$#" -gt 0 ]; do case "$1" in --check) DO_CHECK=1 ;; --global) DO_GLOBAL=1 ;; --skills) DO_SKILLS=1 ;; --roles) DO_ROLES=1 ;; --openspec) DO_OPENSPEC=1 ;; --all) DO_CHECK=1; DO_GLOBAL=1; DO_SKILLS=1; DO_ROLES=1 ;; --superpowers-from) if [ "$#" -lt 2 ]; then echo "option --superpowers-from needs a directory" >&2 exit 2 fi SUPERPOWERS_FROM="$2" SUPERPOWERS_FROM_SET=1 shift ;; --superpowers-from=*) SUPERPOWERS_FROM="${1#*=}" SUPERPOWERS_FROM_SET=1 ;; -h|--help) usage; exit 0 ;; *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; esac shift done # An explicit flag with an empty value is a typo, not "use the default source": # falling through to legacy would mask exactly the misconfiguration D-2 forbids. if [ "$SUPERPOWERS_FROM_SET" = 1 ] && [ -z "$SUPERPOWERS_FROM" ]; then echo "option --superpowers-from needs a non-empty directory" >&2 exit 2 fi # Normalize a superpowers source: accept the repo root (with skills/) or the # skills directory itself. Prints the skills directory, returns 1 otherwise. normalize_superpowers_dir() { local dir="${1:-}" [ -n "$dir" ] || return 1 if [ -f "$dir/skills/using-superpowers/SKILL.md" ]; then printf '%s\n' "$dir/skills" return 0 fi if [ -f "$dir/using-superpowers/SKILL.md" ]; then printf '%s\n' "$dir" return 0 fi return 1 } # The checkout root the sync script consumes: /skills must exist. # Returns 1 when the skills directory is not part of a checkout layout. checkout_root() { local skills_dir="${1:-}" [ "$(basename "$skills_dir")" = skills ] || return 1 printf '%s\n' "$(dirname "$skills_dir")" return 0 } # Resolve the superpowers install source without touching the network. # Prints "\t\t". Returns 2 when an explicitly # configured source is invalid or is not a checkout layout (never fall back # silently; --check and --skills must agree on the verdict), 1 when none is # found. superpowers_source() { local skills="" checkout="" legacy="" if [ -n "$SUPERPOWERS_FROM" ]; then skills="$(normalize_superpowers_dir "$SUPERPOWERS_FROM")" || return 2 checkout="$(checkout_root "$skills")" || return 2 printf 'explicit\t%s\t%s\n' "$checkout" "$skills" return 0 fi if [ -n "${DSH_SUPERPOWERS_SOURCE:-}" ]; then skills="$(normalize_superpowers_dir "$DSH_SUPERPOWERS_SOURCE")" || return 2 checkout="$(checkout_root "$skills")" || return 2 printf 'explicit\t%s\t%s\n' "$checkout" "$skills" return 0 fi if skills="$(normalize_superpowers_dir "$DSH_HOME/superpowers")"; then printf 'pinned\t%s\t%s\n' "$DSH_HOME/superpowers" "$skills" return 0 fi legacy="$(ls -d "$HOME"/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills 2>/dev/null | sort -V | tail -1 || true)" # A cache directory without using-superpowers is a broken cache, not a source: # accepting it would copy 0 skills and report success (the silent empty # bootstrap this change removes). if [ -n "$legacy" ] && skills="$(normalize_superpowers_dir "$legacy")"; then printf 'legacy\t-\t%s\n' "$skills" return 0 fi if skills="$(normalize_superpowers_dir "$HOME/.claude/skills")"; then printf 'legacy\t-\t%s\n' "$skills" return 0 fi return 1 } # Offline ref report for --check: the synced meta first, then git, then config. # The meta path depends on the source kind (T3.1): pinned reads the clone's # skills.synced, explicit reads the side staging tree. report_checkout_ref() { local checkout="${1:-}" meta="${2:-}" ref="" version="" tag="" sha="" if [ -n "$meta" ] && [ -f "$meta" ]; then ref="$(sed -n 's/^[[:space:]]*"ref":[[:space:]]*"\([^"]*\)".*/\1/p' "$meta" | head -n 1)" version="$(sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "$meta" | head -n 1)" if [ -n "$version" ]; then echo "ref ${ref:-unknown} (synced, version $version)" else echo "ref ${ref:-unknown} (synced)" fi return 0 fi if [ -n "$checkout" ] && [ -d "$checkout/.git" ]; then tag="$(git -C "$checkout" describe --tags --exact-match HEAD 2>/dev/null || true)" if [ -n "$tag" ]; then echo "ref $tag (git)" return 0 fi sha="$(git -C "$checkout" rev-parse --short HEAD 2>/dev/null || true)" if [ -n "$sha" ]; then echo "git $sha (ref $DSH_SUPERPOWERS_REF configured)" return 0 fi fi echo "ref $DSH_SUPERPOWERS_REF (configured)" } # Ref recorded for an explicit source: the exact tag, else the short sha, # else "unknown". An explicit source is not pin-enforced, so any of these is # honest metadata for .sync-meta.json. source_ref() { local root="${1:-}" tag="" sha="" if [ -d "$root/.git" ]; then tag="$(git -C "$root" describe --tags --exact-match HEAD 2>/dev/null || true)" [ -n "$tag" ] && { printf '%s\n' "$tag"; return 0; } sha="$(git -C "$root" rev-parse --short HEAD 2>/dev/null || true)" [ -n "$sha" ] && { printf '%s\n' "$sha"; return 0; } fi printf 'unknown\n' } # The effective explicit source directory: the flag first, then the env var. explicit_source_dir() { if [ -n "$SUPERPOWERS_FROM" ]; then printf '%s\n' "$SUPERPOWERS_FROM" else printf '%s\n' "${DSH_SUPERPOWERS_SOURCE:-}" fi } # Human-readable name of the source that made superpowers_source() return 2. explicit_source_name() { if [ -n "$SUPERPOWERS_FROM" ]; then printf '%s\n' "--superpowers-from $SUPERPOWERS_FROM" else printf '%s\n' "DSH_SUPERPOWERS_SOURCE=$DSH_SUPERPOWERS_SOURCE" fi } # Report an invalid explicit source with the accurate reason: either the # source has no using-superpowers/SKILL.md, or it is not a checkout layout # (both are verdict 2, and --check and --skills must fail the same way). report_invalid_explicit_source() { local dir="" skills="" dir="$(explicit_source_dir)" if [ -n "$dir" ] && skills="$(normalize_superpowers_dir "$dir")"; then if ! checkout_root "$skills" >/dev/null 2>&1; then echo "FAIL: explicit superpowers source must be a checkout root or its skills/ directory: $(explicit_source_name)" >&2 return 0 fi fi echo "FAIL: explicit superpowers source is invalid: $(explicit_source_name) has no using-superpowers/SKILL.md (expected /skills/... or /...)" >&2 } check() { local failed=0 for cmd in python3 git node; do if command -v "$cmd" >/dev/null 2>&1; then echo "ok: $cmd ($("$cmd" --version 2>&1 | head -1))" else echo "WARN: $cmd not found" >&2 failed=1 fi done if [ -f "$ROOT/hooks/harness_target.py" ]; then echo "ok: hooks/harness_target.py" else echo "FAIL: $ROOT/hooks/harness_target.py is missing" >&2 failed=1 fi if [ -f "$ROOT/global/CLAUDE.md" ]; then echo "ok: global/CLAUDE.md" else echo "FAIL: $ROOT/global/CLAUDE.md is missing" >&2 failed=1 fi if [ -f "$BUNDLE/index.js" ] && [ -f "$BUNDLE/cordis.patch.yml" ]; then echo "ok: bundle $BUNDLE" else echo "FAIL: bundle is incomplete: $BUNDLE" >&2 failed=1 fi if [ -f "$BUNDLE/hooks/guard-main.sh" ] && [ -f "$BUNDLE/hooks/require-plan.sh" ]; then echo "ok: hook bridge" else echo "FAIL: hook bridge is missing (is dsh/bundle/hooks a symlink to ../../hooks?)" >&2 failed=1 fi if [ -f "$ROOT/dsh/subagent-models.yml" ]; then echo "ok: subagent overlay ($ROOT/dsh/subagent-models.yml)" else echo "WARN: subagent overlay missing; SDD runs on the session model at depth 1" >&2 fi local patch found=0 for patch in "$DSH_HOME"/profiles/*/cordis.patch.yml; do [ -f "$patch" ] || continue if grep -qs 'maxDepth: 2' "$patch"; then found=1 break fi done if [ "$found" = 1 ]; then echo "ok: subagent maxDepth=2 ($patch)" else echo "WARN: subagent maxDepth is not 2 in any profile patch; SDD mode 2 is unavailable" >&2 fi if command -v openspec >/dev/null 2>&1; then echo "ok: openspec $(openspec --version 2>/dev/null | head -1)" else echo "WARN: openspec not found; OpenSpec workflows are unavailable" >&2 fi if [ -f "$ROOT/.agents/skills/openspec-propose/SKILL.md" ]; then echo "ok: openspec agents skills ($ROOT/.agents/skills)" else echo "WARN: .agents/skills missing; run --openspec in the project" >&2 fi # --check is offline: report the pin config and whatever source exists, # never clone or fetch. echo "ok: superpowers ref ${DSH_SUPERPOWERS_REF} (DSH_SUPERPOWERS_REPO=${DSH_SUPERPOWERS_REPO})" local src="" rc=0 kind checkout skills_dir src="$(superpowers_source)" || rc=$? if [ "$rc" -eq 2 ]; then report_invalid_explicit_source failed=1 elif [ "$rc" -ne 0 ]; then if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then echo "WARN: no superpowers source; installed skills in $DSH_HOME/skills are used" >&2 else echo "WARN: no superpowers source; run: $CLONE_HINT" >&2 fi else kind="${src%%$'\t'*}"; src="${src#*$'\t'}" checkout="${src%%$'\t'*}"; skills_dir="${src#*$'\t'}" if [ "$kind" = legacy ]; then echo "WARN: legacy superpowers source ($skills_dir); prefer --superpowers-from or $DSH_HOME/superpowers" >&2 elif [ "$kind" = explicit ]; then echo "ok: superpowers source ($skills_dir); $(report_checkout_ref "$checkout" "$EXPLICIT_OUT/.sync-meta.json")" else echo "ok: superpowers source ($skills_dir); $(report_checkout_ref "$checkout" "$PINNED_OUT/.sync-meta.json")" fi fi if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then echo "ok: dsh skills ($DSH_HOME/skills)" elif [ "$rc" -eq 0 ]; then echo "WARN: superpowers skills are not copied yet; run --skills" >&2 fi if [ -f "$DSH_HOME/skills/role/SKILL.md" ]; then echo "ok: role skill" else echo "WARN: role skill not installed; run --skills" >&2 fi if [ -f "$DSH_HOME/roles/dev.md" ]; then echo "ok: roles ($DSH_HOME/roles)" elif [ -f "$HOME/.claude/roles/dev.md" ]; then echo "ok: roles (~/.claude/roles; run --roles to copy)" elif [ -f "$ROOT/roles/dev.md" ]; then echo "ok: roles ($ROOT/roles; run --roles to install)" else echo "WARN: roles not found; /role will fail until --roles is run" >&2 fi if [ -f "$DSH_HOME/roles/dev.md" ] && grep -q 'В DSH-сессии исполнение плана' "$DSH_HOME/roles/dev.md"; then echo "ok: role dsh notes" else echo "WARN: role has no DSH SDD notes; run --roles" >&2 fi echo "DSH_HOME=$DSH_HOME" return "$failed" } install_global() { if ! command -v python3 >/dev/null 2>&1; then echo "FAIL: python3 is required for --global (AGENTS.md is generated from global/CLAUDE.md)" >&2 return 1 fi warn_mixed_dsh_home mkdir -p "$DSH_HOME" local target="$DSH_HOME/AGENTS.md" if [ -f "$target" ]; then local backup="$target.bak.$(date +%s)" cp "$target" "$backup" echo "backup: $backup" fi { echo "# DeepSeek Harness global instructions" echo echo "Installed from $ROOT/global/CLAUDE.md by dsh/install.sh." echo "The dsh-harness bundle enforces guard-main/require-plan and provides the /role command." echo "DSH adapter: the model loads roles with skill role; /role is the user-facing command." echo "DSH adapter: there is no SessionStart role hint; infer the role from the first message." echo "DSH adapter: role files are ~/.dsh/roles (installed by --roles); legacy Claude Code role locations are a migration fallback." echo "DSH adapter: superpowers skills are ~/.dsh/skills (installed by --skills from --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers); legacy Claude Code locations are a migration fallback." echo "DSH adapter: a project CLAUDE.md/AGENTS.md is the instruction-file format of this harness; it does not imply a Claude Code runtime." echo "DSH adapter: codegraph MCP tools are named mcp__codegraph__codegraph_explore." echo # The shared global/CLAUDE.md is also read by Claude Code and must not # change; only the installed DSH copy points at the DSH role layer. python3 - "$ROOT/global/CLAUDE.md" <<'PY' import sys from pathlib import Path text = Path(sys.argv[1]).read_text(encoding="utf-8") sys.stdout.write(text.replace("~/.claude/roles", "~/.dsh/roles")) PY } > "$target" echo "written: $target" } # The default source: a clone at $DSH_HOME/superpowers pinned to $REF. # Returns 0 when the clone is usable at the ref, 1 when a fresh clone failed # (the caller may fall back to legacy), 2 when pre-existing content blocks the # pin (hard fail: the directory is never touched). ensure_pinned_clone() { local dir="$1" ref="$2" if [ -e "$dir" ] && [ ! -d "$dir" ]; then echo "FAIL: $dir exists and is not a directory; the pinned clone needs it" >&2 return 2 fi if [ -d "$dir" ] && [ -n "$(ls -A "$dir" 2>/dev/null)" ]; then if [ ! -d "$dir/.git" ] || ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "FAIL: $dir has no usable .git: not a pinned superpowers clone" >&2 echo "FAIL: move it away and run: $CLONE_HINT" >&2 return 2 fi # A foreign git repository must never be mutated by fetch/checkout: a real # superpowers clone always carries skills/using-superpowers/SKILL.md (I-1). # The marker also holds for the moved-tag flow — skills are present in # every pinned clone. if [ ! -f "$dir/skills/using-superpowers/SKILL.md" ]; then echo "FAIL: $dir is not a superpowers clone (no skills/using-superpowers/SKILL.md); left untouched" >&2 echo "FAIL: move it away or use --superpowers-from " >&2 return 2 fi # Online: refresh the pin. Fetching from $DSH_SUPERPOWERS_REPO (not from # the clone's origin) makes a repo change take effect on the next run # (T3.3); the force refspec updates the LOCAL tag as well as FETCH_HEAD, so # a later offline run still sees HEAD on the ref (T3.5), and checking out # FETCH_HEAD handles a moved tag, which `git checkout ` (resolving the # stale local ref) would miss. The plain "$ref" fallback covers non-tag # refs (e.g. a branch pin). if git -C "$dir" fetch -q --depth 1 "$DSH_SUPERPOWERS_REPO" "+refs/tags/$ref:refs/tags/$ref" 2>/dev/null || git -C "$dir" fetch -q --depth 1 "$DSH_SUPERPOWERS_REPO" "$ref" 2>/dev/null; then if ! git -C "$dir" checkout -q FETCH_HEAD; then echo "FAIL: checkout of the fetched ref failed in $dir" >&2 return 2 fi echo "ok: superpowers clone at $ref ($dir)" return 0 fi # Offline: reuse only when HEAD is exactly at the pinned ref. if git -C "$dir" rev-parse --verify -q "$ref^{commit}" >/dev/null 2>&1 && [ "$(git -C "$dir" rev-parse HEAD)" = "$(git -C "$dir" rev-parse "$ref^{commit}")" ]; then echo "WARN: offline; reusing the existing clone at $ref ($dir)" >&2 return 0 fi echo "FAIL: cannot update $dir to $ref (offline?) and it is not on that ref" >&2 echo "FAIL: $CLONE_HINT" >&2 return 2 fi # Fresh (absent or empty): clone pinned; a failed clone leaves nothing (RF5). if ! git clone --depth 1 --branch "$ref" "$DSH_SUPERPOWERS_REPO" "$dir"; then echo "FAIL: $CLONE_HINT failed" >&2 rm -rf "$dir" return 1 fi echo "ok: cloned superpowers at $ref into $dir" return 0 } # Sync + verify the checkout into $3 (staging out). Returns 1 (nothing # copied) on any problem; the synced tree is the caller's copy source. sync_skills() { local checkout="$1" ref="$2" out="$3" verified="" verified_n="" verify_rc=0 if ! node "$ROOT/dsh/sync/sync-from-upstream.mjs" "$checkout" --out "$out" --ref "$ref"; then echo "FAIL: superpowers sync failed ($checkout)" >&2 return 1 fi verified="$(node "$ROOT/dsh/sync/verify-skills.mjs" "$out" 2>&1)" || verify_rc=$? printf '%s\n' "$verified" if [ "$verify_rc" -ne 0 ]; then echo "FAIL: superpowers verify failed; nothing copied" >&2 return 1 fi # T2.5: a verified tree with zero skills (exit 0) must never be installed. verified_n="$(printf '%s\n' "$verified" | sed -n 's/.*verified \([0-9][0-9]*\) skills.*/\1/p' | tail -n 1)" case "$verified_n" in ''|*[!0-9]*) echo "FAIL: verify reported no usable skill count; nothing copied" >&2; return 1 ;; esac if [ "$verified_n" -eq 0 ]; then echo "FAIL: verify reported 0 skills; a tree without skills is never installed" >&2 return 1 fi return 0 } # Copy every /SKILL.md under $1 into $DSH_HOME/skills (rm -rf dest, cp -R). # The optional label names where the skills came from in the report line. # $DSH_HOME/.superpowers-manifest remembers the installed superpowers set (one # name per line, sorted): a skill present in the previous manifest but missing # from the new set is removed from $DSH_HOME/skills. Local skills (role, sdd) # are installed by install_local_skills after this and never enter the manifest. copy_skills_from() { local src="$1" label="${2:-$1}" count=0 dir name dest line local manifest="$DSH_HOME/.superpowers-manifest" local old=() new=() if [ -f "$manifest" ]; then while IFS= read -r line; do [ -n "$line" ] && old+=("$line"); done < "$manifest" fi for dir in "$src"/*; do [ -f "$dir/SKILL.md" ] || continue name="$(basename "$dir")" new+=("$name") dest="$DSH_HOME/skills/$name" rm -rf "$dest" cp -R "$dir" "$dest" count=$((count + 1)) done local keep found for name in ${old[@]+"${old[@]}"}; do found=0 for keep in ${new[@]+"${new[@]}"}; do if [ "$keep" = "$name" ]; then found=1 break fi done if [ "$found" -eq 0 ]; then rm -rf "$DSH_HOME/skills/$name" echo "removed $DSH_HOME/skills/$name (no longer in the superpowers source)" fi done printf '%s\n' ${new[@]+"${new[@]}"} | sort -u > "$manifest" echo "copied $count superpowers skills into $DSH_HOME/skills (source: $label)" } # O-2: an explicit DSH_HOME that already exists while $HOME is another tree is # the mixed case: the install writes into the existing DSH_HOME instead of the # isolated one. Warn once per run and continue — the target is valid, only the # HOME/DSH_HOME pair is not isolated together. warn_mixed_dsh_home() { if [ "$DSH_HOME_FROM_ENV" != 1 ]; then return 0; fi if [ "$DSH_HOME" = "$HOME/.dsh" ]; then return 0; fi if [ ! -d "$DSH_HOME" ]; then return 0; fi if [ "${DSH_HOME_WARNED:-0}" = 1 ]; then return 0; fi DSH_HOME_WARNED=1 echo "WARN: DSH_HOME=$DSH_HOME points to an existing directory with a non-standard HOME; both HOME and DSH_HOME must be isolated together" >&2 } install_skills() { warn_mixed_dsh_home mkdir -p "$DSH_HOME/skills" # Explicit source: sync it directly, no network, no pin enforcement. local explicit_dir="" if [ -n "$SUPERPOWERS_FROM" ]; then explicit_dir="$SUPERPOWERS_FROM" elif [ -n "${DSH_SUPERPOWERS_SOURCE:-}" ]; then explicit_dir="$DSH_SUPERPOWERS_SOURCE" fi if [ -n "$explicit_dir" ]; then local skills_dir="" checkout="" skills_dir="$(normalize_superpowers_dir "$explicit_dir")" || { report_invalid_explicit_source return 1 } checkout="$(checkout_root "$skills_dir")" || { report_invalid_explicit_source return 1 } # T3.1: the staging tree lives beside the clone dir, never inside it. if ! sync_skills "$checkout" "$(source_ref "$checkout")" "$EXPLICIT_OUT"; then return 1 fi copy_skills_from "$EXPLICIT_OUT" "$checkout" return 0 fi # Default source: the pinned clone at $DSH_HOME/superpowers. local ensure_rc=0 ensure_pinned_clone "$DSH_HOME/superpowers" "$DSH_SUPERPOWERS_REF" || ensure_rc=$? if [ "$ensure_rc" -eq 0 ]; then if ! sync_skills "$DSH_HOME/superpowers" "$DSH_SUPERPOWERS_REF" "$PINNED_OUT"; then return 1 fi copy_skills_from "$PINNED_OUT" "$DSH_HOME/superpowers" return 0 fi if [ "$ensure_rc" -ge 2 ]; then return 1 fi # A fresh clone failed (offline): legacy sources stay raw with WARN (D-4). local legacy="" legacy_skills="" legacy="$(ls -d "$HOME"/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills 2>/dev/null | sort -V | tail -1 || true)" if [ -n "$legacy" ] && legacy_skills="$(normalize_superpowers_dir "$legacy")"; then : elif legacy_skills="$(normalize_superpowers_dir "$HOME/.claude/skills")"; then : else echo "FAIL: no superpowers source: $CLONE_HINT failed and no legacy source found" >&2 echo "FAIL: clone it once: $CLONE_HINT" >&2 echo "FAIL: or pass --superpowers-from ; local skills (role, sdd) are installed by this run" >&2 return 1 fi echo "WARN: legacy superpowers source $legacy_skills; prefer --superpowers-from or $DSH_HOME/superpowers" >&2 copy_skills_from "$legacy_skills" return 0 } install_local_skills() { local src="$ROOT/dsh/skills" [ -d "$src" ] || return 0 mkdir -p "$DSH_HOME/skills" local count=0 local dir name dest for dir in "$src"/*; do [ -f "$dir/SKILL.md" ] || continue name="$(basename "$dir")" dest="$DSH_HOME/skills/$name" rm -rf "$dest" cp -R "$dir" "$dest" count=$((count + 1)) done echo "copied $count local skills into $DSH_HOME/skills" } install_roles() { warn_mixed_dsh_home mkdir -p "$DSH_HOME/roles" local src="$ROOT/roles" local count=0 local file name for file in "$src"/*.md; do [ -f "$file" ] || continue name="$(basename "$file")" cp "$file" "$DSH_HOME/roles/$name" count=$((count + 1)) done if [ "$count" -eq 0 ]; then echo "WARN: no role files found in $src" >&2 return 0 fi echo "installed $count roles into $DSH_HOME/roles" } # The DSH SDD note belongs to the installed role, not to the shared roles/ layer: # in Claude Code the note does not apply, so it is written here, after the copy, # and is idempotent (the source file is overwritten on every --roles run). install_role_dsh_notes() { local role="$DSH_HOME/roles/dev.md" [ -f "$role" ] || return 0 python3 - "$role" <<'PY' import sys from pathlib import Path path = Path(sys.argv[1]) text = path.read_text(encoding="utf-8") marker = "В DSH-сессии исполнение плана" if marker in text: print("ok: role dsh notes already present") raise SystemExit(0) note = ( "- В DSH-сессии исполнение плана идёт по skill `sdd`, а не " "`subagent-driven-development`: асинхронный `subagent` (бриф и отчёт файлами, " "ожидание завершением хода и уведомлением, ledger " "`.superpowers/sdd//progress.md` с `## Findings`, fix-loop до 5 раундов). " "Режим контроллера — по числу параллельных планов: один план — главная сессия, " "несколько — фоновый контроллер (нужен `maxDepth: 2` из " "`dsh/subagent-models.yml`). В Claude Code строка не действует: там исходный " "`subagent-driven-development`. " "Бандл гейтит цикл: builder-check обязателен до передачи дизайна владельцу " "(иначе `/approve design` откажет); план пишется только после `/approve design`; " "sdd стартует только после `/approve plan` (гейты A/B/C, " "`BLOCKED [approval-gates]`). Пропуск — только явное решение владельца." ) anchor = "- Режим по умолчанию: subagent-driven-development." lines = text.splitlines(keepends=True) for i, line in enumerate(lines): if line.startswith(anchor): lines.insert(i + 1, note + "\n") break else: lines.append((("" if text.endswith("\n") else "\n") + note + "\n")) path.write_text("".join(lines), encoding="utf-8") print("ok: role dsh notes inserted") PY } install_openspec() { local dir="${1:-$PWD}" if ! command -v openspec >/dev/null 2>&1; then echo "FAIL: openspec CLI not found; install it first" >&2 return 1 fi if [ ! -d "$dir/openspec" ]; then echo "FAIL: $dir has no openspec/ root; run: openspec init --tools agents" >&2 return 1 fi if ! ( cd "$dir" && openspec init --tools agents ); then echo "FAIL: openspec init --tools agents failed in $dir" >&2 return 1 fi echo "ok: openspec agents adapter in $dir" } print_bundle_step() { cat < install a bundle from the directory above; - or ask the agent to call plugin_manager install_bundle with target "$BUNDLE". Do not edit \$DSH_HOME/profiles/desktop/package.json or cordis.patch.yml by hand. Optional: add CodeGraph as an MCP server from dsh/mcp.codegraph.yml. EOF } status=0 if [ "$DO_CHECK" = 1 ]; then check || status=1 fi if [ "$DO_GLOBAL" = 1 ]; then install_global fi if [ "$DO_SKILLS" = 1 ]; then # Local skills do not depend on superpowers: install them even when the # superpowers source is missing (the FAIL is reported by the exit status). install_skills || status=1 install_local_skills fi if [ "$DO_ROLES" = 1 ]; then install_roles install_role_dsh_notes fi # A failed --openspec step must not abort the other steps: the explicit return 1 # becomes the script status here, like check() above. if [ "$DO_OPENSPEC" = 1 ]; then install_openspec "$PWD" || status=1 fi if [ "$DO_GLOBAL" = 1 ] || [ "$DO_SKILLS" = 1 ] || [ "$DO_ROLES" = 1 ]; then print_bundle_step fi exit "$status"