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
8 changes: 5 additions & 3 deletions .agents/skills/manage-e-stack/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: manage-e-stack
version: 1.1.0
description: "MUST USE for any work on the e-stack repo — adding, editing, prepping, or publishing skills. Triggers: 'add a skill to e-stack', 'edit an estack skill', 'publish e-stack', 'release to npm', 'prep for release', 'get it ready to publish but don't publish', 'put this in e-stack', 'fix this skill', 'ship it', or any change to files under skills/ or package.json in this repo. Routes to the right step file based on intent."
---

Expand Down Expand Up @@ -33,11 +34,12 @@ These apply to every route. Violating them breaks the install or publish.
- **NEVER push a `v*` tag without intent to publish.** Any `v*` tag push runs a real npm release.
- **Prep ≠ publish.** `steps/prep.md` leaves work release-ready (gates green, versions bumped, `[Unreleased]` entries written, committed and pushed) without releasing anything; the next `steps/publish.md` run sweeps up everything prepped. When the user says "don't publish yet" or other work is in flight, route to prep.
- **Only the repo owner can push to `main`.** Branch protection requires PRs from everyone else.
- **Live install location:** `~/.agents/skills/estack-*/` (symlinked from `~/.claude/skills/estack-*/`; the installer copies from `skills/` here)
- **Live install location:** `~/.agents/skills/<installed-name>/` (symlinked from `~/.claude/skills/<installed-name>/`; the installer copies from `skills/` here)
- **Renaming/removing a skill** needs no installer edit. Deleting the folder from `skills/` is enough: the installer compares its checksum manifest against what the package ships and retires anything it recorded installing that is gone, both on disk and in the manifest. `DEPRECATED_SKILLS` in `bin/install.cjs` is legacy-only — do NOT add to it. It covers two names removed before that existed, where an old install may have no manifest entry to prune.
- **Hooks** live in `hooks/<name>.js` at the repo root (flat — no subfolders). They are Claude Code-only: unlike skills, hooks cannot be installed into `~/.agents/` and discovered from there. The installer copies them to `~/.claude/hooks/` and registers each via a dedicated `setup<Name>Hook(dryRun)` function in `bin/install.cjs` that idempotently patches `~/.claude/settings.json` (and honors `dryRun` by returning before writing). Hook scripts MUST wrap their body in `try { ... } catch { /* never break the tool */ }` and exit 0 — a hook crash must never break the underlying tool call. See `docs/hook-authoring.md`.
- **Installer:** `node bin/install.cjs` from repo root **dry-runs by default** (previews changes, writes nothing); add `--install` to actually sync. `--dry-run` forces a preview even under `npx`. Always pass `--yes` alongside `--install` when running it from an agent or a script: locally modified items trigger an interactive prompt that reads EOF on a non-TTY stdin and aborts the entire install. `--yes` answers it with "back up local changes, install the latest"; `--skip-modified` answers it with "keep local versions". Add `--no-statusline` to remove the statusline (`--statusline` restores it, `ESTACK_NO_STATUSLINE=1` skips it for one run).
- **Installer settings:** there is exactly ONE settings file, `~/.e-stack/.env`, the same one skills read API keys from. Installer settings are env vars in it: `ESTACK_SKILLS_DIR`, `ESTACK_HOOKS_DIR`, `ESTACK_BACKUP_DIR`, `ESTACK_NO_STATUSLINE`, plus `ESTACK_HOME` for the file's own location. Resolution is live environment first, then the file, matching every skill. Never add a second config file, a JSON sidecar, or a marker file — put the new setting in `.env` under an `ESTACK_*` name and write it with `writeSetting()`, which replaces or appends only that one line so shared credentials survive. Installer *state* is a different thing from settings: derive it from the existing checksum manifest (`~/.claude/.estack-checksums.json`) the way the statusline claim does, rather than persisting a new field.
- **Installer:** `node bin/install.cjs` from repo root **dry-runs by default** (previews changes, writes nothing); add `--install` to actually sync. `--dry-run` forces a preview even under `npx`. Always pass `--yes` alongside `--install` when running it from an agent or a script: locally modified items trigger an interactive prompt that reads EOF on a non-TTY stdin and aborts the entire install. `--yes` answers it with "back up local changes, install the latest"; `--skip-modified` answers it with "keep local versions". Add `--no-statusline` to remove the statusline (`--statusline` restores it, `ESTACK_NO_STATUSLINE=1` skips it for one run). Add `--no-prefix` to install skills under their short names (`--prefix` restores the `estack-` prefix, `ESTACK_SKILL_PREFIX=none` does the same from the settings file).
- **Skill name prefix:** skills always ship prefixed in `skills/`, and prefixed is the default install. `ESTACK_SKILL_PREFIX=none` makes the installer rewrite `estack-<short>` to `<short>` in every packaged file as it copies, so the folder, the `name:` line, cross-skill references, and self-referencing install paths all move together. The setting is read on every run including `--silent`/`--startup`, so changing it in `~/.e-stack/.env` reconciles an existing install at the next session start. That works because the checksum manifest is keyed by *installed* name — anything touching the destination or the manifest goes through `installedName()`, and only reads from `PACKAGE_SKILLS_DIR` use the package name. A prefix switch backs up old files and carries per-skill `.env` files, then retires the old name only after successful installation. Silent mode defers edited sources and foreign destinations; manual/startup installs back up foreign skills before replacement. Existing backup directories are retained with unique suffixes for new copies. Quoted standalone storage identifiers and variable-based state paths remain canonical. Package-side hashes are computed on the rewritten content (`computeSkillHash(dir, true)`), or every skill reads as locally modified forever. `scripts/check-skill-name.cjs` rule 5 is what makes the rewrite complete; do not loosen it.
- **Installer settings:** there is exactly ONE settings file, `~/.e-stack/.env`, the same one skills read API keys from. Installer settings are env vars in it: `ESTACK_SKILLS_DIR`, `ESTACK_HOOKS_DIR`, `ESTACK_BACKUP_DIR`, `ESTACK_NO_STATUSLINE`, `ESTACK_SKILL_PREFIX`, plus `ESTACK_HOME` for the file's own location. Resolution is live environment first, then the file, matching every skill. Never add a second config file, a JSON sidecar, or a marker file — put the new setting in `.env` under an `ESTACK_*` name and write it with `writeSetting()`, which replaces or appends only that one line so shared credentials survive. Installer *state* is a different thing from settings: derive it from the existing checksum manifest (`~/.claude/.estack-checksums.json`) the way the statusline claim does, rather than persisting a new field.
- **Testing the installer:** `os.homedir()` follows `USERPROFILE`, so the whole installer runs against a throwaway home — `USERPROFILE=<tmp> node bin/install.cjs --install --yes`. Use it for anything touching settings, paths, or `settings.json` wiring instead of experimenting on a real setup.
- **Prefer deterministic changes.** Any time a change can be made exactly and reproducibly — via a shell command, an existing repo script, or a targeted diff (Edit tool) — use that instead of reading content and regenerating it. AI-generated output is never byte-for-byte identical to the source; transcription errors accumulate even on "trivial" tasks. Apply this to everything:
- **File copies**: `cp -r` / `xcopy /E /I` / `robocopy /E`, not Read+Write
Expand Down
51 changes: 5 additions & 46 deletions .agents/skills/manage-e-stack/scripts/preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,10 @@
# Pre-flight diagnostics for e-stack publishing
# Read-only — does not modify anything

# Real skill files live in ~/.agents/skills/ (symlinked from ~/.claude/skills/).
# Fall back to ~/.claude/skills/ for pre-migration installs.
INSTALL_ROOT="$HOME/.agents/skills"
[ -d "$INSTALL_ROOT" ] || INSTALL_ROOT="$HOME/.claude/skills"

echo "=== Current installed skills (in $INSTALL_ROOT) ==="
for skill in "$INSTALL_ROOT"/estack-*/; do
[ -d "$skill" ] || continue
echo "$(basename "$skill"): $(find "$skill" -type f | wc -l) files"
done

echo ""
echo "=== Repo skills ==="
for skill in skills/estack-*/; do
echo "$(basename "$skill")"
done

echo ""
echo "=== Diffs between repo and installed ==="
diffs=0
for skill in skills/estack-*/; do
name=$(basename "$skill")
if [ ! -d "$INSTALL_ROOT/$name" ]; then
echo "NEW: $name (not yet installed)"
diffs=$((diffs + 1))
else
diff_output=$(diff -rq "$skill" "$INSTALL_ROOT/$name" 2>&1)
if [ -n "$diff_output" ]; then
echo "CHANGED: $name"
echo "$diff_output"
diffs=$((diffs + 1))
fi
fi
done
for skill in "$INSTALL_ROOT"/estack-*/; do
[ -d "$skill" ] || continue
name=$(basename "$skill")
if [ ! -d "skills/$name" ]; then
echo "STALE: $name (installed but not in repo)"
diffs=$((diffs + 1))
fi
done
if [ "$diffs" -eq 0 ]; then
echo "(all match)"
fi
# The installer resolves custom roots and prefix settings and compares the
# transformed package hashes, so unprefixed installs get accurate diagnostics.
echo "=== Installed skills and pending changes ==="
node bin/install.cjs --dry-run

echo ""
echo "=== Current installed hooks ==="
Expand Down Expand Up @@ -142,7 +101,7 @@ echo "=== Version bump check (vs last release tag) ==="
node scripts/check-versions.cjs || true

echo ""
echo "=== Docs sync check (README.md / CLAUDE.md lists) ==="
echo "=== Docs sync check (README.md / AGENTS.md lists) ==="
node scripts/check-docs.cjs || true

echo ""
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/installer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Installer regression tests
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
installer:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: ["18.0.0", "24"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: node scripts/test-install-edge-cases.cjs
- run: node scripts/test-prefix-safety.cjs
7 changes: 6 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: Verify every skill's feedback section matches the template
run: node scripts/update-skill-feedback.cjs --check

- name: Verify README and CLAUDE.md list every skill and hook
- name: Verify README and AGENTS.md list every skill and hook
run: node scripts/check-docs.cjs

- name: Verify skill names are converted correctly (estack- prefix, description, self-references)
Expand All @@ -55,6 +55,11 @@ jobs:
- name: Verify skills store state in ~/.e-stack and credentials in the shared .env
run: node scripts/check-paths.cjs

- name: Verify installer regressions
run: |
node scripts/test-install-edge-cases.cjs
node scripts/test-prefix-safety.cjs

- name: Setup Python
uses: actions/setup-python@v6
with:
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Skill names can drop the `estack-` prefix. `ESTACK_SKILL_PREFIX=none` in `~/.e-stack/.env`, or installing with `--no-prefix`, installs every skill under its short name: `/email-writer` instead of `/estack-email-writer`. Prefixed stays the default and `--prefix` puts it back. The installer rewrites `estack-<short>` to `<short>` in every file as it copies, so the folder name, the `name:` line, cross-skill references in descriptions and bodies, and the self-referencing paths some skills use to run their own scripts all move together. Filenames inside a skill keep their own names — `estack-notify.ps1` still lives in `skills/notify/scripts/`.
- The setting is read on every install, including the `--silent` and `--startup` auto-updates. Change it in `~/.e-stack/.env` and the next session start reconciles an existing install to it: the names from the old setting are retired from `~/.agents/skills/` and `~/.claude/skills/`, the same skills are reinstalled under the new ones, and the `estack-notify` Stop hook in `settings.json` is repointed. Each completed switch backs up the old files and preserves credentials; silent mode defers switches requiring review.
- Short names like `notify` and `repo-search` are ordinary enough that a user may already have a skill on one. Installing unprefixed over a directory the installer never put there names it in a warning and copies it to `~/.estack-backup/` before replacing it, during manual or startup installs; silent updates preserve the collision for review.
- State directories keep their prefixed names whichever way the setting is set: a skill installed at `~/.agents/skills/flight-planner/` still reads and writes `~/.e-stack/estack-flight-planner/config.json`. `~/.e-stack/` is storage rather than a skill reference, so saved configs, caches, and credentials survive changing the setting, and the statusline keeps finding the flag file the notify skill writes.
- The installer distinguishes a skill leaving the pack from a skill changing its name. Retiring 24 skills to rename them now reads `Retired: estack-notify (reinstalling as notify)` followed by a line saying the prefix setting is what moved them, instead of 24 lines of `(no longer in the package)`.

### Fixed
- Prefix switches preserve saved-state paths, back up the old skill without replacing earlier backups, and carry per-skill `.env` files forward. Old names remain available until the replacement installs successfully.
- Silent updates defer conflicting or locally edited prefix switches; startup updates identify foreign skill takeovers explicitly. Unowned deprecated short names are left alone.
- Dry runs no longer migrate legacy settings or change permissions. Conflicting prefix/statusline flags fail before writing anything.

---

## [1.0.77] - 2026-09-05
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,16 @@ ESTACK_NO_STATUSLINE=1
| `ESTACK_HOOKS_DIR` | `~/.claude/hooks` | Where Claude hook scripts are installed |
| `ESTACK_BACKUP_DIR` | `~/.estack-backup` | Where your local changes are backed up |
| `ESTACK_NO_STATUSLINE` | unset | Set to `1` to remove the statusline |
| `ESTACK_SKILL_PREFIX` | `estack-` | Set to `none` to install skills under their short names |
| `ESTACK_HOME` | `~/.e-stack` | Where this file itself lives (env-only) |

Resolution order is the one every skill uses: the live process environment first, then the file. So exporting a variable overrides it for a single run without editing anything. Path values must be absolute; a relative one is ignored and the default applies. The installer only ever replaces or appends its own line, so your API keys are never disturbed.

**Skill names.** Skills install prefixed by default: `/estack-email-writer`, `/estack-repo-search`. Set `ESTACK_SKILL_PREFIX=none` (or install with `--no-prefix`) and they install as `/email-writer` and `/repo-search` instead, with every cross-reference inside the skills rewritten to match. Change the value later and the next session start reconciles an existing install to it — the old names are retired and the same skills are reinstalled under the new ones, after backing up the previous files and preserving per-skill `.env` files. The old name is retired only after its replacement installs successfully. Anything a skill saved stays put: state directories keep their prefixed names either way, so `/flight-planner` still reads the `~/.e-stack/estack-flight-planner/config.json` you already had. Two things to know before turning the prefix off: the short names are ordinary ones like `notify` and `repo-search`, so if you already have a skill of your own on one of them it gets copied to `~/.estack-backup/` and replaced (the installer names it in a warning first); and every doc here writes the prefixed form. Silent updates defer a switch when the source is edited or the destination belongs to another skill, preserving both and printing a diagnostic; run the manual installer to review it. Startup updates back up foreign skills before replacement and identify them separately from local edits. Existing backup directories are retained; newer copies get unique suffixes.

**Statusline.** E-Stack ships a statusline showing the model, tokens used in the current context window, the project folder, rate limits, and an `estack-notify` bell. A first install claims the `statusLine` slot even if you already have one configured, so you can see what it does; your previous value is saved to `~/.estack-backup/statusLine.json`. From then on it is only updated while it is still E-Stack's. Switch back to your own or clear it, and no later update touches it again. `--no-statusline` removes it outright and `--statusline` brings it back.

**Installer flags.** `--install` applies changes (the installer previews by default when run from a clone). `--yes` answers the locally-modified prompt with "back up my changes, install the latest" and `--skip-modified` answers it with "keep my versions" — pass one of them from any script or CI job, since the prompt otherwise reads EOF on a non-interactive stdin and aborts. `--statusline` / `--no-statusline` turn the statusline on or off and remember the choice.
**Installer flags.** Opposing flags such as `--prefix --no-prefix` are rejected before any changes. `--dry-run` never migrates settings or writes files, even when combined with `--startup` or `--silent`. `--install` applies changes (the installer previews by default when run from a clone). `--yes` answers the locally-modified prompt with "back up my changes, install the latest" and `--skip-modified` answers it with "keep my versions" — pass one of them from any script or CI job, since the prompt otherwise reads EOF on a non-interactive stdin and aborts. `--statusline` / `--no-statusline` turn the statusline on or off and remember the choice, and `--prefix` / `--no-prefix` do the same for the `estack-` prefix on skill names.

## Requirements

Expand Down
Loading
Loading