HStack
planning load
965tokens
+ +GStack
planning load
62,514tokens
+diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff674d8..fc209f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 60bf8f8..b4fd1fd 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/install.ps1 b/install.ps1 index 5d54461..0a8dc52 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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. '@ } @@ -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) { @@ -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 @@ -221,9 +233,33 @@ 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) { @@ -231,6 +267,13 @@ if ($Mode -eq 'remote') { 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) @@ -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 } } @@ -255,6 +299,12 @@ 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 @@ -262,10 +312,16 @@ if (-not (Test-Path -LiteralPath $Parent)) { $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) diff --git a/install.sh b/install.sh index 5ba1981..896c766 100755 --- a/install.sh +++ b/install.sh @@ -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 @@ -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.' } @@ -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" @@ -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 @@ -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" @@ -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.$$" diff --git a/scripts/check_install.sh b/scripts/check_install.sh index a340757..8851628 100755 --- a/scripts/check_install.sh +++ b/scripts/check_install.sh @@ -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 diff --git a/scripts/check_piped_install.sh b/scripts/check_piped_install.sh new file mode 100755 index 0000000..b77f504 --- /dev/null +++ b/scripts/check_piped_install.sh @@ -0,0 +1,168 @@ +#!/bin/sh +# Piped install against the local site build, with GNU tar and with BSD tar plus shasum. +set -eu + +ROOT=$(CDPATH='' cd "$(dirname "$0")/.." && pwd -P) +SITE=$ROOT/site +TMP=$(mktemp -d) +PORT=${PORT:-4179} +PID= + +fail() { + printf 'check_piped: %s\n' "$1" >&2 + exit 1 +} + +cleanup() { + if [ -n "$PID" ]; then + kill "$PID" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap cleanup EXIT + +if [ ! -f "$SITE/dist/install.sh" ] || [ ! -f "$SITE/dist/dl/hstack.tar.gz" ]; then + fail "site/dist is missing; run npm run build in site/" +fi + +PORT=$PORT node "$SITE/scripts/preview.mjs" >"$TMP/preview.log" 2>&1 & +PID=$! +i=0 +while ! curl -fsS "http://127.0.0.1:$PORT/install" >/dev/null 2>&1; do + i=$((i + 1)) + if [ "$i" -gt 50 ]; then + fail "preview did not start" + fi + sleep 0.1 +done + +count_skills() { + _n=0 + for _dir in "$1"/skills/hstack/*; do + if [ -f "$_dir/SKILL.md" ]; then + _n=$((_n + 1)) + fi + done + printf '%s\n' "$_n" +} + +install_with() { + label=$1 + path_value=$2 + home=$TMP/$label + mkdir -p "$home" + if [ -n "$path_value" ]; then + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' + else + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' + fi + test -f "$home/hermes/skills/hstack/.hstack-managed" || fail "$label missing marker" + test -f "$home/hermes/skills/hstack/hstack/SKILL.md" || fail "$label missing router" + test -f "$home/hermes/skills/hstack/hstack/references/frame.md" || fail "$label missing playbook" + test "$(count_skills "$home/hermes")" -eq 12 || fail "$label expected 12 skills" + test ! -e "$home/hermes/skills/hstack/VERSION" || fail "$label left VERSION in the skill tree" + grep -q 'version: 0.1.0' "$home/hermes/skills/hstack/.hstack-managed" || fail "$label marker version" + diff -q "$ROOT/skills/hstack/reframe/SKILL.md" "$home/hermes/skills/hstack/reframe/SKILL.md" >/dev/null \ + || fail "$label skill bytes differ" + if [ -n "$path_value" ]; then + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --dry-run' >"$TMP/$label.dry" + PATH="$path_value" HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --uninstall' + else + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --dry-run' >"$TMP/$label.dry" + HOME="$home" HERMES_HOME="$home/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --uninstall' + fi + grep -q "http://127.0.0.1:$PORT/dl/hstack.tar.gz" "$TMP/$label.dry" || fail "$label dry-run omitted the archive URL" + test ! -d "$home/hermes/skills/hstack" || fail "$label uninstall left the tree" +} + +install_with linux "" + +WITH=$TMP/with +mkdir -p "$WITH" +HOME="$WITH" HERMES_HOME="$WITH/hermes" HSTACK_BASE_URL="http://127.0.0.1:$PORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh -s -- --with browser' +test -x "$WITH/hermes/hstack/bin/hstack-visdiff" || fail "piped --with browser missing visdiff" +test -f "$WITH/hermes/hstack/apply_addons.py" || fail "piped --with browser missing apply_addons.py" +grep -q 'hstack_playwright' "$WITH/hermes/config.yaml" || fail "piped --with browser missing playwright" +test -f "$WITH/hermes/skills/hstack/hstack/SKILL.md" || fail "piped --with browser missing skills" +test ! -e "$WITH/hermes/skills/hstack/bin" || fail "helpers landed in the skill tree" +test ! -e "$WITH/hermes/skills/hstack/VERSION" || fail "piped --with browser left VERSION in the skill tree" + +MAC=$TMP/macbin +mkdir -p "$MAC" +link() { + command -v "$1" >/dev/null 2>&1 || fail "missing $1" + ln -sf "$(command -v "$1")" "$MAC/$2" +} +link curl curl +link shasum shasum +link awk awk +link mktemp mktemp +link find find +link grep grep +link od od +link tr tr +link wc wc +link cp cp +link mv mv +link rm rm +link mkdir mkdir +link dirname dirname +link basename basename +link pwd pwd +ln -sf "$(command -v bsdtar)" "$MAC/tar" +ln -sf /bin/sh "$MAC/sh" +if [ -x "$MAC/sha256sum" ]; then + fail "macOS-like PATH still has sha256sum" +fi +install_with macos "$MAC:/bin" + +HOME=$TMP/pin HERMES_HOME=$TMP/pin/hermes HSTACK_BASE_URL="http://127.0.0.1:$PORT" HSTACK_VERSION=0.1.0 \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' +test -f "$TMP/pin/hermes/skills/hstack/shape/SKILL.md" || fail "version pin did not install" + +mkdir -p "$TMP/bad/dl" +cp "$SITE/dist/dl/hstack.tar.gz" "$TMP/bad/dl/hstack.tar.gz" +printf '%s\n' '0000000000000000000000000000000000000000000000000000000000000000 hstack.tar.gz' > "$TMP/bad/dl/hstack.tar.gz.sha256" +cp "$SITE/dist/install.sh" "$TMP/bad/install.sh" +BADPORT=$((PORT + 1)) +python3 - "$TMP/bad" "$BADPORT" << 'PY' & +import http.server, socketserver, sys +root, port = sys.argv[1], int(sys.argv[2]) +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=root, **kwargs) + def log_message(self, fmt, *args): + return + def do_GET(self): + path = self.path.split("?", 1)[0] + if path in ("/install", "/install/"): + self.path = "/install.sh" + return super().do_GET() +with socketserver.TCPServer(("127.0.0.1", port), Handler) as httpd: + httpd.serve_forever() +PY +BPID=$! +trap 'kill "$PID" "$BPID" >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT +j=0 +while ! curl -fsS "http://127.0.0.1:$BADPORT/install" >/dev/null 2>&1; do + j=$((j + 1)) + if [ "$j" -gt 50 ]; then + fail "bad checksum server did not start" + fi + sleep 0.1 +done +if HOME=$TMP/badhome HERMES_HOME=$TMP/badhome/hermes HSTACK_BASE_URL="http://127.0.0.1:$BADPORT" \ + /bin/sh -c 'curl -fsSL "$HSTACK_BASE_URL/install" | sh' >"$TMP/bad.out" 2>"$TMP/bad.err"; then + fail "bad checksum should fail" +fi +grep -q 'checksum mismatch' "$TMP/bad.err" || fail "bad checksum was not reported" +test ! -d "$TMP/badhome/hermes/skills/hstack" || fail "bad checksum wrote a skill tree" + +printf '%s\n' 'check_piped: linux, addons, macos-like, version pin, and checksum mismatch ok' diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..0c199f7 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.vercel +src/data.gen.js diff --git a/site/DEPLOY.md b/site/DEPLOY.md new file mode 100644 index 0000000..06a52a5 --- /dev/null +++ b/site/DEPLOY.md @@ -0,0 +1,51 @@ +# Deploy + +The primary host is Vercel, project `hstack`, on Preetham's Pro plan. Cloudflare Pages on the belweave account, Free plan, is the fallback. Do not deploy from this note; the project owner runs the deploy. + +## Vercel + +Connect the GitHub repository with the root directory set to `site`, or deploy from a checkout with `vercel deploy --prod --cwd site`. `site/vercel.json` pins the build. + +| Setting | Value | +|---|---| +| Project | `hstack` | +| Account | Preetham's Pro plan | +| Root directory | `site` | +| Framework preset | Other (`framework` is `null`) | +| Build command | `npm run build` | +| Output directory | `dist` | + +`cleanUrls` is on. `/install` and `/install/` rewrite to `install.sh` and are served as `text/plain; charset=utf-8` with `Cache-Control: public, max-age=60`. Any other path under `/install/` rewrites to a plain-text 404. `.tar.gz` is `application/gzip`. `.sha256` is `text/plain`. `www.hstack.sh` redirects to `https://hstack.sh` with status 308. A request that still arrives as HTTP on `hstack.sh` redirects to HTTPS with status 308. + +Turn Deployment Protection off for production. A protected deploy returns an HTML login page, and `curl | sh` would not receive the installer. + +## hstack.sh at Porkbun + +Leave the Porkbun nameservers alone. The registrar stays Porkbun. Vercel does not need to host DNS. + +1. In the Vercel project `hstack`, open Settings, then Domains. Add `hstack.sh` and `www.hstack.sh`. +2. The apex card shows an A record. For most projects the value is `76.76.21.21`. Use the address on the card if it differs. +3. At Porkbun, edit DNS for `hstack.sh`. Remove any existing apex A or AAAA record. Add one A record: host `@`, value from the apex card (usually `76.76.21.21`). Do not add an AAAA record. +4. Add a CNAME for `www`. The value is the CNAME target on the `www` domain card. For many projects that target is `cname.vercel-dns.com`. A newer project may show a specific name such as `cname.vercel-dns-0.com`. Use the card. +5. Wait until both cards say the configuration is valid. Vercel issues the certificates. + +`www.hstack.sh` answers 308 to the apex. Before DNS is in place, the deployment hostname on the project card (a `*.vercel.app` host) is what `HSTACK_BASE_URL` should point at. + +## What the deploy serves + +- `https://hstack.sh/install` and `https://hstack.sh/install/` are `install.sh`, `Content-Type: text/plain; charset=utf-8`. +- `https://hstack.sh/install.ps1` is the Windows installer, also `text/plain`. +- Any other path under `/install/` is a plain-text 404 (`not found`), not an HTML page. +- `curl -fsSL hstack.sh/install` follows HTTP to HTTPS and receives the script. `curl -fsSL www.hstack.sh/install` follows the 308 to the apex and receives the same script. +- `https://hstack.sh/dl/hstack.tar.gz` is the latest archive. `hstack.tar.gz.sha256` is its digest. +- `https://hstack.sh/dl/hstack-v0.1.0.tar.gz` is that same release by version. The installer checks the digest before it extracts. + +The archive has one top-level directory. Inside it are `skills/hstack/`, `scripts/apply_addons.py`, `bin/` (the addon helpers), and `VERSION`. + +## Cloudflare Pages fallback + +Use this only if the Vercel project is not the host. Same build: root directory `site`, build command `npm run build`, output directory `dist`, no framework preset. Account belweave, Free plan. + +`site/_headers` and `site/_redirects` are copied into `dist/`. Pages reads them from the output and does not serve them as assets. `site/functions/` stays in the project root. Pages compiles it from there. `functions/install/[...path].js` returns the plain-text 404, because Pages does not honor a 404 status in `_redirects`. `functions/_middleware.js` redirects HTTP to HTTPS when the request is still http. Leave Always Use HTTPS on. + +Pages will not activate the apex while DNS stays at Porkbun. The fallback path is to add the `hstack.sh` zone on the belweave account and set Porkbun's nameservers to the Cloudflare pair. That replaces the Vercel A record and the `www` CNAME. Do not point the apex at both hosts. A project named `hstack` is also reachable at `hstack.pages.dev` before that cutover. diff --git a/site/_headers b/site/_headers new file mode 100644 index 0000000..6f297a1 --- /dev/null +++ b/site/_headers @@ -0,0 +1,39 @@ +/install/* + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + +/install + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install/ + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install.sh + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/install.ps1 + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=60 + +/dl/hstack.tar.gz + Content-Type: application/gzip + Cache-Control: public, max-age=300 + +/dl/hstack.tar.gz.sha256 + Content-Type: text/plain; charset=utf-8 + Cache-Control: public, max-age=300 + +/dl/hstack-v0.1.0.tar.gz + Content-Type: application/gzip + Cache-Control: public, max-age=31536000, immutable + +/dl/hstack-v0.1.0.tar.gz.sha256 + Content-Type: text/plain; charset=utf-8 + Cache-Control: public, max-age=31536000, immutable diff --git a/site/_redirects b/site/_redirects new file mode 100644 index 0000000..7fd46ce --- /dev/null +++ b/site/_redirects @@ -0,0 +1,2 @@ +/install /install.sh 200 +/install/ /install.sh 200 diff --git a/site/fonts/inter-bold.woff2 b/site/fonts/inter-bold.woff2 new file mode 100644 index 0000000..e69189c Binary files /dev/null and b/site/fonts/inter-bold.woff2 differ diff --git a/site/fonts/inter-regular.woff2 b/site/fonts/inter-regular.woff2 new file mode 100644 index 0000000..9348abc Binary files /dev/null and b/site/fonts/inter-regular.woff2 differ diff --git a/site/functions/_middleware.js b/site/functions/_middleware.js new file mode 100644 index 0000000..be2a98d --- /dev/null +++ b/site/functions/_middleware.js @@ -0,0 +1,9 @@ +export async function onRequest(context) { + const url = new URL(context.request.url); + const forwarded = context.request.headers.get("x-forwarded-proto"); + if (url.protocol === "http:" || forwarded === "http") { + url.protocol = "https:"; + return Response.redirect(url.toString(), 301); + } + return context.next(); +} diff --git a/site/functions/install/[...path].js b/site/functions/install/[...path].js new file mode 100644 index 0000000..c7a7f54 --- /dev/null +++ b/site/functions/install/[...path].js @@ -0,0 +1,9 @@ +export function onRequest() { + return new Response("not found\n", { + status: 404, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "X-Content-Type-Options": "nosniff", + }, + }); +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..eac7c58 --- /dev/null +++ b/site/index.html @@ -0,0 +1,158 @@ + + +
+ + +curl -fsSL https://hstack.sh/install | sh
+
+ HStack
planning load
965tokens
+ +GStack
planning load
62,514tokens
+Under 2% of the context. Built for Hermes.
+A community project. Not affiliated with Nous Research.
+Hermes reads every skill description on each turn. HStack keeps one short router there. A playbook loads only when the task needs it. Counts use Hermes's estimator, ceil(bytes / 4).
+ +One line in the index. The body loads when you ask for that skill.
+ +/hstack is the skill to remember. It opens the route catalog and loads one playbook. Everything else stays on disk.
A one-line edit does not need a playbook. If you name another HStack skill, that skill loads and the router steps aside.
+Hermes lists the skills under hstack. Ask with the skill name.
/hstack the export drops rows over 10 MB
+/reframe
+/ship
+/council
+ macOS and Linux. The script is safe to run twice. It only replaces a tree it created.
+curl -fsSL https://hstack.sh/install | sh
+ curl -fsSL https://hstack.sh/install | sh -s -- --dry-run
+ curl -fsSL https://hstack.sh/install | sh -s -- --uninstall
+ From a checkout of the repo, sh install.sh installs the skills next to the script. HSTACK_BASE_URL points the curl form at another host, which is how you try a *.vercel.app URL before DNS. Optional add-ons stay off until you pass --with.
${esc(skill.description)}
${esc(skill.description)}
${skill.idle} idle · ${fmt(skill.body)} body