# Development and release ## Layout | Path | Contents | | --- | --- | | `src/index.ts` | Extension entry point; both hosts load it directly | | `src/tools.ts` | Tool schema, argument construction, pre- and postcondition checks | | `src/argument.ts` | Branch, revision, path, label, and workspace id parsing | | `src/capability.ts` | Session and repository detection | | `src/guidance.ts` | System prompt note added at agent start | | `src/response.ts` | Herdr socket API envelope and worktree payload parsing | | `src/worktree.ts` | `git worktree list --porcelain` parsing, used to verify results | | `src/process.ts` | Child process execution | | `test/*.test.mjs` | Deterministic suites, run by `pnpm test` | | `test/*.e2e.mjs` | Scenarios against real Git, run by `pnpm test:integration` | | `test/fixtures/*.mjs` | The pinned Herdr responses both suites are checked against | Pi loads TypeScript through [jiti](https://github.com/unjs/jiti) and Oh My Pi runs it natively, so `package.json` points both `pi.extensions` and `omp.extensions` at `src/index.ts` and no build step is needed to install from npm, git, or a local path. The `dist/` build exists as a type check and for anyone importing the package directly. `typebox` is a peer dependency. Both hosts bundle it, and a second copy would hand the host a schema it does not recognise. ## Commands ```bash pnpm check # Everything below, in one gate; also run by CI pnpm fix # Apply safe Biome formatting, import, and lint fixes pnpm quality # Check formatting, imports, and lint rules pnpm test # Build, then run the deterministic suite pnpm test:integration # Build, then run the real Git scenarios pnpm deadcode # Find unused files, exports, and dependencies with Knip pnpm package:check # Build and validate the publishable package with publint pnpm security # Audit dependencies for high-severity advisories pnpm release # Prepare, gate, commit, and tag a release ``` The two suites are separated by filename, not by an environment variable, so neither can be skipped without the skip being visible in the run. ## Testing without a Herdr server Herdr's worktree commands need a running server and a terminal session, which CI cannot provide and which a test must not touch even on a developer machine: creating and removing workspaces in a live session changes what the user is looking at. The integration suite therefore puts a stub `herdr` on `PATH`. The stub answers the socket API contract, and does the Git work for real with `git worktree add` and `git worktree remove`. So the argument vocabulary, the JSON envelope parsing, and every postcondition check run against a real repository, while only Herdr's session layer is simulated. The stub is always executable and always first on `PATH`, so no scenario can reach a real Herdr session. Two things keep the stub honest. It carries the option vocabulary again, written out separately, and answers exit 2 for any option outside it, so an option the tool must not send fails the suite instead of being ignored. And `test/fixtures/herdr-responses.mjs` holds one response per operation, recorded from a real Herdr session, with the list of fields the parsers read. The response tests parse the recorded documents, and a scenario checks that the stub answers with those same fields. Herdr sends more than the tool reads and the extras vary between calls, so the check is that list of fields and not the whole document. Each scenario builds its own isolated Git repository. Test files run in parallel processes, so a sandbox directory carries the pid of the process that owns it, and a stale one is only cleared once that process is gone. The scenarios cover: 1. A worktree created, listed, opened, and removed. 2. A worktree created at an explicit path from an explicit base commit. 3. Preconditions rejecting unsafe requests, asserting the worktree list did not change. 4. A declined confirmation leaving the checkout in place. 5. An unsupported Herdr version stopping every operation. 6. A session outside Herdr stopping every operation. 7. A checkout path holding a newline, which must not forge a record in the Git worktree list. 8. A dirty checkout, removed only when `force` is given, with the uncommitted work left alone until then. 9. A call from a subdirectory of the repository. 10. A call from inside a linked worktree, which must still refuse a path in any checkout. 11. A checkout with no workspace, opened by path and then reported as already open. 12. Two creates of one branch at once, leaving exactly one worktree. 13. An aborted call, which must change nothing. 14. A `label` and a `focus` choice arriving at Herdr, and `focus` staying off by default. 15. The stub answering with every field the parsers read from Herdr. The stub's own behaviour is not the contract. After changing anything about argument construction, run the extension against a real Herdr session as well, from a pane where `HERDR_ENV` is `1`. After a Herdr upgrade, re-record the responses in `test/fixtures/herdr-responses.mjs`; its header lists the commands. A renamed field then fails the response tests instead of only failing in production. ## Testing against a real host ```bash pi -e ./src/index.ts omp -e ./src/index.ts ``` Or install the working copy: ```bash pi install /absolute/path/to/pi-herdr-worktree omp plugin link /absolute/path/to/pi-herdr-worktree ``` ## Continuous integration `.github/workflows/ci.yml` runs the full `pnpm check` gate on every push and pull request. The Herdr CLI is not installed there; the integration suite supplies its own stub. ## Release Releases are published by `.github/workflows/release.yml`, triggered by pushing a `v` tag. It runs as three jobs: 1. **verify** checks that the tag matches the version in `package.json`, then runs `pnpm check`. 2. **publish** publishes to npm. It runs in the `npm-publish` environment, so a protection rule there can require manual approval before anything is published. 3. **github-release** creates the GitHub release, using the matching `CHANGELOG.md` section as its notes. To cut a release, run `pnpm release ` from a clean `main`. It refuses to start unless the version is above the current one, the worktree is clean, `main` is checked out, the tag is free, and `CHANGELOG.md` has entries under `## [Unreleased]`. It then: 1. Sets the version in `package.json` and retitles the `Unreleased` section to `## [] - `. 2. Runs `pnpm check`, restoring both files and stopping if the gate fails. 3. Commits `chore: release ` and creates the `v` tag. Pushing stays separate, because that is where the release becomes public: ```bash git push && git push origin v ``` Pass `--push` to have the script do both pushes. Before the tag lands, undo everything with `git tag -d v && git reset --hard HEAD~1`. ### npm authentication Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers): the job requests an OIDC token from GitHub and exchanges it with npm for a short-lived credential. There is no `NPM_TOKEN` secret to store or rotate, and npm attaches a provenance attestation automatically. Two constraints shape the workflow: - npm cannot create a package through trusted publishing, so the first version of a new package must be published manually with `npm login && pnpm publish --access public`. Configure the trusted publisher afterwards, on the package's npm settings page, pointing at this repository, the `release.yml` workflow, and the `npm-publish` environment. - `actions/setup-node` before v7 wrote a placeholder auth token that made npm skip the OIDC exchange, so the workflow pins v7. The publish job holds the OIDC token, so it is kept small: no dependency cache, and `pnpm install --frozen-lockfile --ignore-scripts` so no dependency lifecycle script runs beside the credential. The test suite runs in the separate verify job, which has no token. ## Ecosystem listings Publishing to npm is all either ecosystem needs: - **Pi** lists any package carrying the `pi-package` keyword in its gallery at [pi.dev/packages](https://pi.dev/packages). There is no submission step. - **Oh My Pi** installs npm packages directly with `omp plugin install`, and reads the catalog at `.omp-plugin/marketplace.json` in this repository for `/marketplace add mcuste/pi-herdr-worktree`. That catalog tracks the `main` branch; point its `ref` at a tag to pin marketplace users to releases.