Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
shellcheck -s sh install.sh scripts/check_install.sh
shellcheck -s sh install.sh scripts/check_install.sh scripts/check_piped_install.sh
- name: Python syntax
run: python3 -m py_compile scripts/check_skills.py scripts/apply_addons.py bin/hstack-bench bin/hstack-test bin/hstack-pr bin/hstack-visdiff bin/hstack-scan bin/hstack-decisions
- name: Skills
run: python3 scripts/check_skills.py
- name: Installer
run: sh scripts/check_install.sh
- name: Site
run: |
cd site
npm ci
npm run build
node scripts/check-no-html.mjs
node scripts/check-pages.mjs
node scripts/check-vercel.mjs
cd ..
sh scripts/check_piped_install.sh
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Twelve skills for Hermes Agent. Frame the job, shape the change, prove it, and o
curl -fsSL https://hstack.sh/install | sh
```

From a checkout of this repo, `sh install.sh` installs that checkout. A second run replaces the HStack skills and leaves every other skill alone. Pass `--dry-run` to print the plan and write nothing.
The curl form downloads `install.sh` and the skill archive from hstack.sh, checks the sha256, and extracts into the Hermes skills directory. It does not clone this repository. `HSTACK_BASE_URL` points the download at another host, such as a `*.vercel.app` URL before DNS is in place. `HSTACK_VERSION=0.1.0` pins `hstack-v0.1.0.tar.gz` instead of the latest alias. The archive is one directory containing the skills, `scripts/apply_addons.py`, and the `bin/` helpers, so `--with` works from the curl form too.

Skills land in `~/.hermes/skills/hstack/`. Set `HERMES_HOME` if Hermes lives somewhere else. `install.sh` is for macOS and Linux. On Windows, `install.ps1` takes the same flags. WSL can run `install.sh`.
From a checkout of this repo, `sh install.sh` installs the skills beside the script. A second run replaces the HStack skills and leaves every other skill alone. Pass `--dry-run` to print the plan and write nothing.

Skills land in `~/.hermes/skills/hstack/`. Set `HERMES_HOME` if Hermes lives somewhere else. `install.sh` is for macOS and Linux. On Windows, `https://hstack.sh/install.ps1` takes the same flags and downloads the same archive. WSL can run `install.sh`.

## Why this instead of GStack

Expand All @@ -35,10 +37,10 @@ GStack's Hermes host puts every skill in the always-on index. HStack puts one ro
| | GStack on Hermes | HStack |
|---|---:|---:|
| Idle index | 1,182 | 204 |
| Planning load | 62,514 of SKILL.md, plus a ~5,600 preamble | 2,500 |
| All SKILL.md bodies | 420,401 | 8,550 |
| Planning load | 62,514 of SKILL.md, plus a ~5,600 preamble | 965 |
| All SKILL.md bodies | 420,401 | 3,820 |

The planning path is `hstack`, `reframe`, and `shape`. 2,500 against 62,514 is about 25× smaller. The idle index is about 6× smaller. HStack's body figures are the v0.1 ceilings. 204 is the index cost of these descriptions, including the `hstack` category header.
The planning path is `hstack`, `reframe`, and `shape`. 965 against 62,514 is under 2% of that context. The idle index is about 6× smaller. These HStack figures are measured from the files in this repo. 204 is the index cost of these descriptions, including the `hstack` category header. The v0.1 ceilings in PLAN.md are 2,500 and 8,550.

## Skills

Expand Down
72 changes: 64 additions & 8 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Usage: powershell -File install.ps1 [--dry-run] [--uninstall] [--with name,name]
Installs into the Hermes home, at skills\hstack. The usual home is .hermes in your profile directory.
The default install is skills only. --with adds an optional addon:
browser, github, docs, search, perf, test, security, trail.
HSTACK_REF selects the git branch when the script downloads (default: main).
The download form fetches the archive from hstack.sh.
HSTACK_BASE_URL overrides that host. HSTACK_VERSION pins a release.
Running this file from a git checkout installs that checkout.
'@
}
Expand Down Expand Up @@ -198,13 +199,23 @@ if ((Test-Path -LiteralPath $Dest) -and -not (Test-Path -LiteralPath $Marker)) {
}
}

$Url = ''
$Sum = ''
if (-not $Mode) {
$Mode = 'remote'
$Ref = if ($env:HSTACK_REF) { $env:HSTACK_REF } else { 'main' }
if ($Ref -notmatch '^[A-Za-z0-9._-]+$' -or $Ref.StartsWith('-') -or $Ref.Contains('..')) {
Die 'refusing HSTACK_REF'
$Base = if ($env:HSTACK_BASE_URL) { $env:HSTACK_BASE_URL.TrimEnd('/') } else { 'https://hstack.sh' }
if ($Base -notmatch '^https?://' -or $Base -match '\s|<|>|\.\.') {
Die 'refusing HSTACK_BASE_URL'
}
$Url = "https://github.com/Belweave/hstack/archive/refs/heads/$Ref.tar.gz"
$Name = 'hstack.tar.gz'
if ($env:HSTACK_VERSION) {
if ($env:HSTACK_VERSION -notmatch '^[0-9.]+$' -or $env:HSTACK_VERSION.Contains('..')) {
Die 'refusing HSTACK_VERSION'
}
$Name = "hstack-v$($env:HSTACK_VERSION).tar.gz"
}
$Url = "$Base/dl/$Name"
$Sum = "$Base/dl/$Name.sha256"
}

if ($Dry) {
Expand All @@ -213,6 +224,7 @@ if ($Dry) {
Say "dry-run: source tree $Src"
} else {
Say "dry-run: would download $Url"
Say "dry-run: would verify $Sum"
}
Invoke-Addons 'install'
exit 0
Expand All @@ -221,16 +233,47 @@ if ($Dry) {
if ($Mode -eq 'remote') {
$Work = Join-Path ([System.IO.Path]::GetTempPath()) ("hstack-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $Work | Out-Null
$RemoteOk = $false
try {
$archive = Join-Path $Work 'hstack.tar.gz'
Invoke-WebRequest -Uri $Url -OutFile $archive
$sumfile = Join-Path $Work 'hstack.tar.gz.sha256'
Invoke-WebRequest -Uri $Url -OutFile $archive -UseBasicParsing
Invoke-WebRequest -Uri $Sum -OutFile $sumfile -UseBasicParsing
if ((Get-Item -LiteralPath $archive).Length -lt 1 -or (Get-Item -LiteralPath $sumfile).Length -lt 1) {
Die 'download was empty'
}
$stream = [System.IO.File]::OpenRead($archive)
try {
$b0 = $stream.ReadByte()
$b1 = $stream.ReadByte()
} finally {
$stream.Dispose()
}
if ($b0 -ne 0x1f -or $b1 -ne 0x8b) {
Die 'download was not a gzip archive'
}
$expected = ((Get-Content -LiteralPath $sumfile -TotalCount 1) -split '\s+', 2)[0]
if ($expected -notmatch '^[0-9A-Fa-f]{64}$') {
Die 'checksum was not a sha256 digest'
}
$actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash
if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) {
Die 'checksum mismatch'
}
$members = & tar -tzf $archive
if ($LASTEXITCODE -ne 0) { Die 'could not read archive' }
foreach ($member in $members) {
if (-not $member -or $member.StartsWith('/') -or $member.StartsWith('\') -or $member.Contains('..')) {
Die 'archive member is absolute or contains ..'
}
}
$listing = & tar -tvzf $archive
if ($LASTEXITCODE -ne 0) { Die 'could not list archive' }
foreach ($line in $listing) {
if ($line.StartsWith('l') -or $line.StartsWith('h')) {
Die 'archive contains a link; refusing to extract it'
}
}
& tar -xzf $archive -C $Work
if ($LASTEXITCODE -ne 0) { Die 'could not extract archive' }
$tops = @(Get-ChildItem -LiteralPath $Work -Directory)
Expand All @@ -240,8 +283,9 @@ if ($Mode -eq 'remote') {
if (-not (Test-Path -LiteralPath (Join-Path $Src 'hstack\SKILL.md'))) {
Die 'archive is missing skills/hstack/hstack/SKILL.md'
}
$RemoteOk = $true
} finally {
if ($Mode -eq 'remote' -and (Test-Path -LiteralPath $Work) -and -not $Src) {
if (-not $RemoteOk -and (Test-Path -LiteralPath $Work)) {
Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue
}
}
Expand All @@ -255,17 +299,29 @@ if ($links.Count -gt 0) {
Die 'refusing to copy a link or special file from the skill tree'
}

$ver = '0.1.0'
$versionFile = if ($AddonRoot) { Join-Path $AddonRoot 'VERSION' } else { $null }
if ($versionFile -and (Test-Path -LiteralPath $versionFile)) {
$digits = ([System.IO.File]::ReadAllText($versionFile) -replace '[^0-9.]', '')
if ($digits) { $ver = $digits }
}
$Parent = Split-Path -Parent $Dest
if (-not (Test-Path -LiteralPath $Parent)) {
New-Item -ItemType Directory -Path $Parent | Out-Null
}
$Stage = Join-Path $Parent ('.hstack-stage.' + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $Stage | Out-Null
Copy-Item -Path (Join-Path $Src '*') -Destination $Stage -Recurse -Force
$stagedVersion = Join-Path $Stage 'VERSION'
if (Test-Path -LiteralPath $stagedVersion) {
$digits = ([System.IO.File]::ReadAllText($stagedVersion) -replace '[^0-9.]', '')
if ($digits) { $ver = $digits }
Remove-Item -LiteralPath $stagedVersion -Force
}
if (-not (Test-Path -LiteralPath (Join-Path $Stage 'hstack\SKILL.md'))) {
Die 'install tree is missing hstack/SKILL.md'
}
Set-Content -LiteralPath (Join-Path $Stage '.hstack-managed') -Value "managed-by: hstack install.sh`nversion: 0.1.0`n" -NoNewline -Encoding ascii
Set-Content -LiteralPath (Join-Path $Stage '.hstack-managed') -Value "managed-by: hstack install.sh`nversion: $ver`n" -NoNewline -Encoding ascii

if (Test-Path -LiteralPath $Dest) {
$Prev = Join-Path $Parent ('.hstack-prev.' + $PID)
Expand Down
91 changes: 78 additions & 13 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# Install HStack into Hermes Agent.
# curl -fsSL https://hstack.sh/install | sh
# curl -fsSL https://hstack.sh/install | sh -s -- --uninstall
# sh install.sh [--dry-run] [--uninstall]
# sh install.sh [--dry-run] [--uninstall] [--with name,name]
# Skills go to ${HERMES_HOME:-$HOME/.hermes}/skills/hstack.
# A second run replaces an install this script created. It will not touch
# any other skill, and it will not delete a directory it did not mark.
# The curl form downloads the archive from hstack.sh. The repository is private,
# so this script does not fetch from a git host. The archive also carries the
# addon helpers under bin/ and scripts/apply_addons.py.

set -eu

Expand All @@ -25,7 +28,8 @@ usage() {
'Installs into the Hermes home, at skills/hstack. The usual home is .hermes in your home directory.' \
'The default install is skills only. --with adds an optional addon:' \
'browser, github, docs, search, perf, test, security, trail.' \
'HSTACK_REF selects the git branch when the script downloads (default: main).' \
'The curl form downloads the archive from hstack.sh.' \
'HSTACK_BASE_URL overrides that host. HSTACK_VERSION pins a release.' \
'Running this file from a git checkout installs that checkout.'
}

Expand Down Expand Up @@ -251,6 +255,7 @@ fi
MODE=
SRC=
URL=
SUM=
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/skills/hstack/hstack/SKILL.md" ]; then
MODE=local
SRC="$SCRIPT_DIR/skills/hstack"
Expand All @@ -259,14 +264,26 @@ fi

if [ -z "$MODE" ]; then
MODE=remote
REF=${HSTACK_REF:-main}
case "$REF" in
''|-*|*..*|*[[:space:]]*|*'/'*) die "refusing HSTACK_REF" ;;
BASE=${HSTACK_BASE_URL:-https://hstack.sh}
case "$BASE" in
*/) BASE=${BASE%/} ;;
esac
case "$REF" in
*[!A-Za-z0-9._-]*) die "refusing HSTACK_REF" ;;
case "$BASE" in
http://*|https://*) ;;
*) die "refusing HSTACK_BASE_URL" ;;
esac
URL="https://github.com/Belweave/hstack/archive/refs/heads/${REF}.tar.gz"
case "$BASE" in
*[[:space:]]*|*'<'*|*'..'*) die "refusing HSTACK_BASE_URL" ;;
esac
NAME=hstack.tar.gz
if [ -n "${HSTACK_VERSION:-}" ]; then
case "$HSTACK_VERSION" in
''|*[!0-9.]*|*..*) die "refusing HSTACK_VERSION" ;;
esac
NAME="hstack-v${HSTACK_VERSION}.tar.gz"
fi
URL="$BASE/dl/$NAME"
SUM="$BASE/dl/${NAME}.sha256"
fi

if [ "$DRY" -eq 1 ]; then
Expand All @@ -275,23 +292,57 @@ if [ "$DRY" -eq 1 ]; then
say "dry-run: source tree $SRC"
else
say "dry-run: would download $URL"
say "dry-run: would verify $SUM"
fi
run_addons install
exit 0
fi

hash_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk 'NR==1 { print $1 }'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk 'NR==1 { print $1 }'
else
die "sha256sum or shasum is required"
fi
}

if [ "$MODE" = remote ]; then
command -v curl >/dev/null 2>&1 || die "curl is required to download HStack"
command -v tar >/dev/null 2>&1 || die "tar is required to download HStack"
WORK=$(mktemp -d)
archive="$WORK/hstack.tar.gz"
sumfile="$WORK/hstack.tar.gz.sha256"
curl -fsSL -o "$archive" "$URL" || die "download failed"
if [ ! -s "$archive" ]; then
curl -fsSL -o "$sumfile" "$SUM" || die "checksum download failed"
if [ ! -s "$archive" ] || [ ! -s "$sumfile" ]; then
die "download was empty"
fi
magic=$(od -An -t x1 -N 2 "$archive" | tr -d ' \n')
case "$magic" in
1f8b) ;;
*) die "download was not a gzip archive" ;;
esac
expected=$(awk 'NR==1 { print $1 }' "$sumfile")
case "$expected" in
*[!0-9A-Fa-f]*|'') die "checksum was not a sha256 digest" ;;
esac
elen=$(printf '%s' "$expected" | wc -c | tr -d ' ')
if [ "$elen" -ne 64 ]; then
die "checksum was not a sha256 digest"
fi
actual=$(hash_file "$archive")
if [ "$actual" != "$expected" ]; then
die "checksum mismatch"
fi
tar -tzf "$archive" > "$WORK/members" || die "could not read archive"
while IFS= read -r member || [ -n "$member" ]; do
case "$member" in
norm=$member
case "$norm" in
./*) norm=${norm#./} ;;
esac
case "$norm" in
''|/*|*..*) die "archive member is absolute or contains .." ;;
esac
done < "$WORK/members"
Expand All @@ -316,26 +367,40 @@ if [ "$MODE" = remote ]; then
SRC="$top/skills/hstack"
ADDON_ROOT=$top
if [ ! -f "$SRC/hstack/SKILL.md" ]; then
die "archive is missing skills/hstack/hstack/SKILL.md"
die "archive is missing hstack/SKILL.md"
fi
fi

if [ ! -f "$SRC/hstack/SKILL.md" ]; then
die "missing skills/hstack/hstack/SKILL.md"
die "missing hstack/SKILL.md"
fi

if find "$SRC" \( -type l -o -type b -o -type c -o -type p -o -type s \) -print | grep -q .; then
die "refusing to copy a link or special file from the skill tree"
fi

ver=0.1.0
if [ -n "$ADDON_ROOT" ] && [ -f "$ADDON_ROOT/VERSION" ]; then
got=$(tr -cd '0-9.' < "$ADDON_ROOT/VERSION")
if [ -n "$got" ]; then
ver=$got
fi
fi
parent=$(dirname "$DEST")
mkdir -p "$parent"
STAGE=$(mktemp -d "${parent}/.hstack-stage.XXXXXX")
cp -R "$SRC/." "$STAGE/"
if [ -f "$STAGE/VERSION" ]; then
got=$(tr -cd '0-9.' < "$STAGE/VERSION")
if [ -n "$got" ]; then
ver=$got
fi
rm -f "$STAGE/VERSION"
fi
if [ ! -f "$STAGE/hstack/SKILL.md" ]; then
die "install tree is missing hstack/SKILL.md"
fi
printf '%s\n' 'managed-by: hstack install.sh' 'version: 0.1.0' > "$STAGE/.hstack-managed"
printf '%s\n' 'managed-by: hstack install.sh' "version: $ver" > "$STAGE/.hstack-managed"

if [ -d "$DEST" ]; then
PREV="${parent}/.hstack-prev.$$"
Expand Down
10 changes: 10 additions & 0 deletions scripts/check_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ fail() {
exit 1
}

if grep -q 'github.com' "$INSTALL"; then
fail "installer must not fetch from GitHub"
fi
if grep -q 'github.com' "$ROOT/install.ps1"; then
fail "install.ps1 must not fetch from GitHub"
fi
if grep -q 'HSTACK_REF' "$INSTALL" "$ROOT/install.ps1"; then
fail "installers still select a git ref"
fi

count_skills() {
_n=0
for _dir in "$1"/skills/hstack/*; do
Expand Down
Loading
Loading