claude-harness/dsh/install.sh
Andrey Epifancev 9ecc3eb793 feat(dsh): --check сообщает OpenSpec CLI и agents-скиллы (#42)
Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
2026-10-24 19:20:42 +04:00

315 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
# bash dsh/install.sh --roles
# bash dsh/install.sh --all
#
# 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"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
DO_CHECK=0
DO_GLOBAL=0
DO_SKILLS=0
DO_ROLES=0
usage() {
cat <<EOF
Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all]
--check verify python3, git, node, hooks, bundle, skills, and roles
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
--skills copy superpowers skills 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
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
for arg in "$@"; do
case "$arg" in
--check) DO_CHECK=1 ;;
--global) DO_GLOBAL=1 ;;
--skills) DO_SKILLS=1 ;;
--roles) DO_ROLES=1 ;;
--all) DO_CHECK=1; DO_GLOBAL=1; DO_SKILLS=1; DO_ROLES=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;;
esac
done
superpowers_skills() {
local dir
dir="$(ls -d "$HOME"/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills 2>/dev/null | sort -V | tail -1 || true)"
if [ -n "$dir" ] && [ -d "$dir" ]; then
printf '%s\n' "$dir"
return 0
fi
return 1
}
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
if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then
echo "ok: dsh skills ($DSH_HOME/skills)"
elif superpowers_skills >/dev/null 2>&1; then
echo "ok: superpowers source ($(superpowers_skills)); run --skills to copy"
else
echo "WARN: superpowers plugin not found; the bootstrap will be skipped" >&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() {
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, then ~/.claude/roles, then $ROOT/roles."
echo "DSH adapter: bootstrap prefers ~/.dsh/skills; the ~/.claude cache is a fallback."
echo "DSH adapter: codegraph MCP tools are named mcp__codegraph__codegraph_explore."
echo
cat "$ROOT/global/CLAUDE.md"
} > "$target"
echo "written: $target"
}
install_skills() {
mkdir -p "$DSH_HOME/skills"
local skills
if ! skills="$(superpowers_skills)"; then
echo "WARN: superpowers skills not found; nothing copied" >&2
return 0
fi
local count=0
local dir name dest
for dir in "$skills"/*; 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 superpowers skills into $DSH_HOME/skills"
}
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() {
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/<plan>/progress.md` с `## Findings`, fix-loop до 5 раундов). "
"Режим контроллера — по числу параллельных планов: один план — главная сессия, "
"несколько — фоновый контроллер (нужен `maxDepth: 2` из "
"`dsh/subagent-models.yml`). В Claude Code строка не действует: там исходный "
"`subagent-driven-development`."
)
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
}
print_bundle_step() {
cat <<EOF
Bundle: $BUNDLE
Install it in the current DSH profile (the Desktop profile is Electron-managed):
- GUI: Plugins -> 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
install_skills
install_local_skills
fi
if [ "$DO_ROLES" = 1 ]; then
install_roles
install_role_dsh_notes
fi
if [ "$DO_GLOBAL" = 1 ] || [ "$DO_SKILLS" = 1 ] || [ "$DO_ROLES" = 1 ]; then
print_bundle_step
fi
exit "$status"