From 634cdc31caaa7c8190d6b7823af716a90c458fab Mon Sep 17 00:00:00 2001 From: Iko Date: Mon, 5 Oct 2026 20:26:07 -0700 Subject: [PATCH] single-source installs: retire the dist-publish hop Installs now come directly from this repo's pstack/ subdir (`hermes plugins install iap/pstack-hermes/pstack`); the separate dist repo (iap/pstack) is retired. - publish-plugin.yml deleted; UPSTREAM_PIN lock-step narrowed to three workflows (tools/check_pins.py + its tests + all docs updated). - after-install.md / README templates now teach the single-source route: updates are reinstalls (`--force`), immutable installs via `--ref`; docs pointers moved to this repo. - EOL contract now rides the repository root .gitattributes (what an install clone actually receives); dist_checkout_check stages source-tree copies with the root rules and the root pins *.png binary. - Compatibility fixes along the way: AGENTS/USING/RUNBOOK rows unified on the single route; README version references refreshed to v0.15.5. - pstack/ rebuilt (converter doc templates changed); all gates green: pytest 94, ruff, actionlint, check_pins, check_action_shell, validate, scanner, dist-checkout. Co-authored-by: Hermes --- .gitattributes | 4 + .github/actions/build-package/action.yml | 10 +- .github/workflows/ci.yml | 4 +- .github/workflows/publish-plugin.yml | 122 --------------------- .github/workflows/release.yml | 2 +- .github/workflows/upstream-drift-watch.yml | 4 +- AGENTS.md | 27 +++-- CHANGELOG.md | 1 + CONTRIBUTING.md | 28 ++--- README.md | 34 +++--- docs/RUNBOOK-upstream-drift.md | 10 +- docs/USING.md | 4 +- pstack/.build-provenance.txt | 4 +- pstack/README.md | 18 +-- pstack/after-install.md | 4 +- tools/check_pins.py | 5 +- tools/convert.py | 22 ++-- tools/dist_checkout_check.py | 21 ++-- tools/tests/test_check_pins.py | 2 +- 19 files changed, 102 insertions(+), 224 deletions(-) delete mode 100644 .github/workflows/publish-plugin.yml diff --git a/.gitattributes b/.gitattributes index 68b5b3a..8b3a171 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,7 @@ # Mark it generated so GitHub collapses its diffs and excludes it from # language stats: review it as build output, not as hand-written code. pstack/** linguist-generated=true + +# Binaries stay byte-identical on checkout (assets/logo.png): pin the intent +# so a future text rule cannot start rewriting them. +*.png binary diff --git a/.github/actions/build-package/action.yml b/.github/actions/build-package/action.yml index 59e1fd1..116f9c5 100644 --- a/.github/actions/build-package/action.yml +++ b/.github/actions/build-package/action.yml @@ -2,16 +2,16 @@ name: Build pstack package description: >- Shared build prologue for every workflow that produces the generated pstack/ package: pin the upstream clone, verify it resolved, convert, validate and - scanner-gate. Extracted from the step bodies that ci.yml, release.yml and - publish-plugin.yml each carried their own copy of, so all three callers now - get the same fail-loud SHA verification. + scanner-gate. Extracted from the step bodies that ci.yml and release.yml each + carried their own copy of, so every caller now gets the same fail-loud SHA + verification. # WHY A COMPOSITE ACTION AND NOT A REUSABLE WORKFLOW # A reusable workflow (`uses: ./.github/workflows/build.yml`) would have to contain # the caller's ENTIRE job, because a `uses:` job cannot be followed by more steps -# in that same job. All three callers continue past the shared prologue with their +# in that same job. Both callers continue past the shared prologue with their # own steps (ci: reproducibility gate + artifact upload; release: zip + notes + -# gh release; publish: dist push + tag), so a reusable workflow cannot express +# gh release), so a reusable workflow cannot express # this shape without duplicating those steps into the called file. A composite # action composes into the middle of a job and is the correct tool here. # The usual reason to prefer a reusable workflow - secrets - does not apply: this diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98d1d4e..43f05be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,8 +49,8 @@ jobs: # job above validates the source pstack/ directory on disk, where files are LF # because the converter wrote them that way. Nothing here constructs a git # checkout of the dist tree, so a line-ending or binary-asset regression in the - # published artifacts would ship uncaught. This stages the package exactly as - # publish-plugin.yml does and checks out the result with core.autocrlf=true. + # published artifacts would ship uncaught. This stages the package exactly as an + # install clone receives it (pstack/ subdir, core.autocrlf=true). - name: Dist checkout is LF-only under autocrlf shell: bash run: uv run --frozen tools/dist_checkout_check.py --package pstack --source-tree diff --git a/.github/workflows/publish-plugin.yml b/.github/workflows/publish-plugin.yml deleted file mode 100644 index 8e2989c..0000000 --- a/.github/workflows/publish-plugin.yml +++ /dev/null @@ -1,122 +0,0 @@ -# Publish: push the built plugin package to the dist repo (root-level install source). -# -# The dist repo (DIST_REPO below) is build output, like pstack/ itself: its -# content is written only by this workflow and never edited by hand. Its tree -# root IS the package (plugin.json at root), which is what makes the root -# install shorthand work: -# -# hermes plugins install iap/pstack --enable # no /pstack subdir needed -# hermes plugins update pstack # in-place: root installs keep .git -# -# Publishes are APPEND-ONLY commits on top of the dist repo's main branch - -# never a history rewrite. `hermes plugins update` is a `git pull --ff-only` -# on the installed clone, so a rewritten remote would break every user's -# in-place update. -# -# Prerequisites (one-time, maintainer): -# Add a fine-grained PAT (contents: read/write on DIST_REPO only) as the -# PLUGIN_DIST_TOKEN secret in this repository. The dist repo -# (github.com/iap/pstack) exists; until the secret is set, publishes are -# skipped with a warning and the build gates above still run. - -name: publish-plugin - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -env: - UPSTREAM_PIN: c47b12849e43f18d5c374c7069c744cc55b0ea00 - DIST_REPO: iap/pstack - -concurrency: - group: publish-plugin - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - persist-credentials: false - - # Shared build prologue - same action ci.yml and release.yml use, so the - # published tree can never be built by a divergent copy of the steps. - - name: Build package from pin - uses: ./.github/actions/build-package - with: - upstream-pin: ${{ env.UPSTREAM_PIN }} - - - name: Publish dist tree - shell: bash - env: - PLUGIN_DIST_TOKEN: ${{ secrets.PLUGIN_DIST_TOKEN }} - run: | - set -euo pipefail - if [ -z "${PLUGIN_DIST_TOKEN}" ]; then - echo "::warning::PLUGIN_DIST_TOKEN secret is not set - skipping dist publish to ${DIST_REPO} (build gates above still ran). Create the dist repo and add the secret to enable publishing." - exit 0 - fi - authed_url="https://x-access-token:${PLUGIN_DIST_TOKEN}@github.com/${DIST_REPO}.git" - - dist="$RUNNER_TEMP/dist" - if ! git clone -q "$authed_url" "$dist" 2>/dev/null; then - echo "::error::clone of ${DIST_REPO} failed (repo missing, or token lacks access)" - exit 1 - fi - # Scrub the token from the clone's remote config; pushes pass the URL - # explicitly so it is never persisted. - git -C "$dist" remote set-url origin "https://github.com/${DIST_REPO}.git" - # Empty dist repo on first publish: the clone warns and has no branches. - git -C "$dist" checkout -q main 2>/dev/null || git -C "$dist" checkout -q -b main - - # Replace the tree with the fresh build (.git kept): an append-only - # commit on top of main so installed clones can fast-forward. - find "$dist" -mindepth 1 -maxdepth 1 -not -name ".git" -exec rm -rf {} + - cp -a pstack/. "$dist/" - # The dist repo is consumed by `hermes plugins install` (a git clone), so it needs - # its own .gitattributes. The package contract is LF-only (tools/validate.py fails on - # any CRLF), but without this a consumer on Windows -- where core.autocrlf=true is - # common -- checks the package out as CRLF and then `hermes plugins update`'s - # stash/pull/reapply silently rewrites their customized config/models.json. - # Defensive: git already sniffs PNG as binary by content, so this line is not - # what currently protects assets/logo.png. It pins the intent, so a future - # `text` rule cannot silently start rewriting it. - # The rule itself lives in tools/assets/dist.gitattributes so this publisher and - # tools/dist_checkout_check.py (the regression test for it) cannot drift apart. - cp -a tools/assets/dist.gitattributes "$dist/.gitattributes" - git -C "$dist" add -A - # Gate on the tree we are about to publish, as a consumer would receive it. - # validate.py checks the source pstack/ directory on disk, where files are LF - # because the converter wrote them that way; it cannot see a line-ending or - # binary-asset regression introduced by the staging above. - if ! uv run --frozen tools/dist_checkout_check.py --package "$dist"; then - echo "::error::staged dist tree fails the consumer checkout check - refusing to publish" - exit 1 - fi - if git -C "$dist" diff --cached --quiet; then - echo "dist tree unchanged - nothing to publish" - exit 0 - fi - export GIT_AUTHOR_NAME="github-actions[bot]" GIT_AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com" - export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" - # One line: pin + run URL. Everything else (converter hash, timestamp) - # is already in .build-provenance.txt inside the tree. - git -C "$dist" commit -q \ - -m "publish: pstack from pin ${UPSTREAM_PIN:0:7} (run ${GITHUB_RUN_ID})" - - git -C "$dist" push -q "$authed_url" main:main - # Tag every real publish: the hermes plugin catalog requires the - # submitted repo to carry real tags (not just a default branch), and - # each tag makes one dist state human-addressable. Tag name embeds - # the build date + the dist commit, so tags are unique per publish. - tag="pstack-$(date -u +%Y%m%d)-$(git -C "$dist" rev-parse --short HEAD)" - git -C "$dist" tag "$tag" - git -C "$dist" push -q "$authed_url" "refs/tags/$tag" - echo "published ${DIST_REPO}@$(git -C "$dist" rev-parse HEAD) (tag ${tag})" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 115cdef..dfc5fae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false # release.yml previously carried its own copy of the build prologue AND was - # missing the "Verify pinned SHA" step that ci.yml and publish-plugin.yml + # missing the "Verify pinned SHA" step that ci.yml # had, so a release could be cut from a pin that silently failed to # resolve. The shared action gives every caller the verified-pin behaviour. - name: Build package from pin diff --git a/.github/workflows/upstream-drift-watch.yml b/.github/workflows/upstream-drift-watch.yml index 1af255d..eabdf82 100644 --- a/.github/workflows/upstream-drift-watch.yml +++ b/.github/workflows/upstream-drift-watch.yml @@ -92,8 +92,8 @@ jobs: echo "" echo "Rebuild against the new upstream, update the transform maps for any" echo "reworded anchors (the converter's anchor audit will name them), bump" - echo "UPSTREAM_PIN in all four workflows (ci.yml / release.yml /" - echo "publish-plugin.yml / upstream-drift-watch.yml - tools/check_pins.py" + echo "UPSTREAM_PIN in all three workflows (ci.yml / release.yml /" + echo "upstream-drift-watch.yml - tools/check_pins.py" echo "enforces lock-step), and re-run the pipeline." } > "$RUNNER_TEMP/drift-body.md" # Guard for the next step: split across steps, the no-drift `exit 0`s diff --git a/AGENTS.md b/AGENTS.md index fea8cbf..0b28234 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,9 +38,9 @@ to improve **stock** pstack skills shipped in this package. |---|---|---| | Change stock method for all users | Converter / `tools/assets/` → rebuild → validate → release | Yes (product) | | Personal or project procedure | `skill_manage` / `/learn` into **local** `~/.hermes/skills/` | Yes (outside package) | -| Owned variant of pstack skills | **Fork** of dist `iap/pstack`, install from the fork | Yes (you control merge) | +| Owned variant of pstack skills | **Fork** of this repo, install from the fork's `pstack/` subdir | Yes (you control merge) | | New skills beside pstack | **Skill tap** (`hermes skills tap add` / install) | Yes | -| Durable "learning" into installed plugin `SKILL.md` files | **Forbidden** as source of truth | No — updates and dist republish collide | +| Durable "learning" into installed plugin `SKILL.md` files | **Forbidden** as source of truth | No — reinstalls overwrite the installed tree | > [!WARNING] > Never instruct `skill_manage` (or any self-evolution path) to patch @@ -75,7 +75,7 @@ Default home for work is **this repo**. Escalate only when the table says so. | If you need to… | Do this | Not this | |---|---|---| | Change skill prose, delegation wording, model panel, hermesbot, bans, docs, converter maps | Edit `tools/` / `tools/assets/` → rebuild → validate → (ledger row if converter changed) | Hand-edit `pstack/`; rewrite in Cursor vocabulary | -| Personal skill improvement or new procedures | Local skills, skill tap, or fork of `iap/pstack` ([docs/USING.md](docs/USING.md)) | `skill_manage` against stock plugin skills; durable edits under the install tree | +| Personal skill improvement or new procedures | Local skills, skill tap, or fork of this repo ([docs/USING.md](docs/USING.md)) | `skill_manage` against stock plugin skills; durable edits under the install tree | | Package validates / doctor is clean, but stock install misbehaves on hermes | File/fix in `NousResearch/hermes-agent`; `patches/` is **historical reference only** ([docs/PATCHES.md](docs/PATCHES.md)) | Paper over harness gaps with skill prose that claims nonexistent tools | | Bug reproduces on upstream Cursor pstack *without* this port | Route to `cursor/plugins` ([SECURITY.md](SECURITY.md)) | "Fix" it in the converter as if it were a port bug | | Touch `.cursor-plugin/`, `pstack/agents/`, attribution, excluded `benny` / `make-bot-ui` | Leave as-is (dual-load / scanner contract) | "Clean up", re-add, or modernize | @@ -183,23 +183,23 @@ Cursor still appears in three **legitimate** roles only — preserve, do not ## Hard rules (CI enforces) - **Pin lock-step.** `UPSTREAM_PIN` - (`93b00b89ef425a9c1bac0d0b317dfc49c930ac99`) is declared in `ci.yml`, - `release.yml`, `publish-plugin.yml`, and `upstream-drift-watch.yml` — all four - move together. The build steps that consume it are shared via + (`c47b12849e43f18d5c374c7069c744cc55b0ea00`) is declared in `ci.yml`, + `release.yml`, and `upstream-drift-watch.yml` — all three move together. The build steps that consume it are shared via `.github/actions/build-package`. Procedure: [docs/RUNBOOK-upstream-drift.md](docs/RUNBOOK-upstream-drift.md). - **Anchors audited.** A converter transform whose anchor matches nothing in the upstream build fails loudly. Re-anchor or prune; never silence the audit. -- **Two version lines.** Package keeps upstream identity `0.14.8`; repo +- **Two version lines.** Package keeps upstream identity `0.15.5`; repo releases are `v0.4.x` in `CHANGELOG.md`. Never mix them. - **Encoding.** LF-only, no BOM, UTF-8 (`.gitattributes` + `validate.py`). - **Converter changes are documented.** New pass/map → ADAPTATIONS row with reviewer check + rebuilt `pstack/` in the same PR. - **Excluded upstreams stay excluded.** `automations/benny` and `skills/make-bot-ui` omitted (slot filled by repo-owned `hermesbot`). -- **Dist repo is build output.** `iap/pstack` is written only by CI - (`publish-plugin.yml`), append-only. Never hand-push. Never point install - docs at this dev repo's root (scanner sees `tools/` ban needles). +- **Single-source installs.** Installs come from this repo's `pstack/` subdir + (`hermes plugins install iap/pstack-hermes/pstack`). The old dist repo + `iap/pstack` is retired — never publish there again; never document a + root install of this dev repo (scanner sees `tools/` ban needles). ## Common traps @@ -209,10 +209,9 @@ Cursor still appears in three **legitimate** roles only — preserve, do not `tools/assets/model-panel.json` and rebuild; `validate.py` checks they match. - "Fixing" Cursor mentions that are attribution or dual-load — correct as-is. - Rewriting hermes primitives back into Cursor vocabulary — bans catch this. -- Blurring install routes: **root** (`hermes plugins install iap/pstack`) keeps - `.git` and updates via `hermes plugins update pstack`; **subdir** - (`iap/pstack-hermes/pstack`) strips `.git` → reinstall only. Docs must keep - them distinct. +- Install-update mechanics: subdir installs strip `.git` (hermes design), so + `hermes plugins update` does NOT work for them — updates are reinstalls + (`hermes plugins install iap/pstack-hermes/pstack --force`). Docs must say so. - Using Cursor IDE dual-load habits as the implementation or review path for hermes delivery — verify with convert → validate → doctor / hermes invoke. - Pointing hermes `skill_manage` / learning / self-evolution at **stock** diff --git a/CHANGELOG.md b/CHANGELOG.md index f678367..d12d0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the pstack-hermes port tooling will be documented in this - `.github/actions/drift-issue-upsert/`: shared composite action for the idempotent issue-by-title upsert both drift watchers need; the two callers' copies had already drifted apart (different jq expressions for the same intent). ### Changed +- **Single-source installs.** The package installs directly from this repository's `pstack/` subdir (`hermes plugins install iap/pstack-hermes/pstack`); the dist-repo publish pipeline (`publish-plugin.yml` → `iap/pstack`) is retired and `UPSTREAM_PIN` lock-step now covers three workflows. Updates are reinstalls (`--force`) - documented in README and the installed `after-install.md`. - **Re-pin to upstream `c47b1284` (pstack v0.15.5); `pstack/` rebuilt.** Every converter anchor was re-anchored or pruned against the new upstream text (36 dead anchors at the new pin -> 0; `T8`-`T14` + delegation), **F16 retired** (upstream now ships the slug-agnostic lane check itself), `reflect`'s 4-column reviewer table adopted with the upstream `budget` key in the setup-written panel shape, and the two new upstream principles ported (`principle-attack-the-premise`, `principle-test-behavior-not-implementation`) - 45 -> 47 skills. Prose model defaults refreshed (`grok-4.7-xhigh-fast`, `claude-opus-5-5-max`); the shipped panel keeps its vendor-qualified verified slugs. `UPSTREAM_PIN` bumped in all four workflows (lock-step enforced by `tools/check_pins.py`); details and the triage correction (no `skills/orchestrate` ever existed) in `docs/ADAPTATIONS.md` section 4. - Docs / agent policy (Phase A, no package rebuild): `AGENTS.md` and `docs/USING.md` codify the stock-skills write surface — Hermes self-improvement (`skill_manage` / learning) targets local skills, skill taps, or a dist fork, not stock plugin skills. Stack forge freeze: **Graphite (`gt`) default**, **GitHub (`gh`) fallback**, **GitLab (`glab`) halted**. README Known limitations aligned (was incorrectly describing Graphite as optional-only). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bc980c..e9db793 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,17 +70,11 @@ bun run typecheck # deps + tsc --noEmit --strict directory, elsewhere it can resolve to an installed package. For branch verification, pass the resolved local path (`$(pwd)/pstack`); for the installed dist package, use the name as installed. -4. **Publish** — on every push to `main`, - [publish-plugin.yml](.github/workflows/publish-plugin.yml) re-runs - convert → validate → scanner gate and publishes the built tree to the - dist repo `iap/pstack` as an **append-only commit**, tagged - `pstack--` (the plugin catalog admits only repos with real - tags). Build output: never edited by hand, history never rewritten — - `hermes plugins update` fast-forwards installed clones. The dist - repo's tree root is the package, which is what makes the root install - shorthand (`hermes plugins install iap/pstack`) and in-place - `hermes plugins update pstack` work. Requires the one-time `PLUGIN_DIST_TOKEN` - secret (fine-grained PAT, contents: read/write on the dist repo only). +4. **Install payload** — CI builds and re-validates the package on every + push (convert → validate → scanner gate), and the validated + `pstack/` subtree on `main` is what users install + (`hermes plugins install iap/pstack-hermes/pstack`). There is no separate + dist repo: nothing is published to a second repository. ## Repository contract @@ -106,8 +100,8 @@ one has already caught (or would have caught) a real defect here. group PR updates both the SHA and the comment. Review SHA bumps like any dependency change - the usual CI gates run on the PR. - **`UPSTREAM_PIN` moves in lock-step.** It is declared in `ci.yml`, - `release.yml`, `publish-plugin.yml` and `upstream-drift-watch.yml`; - `tools/check_pins.py` fails CI when any of the four is missing, duplicated, + `release.yml` and `upstream-drift-watch.yml`; + `tools/check_pins.py` fails CI when any of the three is missing, duplicated, malformed, or out of step. Follow [docs/RUNBOOK-upstream-drift.md](docs/RUNBOOK-upstream-drift.md). - **Workflows must pass `actionlint`** (with `shellcheck`) - the `Lint @@ -150,11 +144,11 @@ one has already caught (or would have caught) a real defect here. Follow [docs/RUNBOOK-upstream-drift.md](docs/RUNBOOK-upstream-drift.md) verbatim. The one rule people forget: `UPSTREAM_PIN` is declared independently -in **four workflows** — `ci.yml`, `release.yml`, `publish-plugin.yml`, -`upstream-drift-watch.yml` — and all four must move in the same PR. Update maps, +in **three workflows** — `ci.yml`, `release.yml`, +`upstream-drift-watch.yml` — and all three must move in the same PR. Update maps, never the package. (The build prologue itself now lives once, in the -`.github/actions/build-package` composite action, so `ci.yml`, `release.yml` and -`publish-plugin.yml` pass the pin to it as an input rather than each repeating +`.github/actions/build-package` composite action, so `ci.yml` and `release.yml` +pass the pin to it as an input rather than each repeating the clone/verify/convert steps. `tools/check_pins.py` now enforces the lock-step rule in CI, so a missed file fails fast instead of at release time.) diff --git a/README.md b/README.md index b6a4405..89bd3a0 100644 --- a/README.md +++ b/README.md @@ -18,26 +18,26 @@ Cursor-plugin bugs that reproduce without this port belong upstream. ## Install ```sh -# hermes (portable plugin path) — root install (recommended) -hermes plugins install iap/pstack --enable - -# Legacy: subdir install from this dev repo (still works) +# single-source install (this repository's built package) hermes plugins install iap/pstack-hermes/pstack --enable + +# immutable variant: pin an exact commit +hermes plugins install iap/pstack-hermes/pstack --ref <40-character commit SHA> ``` > [!NOTE] -> The root form installs from the **dist repo** (`iap/pstack`), whose tree root -> is the built package — published by CI from the pinned upstream build -> ([publish-plugin.yml](.github/workflows/publish-plugin.yml)). The dev repo -> itself (`iap/pstack-hermes`) cannot be installed from its root: the install -> scanner scans the whole tree, and this repo deliberately ships its -> development tooling (`tools/`, CI) alongside the package. The `pstack` subdir -> is the plugin; the dist repo is the plugin. +> Installs come straight from this repository's `pstack/` subdirectory — the +> built package CI validates on every push. There is no separate dist repo; +> the old root shortcut (`iap/pstack`) is retired and no longer published. +> +> **Updates:** subdir installs have no `.git` (hermes keeps `.git` only for +> root installs), so `hermes plugins update` does not apply. Update with a +> reinstall: `hermes plugins install iap/pstack-hermes/pstack --force`. +> Coming from the old dist install? That same command migrates you. > [!NOTE] > The bare-name form `hermes plugins install pstack` is not available — -> the package has no entry in the hermes plugin catalog yet. Use one of the -> two forms above. +> the package has no entry in the hermes plugin catalog yet. Use the form above. **Update:** a root install keeps `.git`, so it updates in-place: ```sh @@ -75,7 +75,7 @@ patches/ historical hermes-fork patches (reference only; not required docs/ ADAPTATIONS.md (the ledger), RUNBOOK-upstream-drift.md, USING.md, PATCHES.md + UPSTREAM-PR.md (historical) .github/ CI (ci.yml: 2-OS matrix, SHA gates, determinism, poteto-mode Bun suite), - publish-plugin.yml (dist-repo publisher), PR labeler, issue templates + PR labeler, issue templates ``` ## Verification @@ -125,11 +125,11 @@ patch reference. End-user invoke and customization: | Thing | Name | Why | |---|---|---| -| The package | `pstack` | upstream identity (plugin.json, v0.14.8) — preserved | +| The package | `pstack` | upstream identity (plugin.json, v0.15.5) — preserved | | This repository | `pstack-hermes` | the project slug and published repo name (docs title: Pstack Hermes); the development/pipeline repo | -| Dist repo | `pstack` (`iap/pstack`) | plugin-only build output, published by [publish-plugin.yml](.github/workflows/publish-plugin.yml) (append-only commits); the root install source | +| Dist repo (retired) | — | superseded: installs now come from this repo's `pstack/` subdir; nothing is published to a second repository | | Tooling project | `pstack-hermes-plugin-tools` | uv project scoping the converter/validator only | -| Repo releases | `v0.4.x` | tooling/CHANGELOG version line — a release never renumbers the package, which keeps the upstream pstack version (`0.14.8`) | +| Repo releases | `v0.4.x` | tooling/CHANGELOG version line — a release never renumbers the package, which keeps the upstream pstack version (`0.15.5`) | | Plugin namespace | `agent-plugin-pstack-7171b73f:` | hermes portable-path id (derived from the manifest) | ## Cursor dual-load (incidental, unsupported) diff --git a/docs/RUNBOOK-upstream-drift.md b/docs/RUNBOOK-upstream-drift.md index 6413a5a..d8c9736 100644 --- a/docs/RUNBOOK-upstream-drift.md +++ b/docs/RUNBOOK-upstream-drift.md @@ -41,8 +41,8 @@ Any of these means upstream moved past the pin: contract or Known limitations describe, update the README template in `convert.py` and rebuild. 7. **Update `UPSTREAM_PIN` everywhere it is declared.** It is repeated - independently in **four** workflows — `ci.yml`, `release.yml`, - `publish-plugin.yml`, and `upstream-drift-watch.yml` — and all four must move + independently in **three** workflows — `ci.yml`, `release.yml`, + `upstream-drift-watch.yml` — and all three must move together. The converter holds no pin: it records the SHA of the clone it is pointed at, so `source_commit` in `.build-provenance.txt` follows the checkout. @@ -53,10 +53,10 @@ Any of these means upstream moved past the pin: `iap/plugins` is a fork of `cursor/plugins` kept as pin insurance. If upstream is deleted or moves, point the build action's `upstream-pin` input in the workflows that declare -`UPSTREAM_PIN` (`ci.yml`, `release.yml`, `publish-plugin.yml`, +`UPSTREAM_PIN` (`ci.yml`, `release.yml`, `upstream-drift-watch.yml`) at the fork — the port then continues from the -frozen pin. Users are never affected: installs and updates come from -`iap/pstack`, not upstream. +frozen pin. Users are never affected: installs come from this repo's `pstack/` +subdir, not upstream. ## Do not diff --git a/docs/USING.md b/docs/USING.md index 3fe4b29..e223e33 100644 --- a/docs/USING.md +++ b/docs/USING.md @@ -82,8 +82,8 @@ Pick by what you want to change — one of three durable routes: | Intent | Route | Notes | |---|---|---| -| Tweak a line or two (temporary) | Edit installed `SKILL.md` in place, restart gateway | Keep edits **uncommitted** on root installs so `hermes plugins update` can autostash/reapply. Subdir installs **wipe** edits on reinstall — copy out first. Not a durable learning path. | -| Own several changed skills, versioned | **Fork** dist repo `iap/pstack`, install from your fork | `hermes plugins install /pstack --enable`. Merge upstream when you choose. | +| Tweak a line or two (temporary) | Edit installed `SKILL.md` in place, restart gateway | Keep edits small and copy them out before a reinstall — subdir installs **wipe** edits, and `hermes plugins update` does not apply to them. Not a durable learning path. | +| Own several changed skills, versioned | **Fork** this repo, install from your fork's `pstack/` subdir | `hermes plugins install /pstack-hermes/pstack --enable`. Merge upstream when you choose. | | New skills beside stock pstack | **Skill tap** or local `~/.hermes/skills/` | `hermes skills tap add /skills-repo`, then install; or `skill_manage` / `/learn` into **local** skills / your tap. Stock pstack stays stock. | Structural changes that must survive upstream re-pins (new adaptations, diff --git a/pstack/.build-provenance.txt b/pstack/.build-provenance.txt index 9076545..e594846 100644 --- a/pstack/.build-provenance.txt +++ b/pstack/.build-provenance.txt @@ -2,8 +2,8 @@ package: pstack (hermes agent-plugins-v1, Phase-0 conversion) source: (local clone; public origin in source_url) source_url: https://github.com/cursor/plugins.git source_commit: c47b12849e43f18d5c374c7069c744cc55b0ea00 -converted_at: 2026-10-06T00:29:16Z -converter: convert.py (sha256[:16]=529bf4885507192c) +converted_at: 2026-10-06T00:58:37Z +converter: convert.py (sha256[:16]=d34c83cfd816fbea) schema: https://agent-plugins.org/schemas/1.0.0/plugin.schema.json fixes: - F-publish: skills/make-bot-ui excluded (Tailscale privilege-escalation scan; F33; upstream flattened the grokbot container) diff --git a/pstack/README.md b/pstack/README.md index 6cf3c83..c20021d 100644 --- a/pstack/README.md +++ b/pstack/README.md @@ -8,11 +8,12 @@ package keeps the upstream identity pstack v0.15.5.* ## Install ```sh -hermes plugins install iap/pstack --enable +hermes plugins install iap/pstack-hermes/pstack --enable ``` -Legacy route: `hermes plugins install iap/pstack-hermes/pstack --enable` -(the dev repo's subdir; works, but cannot update in-place). +Update: `hermes plugins install iap/pstack-hermes/pstack --force` +(subdir installs have no `.git`, so `hermes plugins update` does not apply). +Pin a commit with `--ref <40-character SHA>` for an immutable install. ## First run @@ -35,19 +36,18 @@ enter the prompt index with 60-char descriptions and lose the namespace. ## Update ```sh -hermes plugins update pstack +hermes plugins install iap/pstack-hermes/pstack --force ``` -Root installs keep `.git`: this pulls the latest published build and -autostashes uncommitted local skill edits. Subdir installs strip `.git` — -reinstall to update (local edits are wiped; copy them out first). +Subdir installs strip `.git`, so updates are reinstalls; local skill edits are +wiped on reinstall - copy them out first. ## Customizing skills - **Tweak a line or two** — edit the installed `SKILL.md`, restart the gateway, keep the edit uncommitted (updates autostash it). -- **Own several changed skills** — fork `iap/pstack`, commit, install from - your fork (`hermes plugins install /pstack --enable`). +- **Own several changed skills** — fork `iap/pstack-hermes`, commit, install from + your fork's subdir (`hermes plugins install /pstack-hermes/pstack --enable`). - **Add your own new skills alongside** — a skill tap: `hermes skills tap add /skills-repo`, then `hermes skills install /skills-repo/`. diff --git a/pstack/after-install.md b/pstack/after-install.md index c6f83e3..1fc2681 100644 --- a/pstack/after-install.md +++ b/pstack/after-install.md @@ -9,5 +9,5 @@ Skills are opt-in on the portable path: load one with `skill_view agent-plugin-pstack-:`, or add this package's `skills/` dir to `skills.external_dirs` in the hermes config for -`/` slash commands. Update anytime with `hermes plugins update pstack`. -Docs: +`/` slash commands. Update with a reinstall: `hermes plugins install iap/pstack-hermes/pstack --force`. +Docs: diff --git a/tools/check_pins.py b/tools/check_pins.py index a39a9d1..e688a14 100644 --- a/tools/check_pins.py +++ b/tools/check_pins.py @@ -2,9 +2,9 @@ """Assert every UPSTREAM_PIN declaration agrees, and is well-formed. AGENTS.md and CONTRIBUTING.md both state the rule - the pin must stay in -lock-step across ci.yml / release.yml / publish-plugin.yml / +lock-step across ci.yml / release.yml / upstream-drift-watch.yml - but nothing enforced it. A bump that missed one file -would publish a dist built from a pin that CI never validated, and only a code +would ship a package built from a pin that CI never validated, and only a code review would notice. This gate makes the documented rule machine-checked and exists specifically to de-risk the re-pin procedure in docs/RUNBOOK-upstream-drift.md. @@ -27,7 +27,6 @@ REQUIRED_DECLARERS = ( "ci.yml", "release.yml", - "publish-plugin.yml", "upstream-drift-watch.yml", ) diff --git a/tools/convert.py b/tools/convert.py index 6b27c1b..63a9d34 100644 --- a/tools/convert.py +++ b/tools/convert.py @@ -1838,11 +1838,12 @@ def _local_asset_refs(obj): ## Install ```sh -hermes plugins install iap/pstack --enable +hermes plugins install iap/pstack-hermes/pstack --enable ``` -Legacy route: `hermes plugins install iap/pstack-hermes/pstack --enable` -(the dev repo's subdir; works, but cannot update in-place). +Update: `hermes plugins install iap/pstack-hermes/pstack --force` +(subdir installs have no `.git`, so `hermes plugins update` does not apply). +Pin a commit with `--ref <40-character SHA>` for an immutable install. ## First run @@ -1865,19 +1866,18 @@ def _local_asset_refs(obj): ## Update ```sh -hermes plugins update pstack +hermes plugins install iap/pstack-hermes/pstack --force ``` -Root installs keep `.git`: this pulls the latest published build and -autostashes uncommitted local skill edits. Subdir installs strip `.git` — -reinstall to update (local edits are wiped; copy them out first). +Subdir installs strip `.git`, so updates are reinstalls; local skill edits are +wiped on reinstall - copy them out first. ## Customizing skills - **Tweak a line or two** — edit the installed `SKILL.md`, restart the gateway, keep the edit uncommitted (updates autostash it). -- **Own several changed skills** — fork `iap/pstack`, commit, install from - your fork (`hermes plugins install /pstack --enable`). +- **Own several changed skills** — fork `iap/pstack-hermes`, commit, install from + your fork's subdir (`hermes plugins install /pstack-hermes/pstack --enable`). - **Add your own new skills alongside** — a skill tap: `hermes skills tap add /skills-repo`, then `hermes skills install /skills-repo/`. @@ -1966,8 +1966,8 @@ def _local_asset_refs(obj): Skills are opt-in on the portable path: load one with `skill_view agent-plugin-pstack-:`, or add this package's `skills/` dir to `skills.external_dirs` in the hermes config for -`/` slash commands. Update anytime with `hermes plugins update pstack`. -Docs: +`/` slash commands. Update with a reinstall: `hermes plugins install iap/pstack-hermes/pstack --force`. +Docs: """ (out / "after-install.md").write_bytes(after_install.encode("utf-8")) st.files_copied += 1 diff --git a/tools/dist_checkout_check.py b/tools/dist_checkout_check.py index 194a5e9..b28b4c6 100644 --- a/tools/dist_checkout_check.py +++ b/tools/dist_checkout_check.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -"""Regression check: a checkout of the DIST tree must be LF-only, with binaries intact. +"""Regression check: a checkout of the package (install clone) must be LF-only, with binaries intact. Why this exists --------------- The package contract is LF-only (see ``tools/validate.py::check_encoding``), and -the dist repo is what ``hermes plugins install iap/pstack`` actually clones. The +the pstack/ subtree is what ``hermes plugins install +iap/pstack-hermes/pstack`` actually clones. The ``build`` CI job validates the *source* ``pstack/`` directory, where files are LF because ``tools/convert.py`` wrote them that way. That job is structurally incapable of catching a line-ending or binary-asset regression introduced by the @@ -15,9 +16,10 @@ from a staged dist tree with ``core.autocrlf=true``, the default on Windows, and assert that what a consumer would receive is byte-correct. -The attributes rule lives in ``tools/assets/dist.gitattributes`` and is copied to -both the staged dist tree and this check, so the publisher and the regression -test cannot drift apart. ``--package`` may be used to verify a real checkout. +For ``--source-tree`` the staged copy is governed by the repository root +``.gitattributes`` - the rules an install clone actually receives. The legacy +dist rule set lives in ``tools/assets/dist.gitattributes``. ``--package`` may +be used to verify a real checkout. Exit codes: 0 every text file checked out LF, every binary byte-identical @@ -119,10 +121,11 @@ def stage_tree(package: Path, dest: Path, *, source_tree: bool = False) -> None: tree_attrs = package / ATTRS_NAME if not tree_attrs.is_file(): if source_tree: - # The source pstack/ tree has no .gitattributes by design: the - # publisher copies one in when it stages the dist repo. Stage a - # throwaway copy so the line-ending question can still be asked. - shutil.copy2(ATTRS_SOURCE, dest / ATTRS_NAME) + # The source pstack/ tree ships no .gitattributes of its own; the + # repository root's rules govern what an install clone receives. + # Stage a throwaway copy with those rules so the line-ending + # question can still be asked. + shutil.copy2(SCRIPT_DIR.parent / ATTRS_NAME, dest / ATTRS_NAME) return raise RuntimeError( f"package has no {ATTRS_NAME}: a published tree must carry the rule, " diff --git a/tools/tests/test_check_pins.py b/tools/tests/test_check_pins.py index 1b618db..3302923 100644 --- a/tools/tests/test_check_pins.py +++ b/tools/tests/test_check_pins.py @@ -15,7 +15,7 @@ SHA = "93b00b89ef425a9c1bac0d0b317dfc49c930ac99" OTHER = "4e5b1cf2ccb0ea3716f08c8ee0a5856b5ab93536" -REQUIRED = ("ci.yml", "release.yml", "publish-plugin.yml", "upstream-drift-watch.yml") +REQUIRED = ("ci.yml", "release.yml", "upstream-drift-watch.yml") def _decl(value: str) -> str: