@jakobm/peacekeeper 0.0.0-stage → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakob Mattsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,383 @@
1
- # Temporary Holding Version
1
+ # Peacekeeper
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node.js scripts for Codex Git worktree hooks, merged branch cleanup, and cached test receipts. The package requires Node.js 20 or newer and Git. Each calling project supplies its own configuration at its Git root.
4
+
5
+ The public npm package is `@jakobm/peacekeeper`; its CLI command is `peacekeeper`.
6
+ Install it with `npm install --save-exact @jakobm/peacekeeper@<version>`, selecting
7
+ a published version. Replace the example dependency pin below with that version.
8
+
9
+ When moving an existing project to Peacekeeper, update its dependency and lockfile,
10
+ package scripts, and configuration filename together. Replace previous lifecycle
11
+ hook commands with the definitions below before running `peacekeeper install-hooks`.
12
+ The installer recognizes Peacekeeper commands; it does not translate commands
13
+ from a differently named package. Each existing worktree needs the new dependency
14
+ and configuration. Test receipts start fresh under the new package identity;
15
+ finish any pending release before switching release tools.
16
+
17
+ ## Recommended caller setup
18
+
19
+ Add the three files below to the calling project. Replace the Git branch settings and test commands with that project's values. Install the package from npm; no GitHub SSH access is needed for the dependency.
20
+
21
+ ### 1. `package.json`
22
+
23
+ Include these entries in the caller's `package.json`, alongside its other scripts and dependencies. Pin the npm dependency to an exact version and change it deliberately when upgrading. The `lint` script and ESLint dependency are needed only for the `eslint .` test command in this example.
24
+
25
+ ```json
26
+ {
27
+ "private": true,
28
+ "scripts": {
29
+ "lint": "eslint .",
30
+ "test": "peacekeeper run-tests",
31
+ "cleanup:branches": "peacekeeper cleanup-merged-branches",
32
+ "cleanup:empty-directories": "peacekeeper remove-empty-directories"
33
+ },
34
+ "devDependencies": {
35
+ "eslint": "^9.39.5"
36
+ },
37
+ "dependencies": {
38
+ "@jakobm/peacekeeper": "0.12.1"
39
+ }
40
+ }
41
+ ```
42
+
43
+ Run `npm install` from the project root and commit the resulting `package-lock.json`. npm adds `node_modules/.bin` to the `PATH` of package scripts, so package scripts can call `peacekeeper` by name.
44
+
45
+ ### 2. `peacekeeper.config.json`
46
+
47
+ Place this required file at the caller's Git root. All fields shown here are required and unknown fields are rejected. `git.checkpointPushRemotes`, `git.releaseMergeMethod`, `git.releaseRequiredChecks`, and `git.releaseTestWorkflow` are optional fields described below; release commands require `git.releaseRequiredChecks`. Optional `projectChecks` settings are described under local project checks. There are no built-in test-command defaults: each caller must define `tests.commands` explicitly for its own toolchain.
48
+
49
+ ```json
50
+ {
51
+ "git": {
52
+ "remote": "origin",
53
+ "defaultBranch": "main",
54
+ "branchPrefix": "codex/"
55
+ },
56
+ "tests": {
57
+ "commands": ["eslint .", "node --test"],
58
+ "receiptMaxAgeMinutes": 60,
59
+ "npmTestUsesRunner": true
60
+ },
61
+ "loggingDirectory": "tmp/logs"
62
+ }
63
+ ```
64
+
65
+ `git.remote` and `git.defaultBranch` identify the branch used for checkpointing and cleanup. `git.branchPrefix` names new task branches. `tests.commands` are complete shell commands, run in order from the caller's Git root; install any executables they use. The test runner stops on the first failure and records a receipt only after every command succeeds. `receiptMaxAgeMinutes` controls how long a receipt can be reused. Set `npmTestUsesRunner` to `true` only when the caller's `npm test` script invokes `peacekeeper run-tests`; this makes the rerun hint `npm test -- rerun`. Otherwise the hint uses the explicit package path. `loggingDirectory` stores hook invocation logs relative to the primary Git worktree's root.
66
+
67
+ To publish checkpoint branches automatically, add `checkpointPushRemotes` to `git`:
68
+
69
+ ```json
70
+ {
71
+ "remote": "origin",
72
+ "defaultBranch": "main",
73
+ "branchPrefix": "codex/",
74
+ "checkpointPushRemotes": ["origin", "backup"]
75
+ }
76
+ ```
77
+
78
+ Each name must identify a Git remote configured in the calling repository. With this
79
+ setting, checkpoints push to exactly those remotes, creating the task branch if
80
+ needed. An empty list disables checkpoint pushes. When the field is omitted,
81
+ checkpoints keep the previous behavior: push only when the branch already exists
82
+ on `git.remote`. Pushes never use force. Only a push to `git.remote` sets the
83
+ branch's upstream. If a push fails, earlier pushes are kept and the checkpoint
84
+ records a failure; fix the cause and rerun the checkpoint command to retry without
85
+ creating another commit when there are no new changes.
86
+
87
+ Receipts are stored outside the repository under `peacekeeper-test-receipts` in Node.js's `os.tmpdir()`. Each Git root has a separate receipt file. Receipt validation fingerprints changed files and the process environment. In CI, the runner always runs the configured commands instead of reusing a receipt.
88
+
89
+ Starting a new test run invalidates any previous receipt, including when a forced
90
+ rerun fails. The runner compares repository contents and the test environment
91
+ before and after the commands. If either changes during the run, it exits with
92
+ an error and records no receipt; rerun the tests on the resulting contents.
93
+ Test-generated files should be ignored or written outside the repository.
94
+
95
+ ### 3. `.codex/hooks.json`
96
+
97
+ Install or update the project hooks from the caller's Git worktree:
98
+
99
+ ```sh
100
+ npm exec --no -- peacekeeper install-hooks --dry-run
101
+ npm exec --no -- peacekeeper install-hooks
102
+ ```
103
+
104
+ The command creates or updates `.codex/hooks.json` at the Git root. It preserves
105
+ other handlers, their order, group settings, and other configuration fields.
106
+ Known Peacekeeper invocations are replaced with current definitions and
107
+ duplicates are removed. Our hooks are installed in their own groups, without
108
+ inheriting another group's matcher. Repeating the command makes no further change.
109
+
110
+ Supported migrations include standalone `peacekeeper`, `npm exec --no --`,
111
+ `npx --no-install`, documented `node` bin paths, and direct `node scripts/` hook
112
+ invocations. Unknown wrappers, composed shell commands, and additional handler
113
+ options are not replaced automatically. If an entry appears related to Peacekeeper
114
+ but cannot be safely migrated, the command exits with an error identifying
115
+ it and leaves the entire file unchanged. Review that entry, preserve any custom
116
+ logic as a separate hook, then rerun the installer. Wrappers whose names do not
117
+ identify Peacekeeper cannot be detected; review those yourself before installing.
118
+
119
+ The installer writes atomically and rejects malformed files. It changes only
120
+ `.codex/hooks.json`, not `AGENTS.md`, project configuration, or hooks in other
121
+ configuration layers. Configure Git remotes, tests, and dependencies separately,
122
+ and review and trust the installed hooks with `/hooks` in Codex.
123
+
124
+ The installed file contains these definitions (other existing hooks are retained):
125
+
126
+ ```json
127
+ {
128
+ "description": "Repository lifecycle hooks.",
129
+ "hooks": {
130
+ "SessionStart": [
131
+ {
132
+ "hooks": [
133
+ {
134
+ "type": "command",
135
+ "command": "node \"$(git rev-parse --show-toplevel)/node_modules/@jakobm/peacekeeper/bin/peacekeeper.js\" checkpoint-codex-session-start",
136
+ "timeout": 30,
137
+ "statusMessage": "Loading Codex checkpoint instructions"
138
+ }
139
+ ]
140
+ }
141
+ ],
142
+ "Stop": [
143
+ {
144
+ "hooks": [
145
+ {
146
+ "type": "command",
147
+ "command": "node \"$(git rev-parse --show-toplevel)/node_modules/@jakobm/peacekeeper/bin/peacekeeper.js\" checkpoint-codex-stop",
148
+ "timeout": 30,
149
+ "statusMessage": "Checking for an uncommitted Codex checkpoint"
150
+ }
151
+ ]
152
+ }
153
+ ],
154
+ "SessionEnd": [
155
+ {
156
+ "hooks": [
157
+ {
158
+ "type": "command",
159
+ "command": "node \"$(git rev-parse --show-toplevel)/node_modules/@jakobm/peacekeeper/bin/peacekeeper.js\" cleanup-ended-session",
160
+ "timeout": 3,
161
+ "statusMessage": "Scheduling merged branch cleanup"
162
+ }
163
+ ]
164
+ }
165
+ ]
166
+ }
167
+ }
168
+ ```
169
+
170
+ The commands resolve the package from the current Git worktree. Install dependencies in each worktree where the hooks will run; the commands need that worktree's `node_modules/@jakobm/peacekeeper`. The checkpoint hooks apply only to linked worktrees. `SessionStart` supplies the checkpoint instructions before work begins, including on resume, fork, and after compaction; no matcher is needed. `Stop` uses the same package-owned instructions as a fallback. You do not need to copy checkpoint rules into each project's `AGENTS.md`. Explicit user instructions not to commit or push take precedence: the agent must skip the checkpoint command when they conflict, even if `Stop` requests it. The hook checks Git state and cannot determine user intent itself. When uncommitted changes remain, the `Stop` hook asks the agent to review the diff and run `checkpoint-codex-changes` with a descriptive English commit message. That command stages all changes and commits them on a task branch; it refuses the configured default branch. With `git.checkpointPushRemotes`, it pushes without force to each listed remote, including new branches. Without that setting, it pushes only if the branch already exists on `git.remote`, otherwise the branch stays local.
171
+
172
+ `SessionEnd` starts a detached worker to clean up the session worktree and branch only if the captured branch tip is still unchanged and fully merged into the remote default branch. Dirty or locked worktrees are kept. Worker output is appended to `cleanup-ended-session.log` in the shared Git directory. Cleanup does not require the primary worktree to have the default branch checked out. A local default branch that can fast-forward is updated before cleanup: a branch not checked out in any worktree is moved atomically, while a checked-out branch is merged in its clean worktree so its files and index stay synchronized. A dirty or diverged default branch is left unchanged. Codex loads project hooks only after the project hook source is trusted; review them with `/hooks`. The three-second `SessionEnd` timeout is the maximum documented for that event. See the [Codex hooks reference](https://learn.chatgpt.com/docs/hooks) for hook loading, trust, and timeout behavior.
173
+
174
+ ### Adopting shared checkpoint instructions
175
+
176
+ For a project that already uses the Stop hook, upgrade to a package release that
177
+ includes `checkpoint-codex-session-start` and `install-hooks`, then run the installer above.
178
+ The installer also updates `Stop` and `SessionEnd`. Review and trust the updated hooks with `/hooks`,
179
+ then start a new session or resume one to load the instructions. The pinned package version must include these commands.
180
+
181
+ Remove the duplicated checkpoint section from the project's `AGENTS.md` once the
182
+ new hook is enabled. Keep any project-specific rules there. Installing the npm
183
+ dependency alone does not activate hooks: each caller must configure and trust
184
+ them. The shared policy then follows the installed package version.
185
+
186
+ ## Check the setup
187
+
188
+ From the caller's Git worktree, run `npm test` for the configured commands, `npm test -- rerun` to bypass a reusable receipt, `npm run cleanup:branches` to preview merged branch cleanup, and `npm run cleanup:empty-directories` to preview removal of empty directories and directories containing only ignored files. The latter command protects the root `.git`, `node_modules`, and `tmp` directories; other selected directories are removed recursively, including their ignored contents. Add `-- --execute` to either cleanup package script to apply it. Run `npm exec --no -- peacekeeper --help` for the complete command list. To create a manual checkpoint, use `npm exec --no -- peacekeeper checkpoint-codex-changes --subject "<subject>" --body "<body paragraph>"`.
189
+
190
+ Directory cleanup also preserves nested Git repositories and linked worktrees,
191
+ including their parent directories when those parents are ignored. It does not
192
+ clean inside those repositories. Branch cleanup rechecks local branch tips and
193
+ worktree identity, locks, and changes before removal; a changed plan is skipped
194
+ before deleting the affected worktree or branches. Remote branch deletion still
195
+ uses an exact-commit lease.
196
+
197
+ This repository uses the same hooks and configuration, with commands pointing to
198
+ its own `bin/peacekeeper.js` instead of an installed copy. Its `git.checkpointPushRemotes` is set to `["origin"]` to publish checkpoint branches automatically. There is no dependency
199
+ on a published version of itself. `npm exec --no -- peacekeeper` also resolves
200
+ the root package's own bin, including the checkpoint command requested by the
201
+ Stop hook. Install dependencies with `npm ci` to run the full test suite.
202
+
203
+ Run `npm test` in this repository to verify the shared package itself, or
204
+ `npm test -- rerun` to force a fresh run. Its configured test command runs Node's
205
+ test runner directly, avoiding recursion through `npm test`. The cleanup package
206
+ scripts work the same way as in calling projects.
207
+
208
+ When first adopting these hooks, make sure the primary checkout also contains
209
+ `peacekeeper.config.json` before trusting them with `/hooks`: invocation logs
210
+ are stored there and session cleanup reads that checkout's configuration.
211
+ The hook definitions can then be reviewed and trusted in Codex; committing them
212
+ does not grant trust automatically.
213
+
214
+ ## Check project conventions locally
215
+
216
+ Run `peacekeeper check-project` to check the local project configuration.
217
+ Run `peacekeeper check-project --explain` for the complete expected
218
+ architecture, requirements, configuration examples, and verification limits.
219
+ The command prints the bundled [formal requirements](scripts/check-project-requirements.txt),
220
+ which can also be read without a repository or project configuration.
221
+
222
+ ## Publish an npm release
223
+
224
+ Install [Volta](https://volta.sh/) and ensure its shims are first in `PATH`
225
+ (`volta setup` configures supported shell profiles; restart your shell afterward).
226
+ Then run `npm ci` at the repository root. Volta automatically selects the exact
227
+ Node.js and npm versions in `package.json` for ordinary `node`, `npm`, and `npx`
228
+ commands, including in non-interactive agent shells with Volta on `PATH`.
229
+ GitHub Actions uses `volta-cli/action` to select the same project versions.
230
+ Check `volta which node`, `node --version`, and `npm --version` to verify your
231
+ setup. Use `volta pin node@<version> npm@<version>` to update the project versions;
232
+ `engines.node` separately describes supported consumer versions.
233
+ Review `npm pack --dry-run --json` before
234
+ publishing; npm includes only `bin/`, `scripts/`, the README, and package metadata.
235
+
236
+ Configure npm trusted publishing for `@jakobm/peacekeeper` with repository owner
237
+ `jakobmattsson`, repository `peacekeeper`, and workflow filename `package.yml`,
238
+ with direct publishing enabled. Confirm npm publish access to the `@jakobm`
239
+ scope separately from GitHub repository access. The workflow verifies
240
+ the release and publishes via OIDC. It needs no npm access token.
241
+
242
+ `test.yml` runs `test` and `Test package` on pull requests. `package.yml` runs
243
+ on `v*` tags and only verifies and publishes. Merging to `main` does not start
244
+ another run.
245
+
246
+ Release verification uses GitHub's enforced branch policy instead of retaining
247
+ artifacts of tested contents. Configure `git.releaseRequiredChecks` in the root
248
+ `peacekeeper.config.json` with the exact CI job names required for a release
249
+ (for this repository: `["test", "Test package"]`). These checks must be mandatory
250
+ with **Require branches to be up to date before merging** enabled. Classic branch
251
+ protection must enforce checks for administrators too. Alternatively, active
252
+ rulesets must require strict checks and have no bypass actors. Rules inherited
253
+ from the organization are included. Merge queues are not currently an alternative
254
+ to this strict policy.
255
+
256
+ The PR test job runs `peacekeeper verify-release-protection` before its tests.
257
+ The command checks `GITHUB_BASE_REF`, or `git.defaultBranch` outside PR CI.
258
+ `peacekeeper release` checks the default branch before preparing a release
259
+ and again immediately before each merge attempt. Before publishing,
260
+ `peacekeeper verify-release` checks the policy, matching package/tag/lockfile
261
+ versions, a same-repository merged `release/v<version>` PR, and successful required
262
+ jobs in its latest PR workflow run. Manual tags without that PR are rejected.
263
+ `verify-release-version` remains available for local version checks alone.
264
+
265
+ API failures, missing permissions, and unverifiable rules stop the commands.
266
+ The workflow expects a `RELEASE_POLICY_TOKEN` Actions secret, exposed to the
267
+ commands as `GH_TOKEN`. Use a fine-grained PAT or GitHub App token with access to
268
+ this repository and Administration read, Contents read, Pull requests read and
269
+ Actions read permissions. GitHub only exposes ruleset bypass actors to callers
270
+ with write access to the ruleset; ruleset users must additionally grant that
271
+ access (Administration write for repository rulesets, or the appropriate access
272
+ for inherited organization rulesets). Missing bypass information also fails.
273
+ The normal workflow `GITHUB_TOKEN` cannot be granted Administration permissions
274
+ through workflow YAML. After correcting access or rules, rerun the failed job.
275
+ Fork PRs do not receive this secret and fail the policy check; do not expose it
276
+ by executing fork code with `pull_request_target`.
277
+
278
+ To set this up for this repository:
279
+
280
+ 1. Open the [token creation form with the required read permissions prefilled](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=read&contents=read&pull_requests=read&actions=read).
281
+ For repository rulesets, use the [form with Administration write instead](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=write&contents=read&pull_requests=read&actions=read)
282
+ so GitHub exposes bypass actors. For another repository, change the resource
283
+ owner. Select **Only select repositories**, choose `peacekeeper`, review
284
+ the permissions and expiration, and generate the token. If the owner requires
285
+ approval, wait until the token is approved.
286
+ 2. Save the token as an Actions secret with the following command. `gh` prompts
287
+ for the value interactively; paste the token at the prompt, keeping it out of
288
+ the command and shell history. The `gh` account running this command needs
289
+ permission to manage repository Actions secrets. The policy token itself
290
+ does not need Secrets permission.
291
+
292
+ ```sh
293
+ gh secret set RELEASE_POLICY_TOKEN --repo jakobmattsson/peacekeeper
294
+ ```
295
+
296
+ 3. Confirm the secret name exists, then find and rerun the failed test workflow:
297
+
298
+ ```sh
299
+ gh secret list --repo jakobmattsson/peacekeeper
300
+ gh run list --repo jakobmattsson/peacekeeper --workflow test.yml --limit 5
301
+ gh run rerun RUN_ID --failed --repo jakobmattsson/peacekeeper
302
+ ```
303
+
304
+ Replace `RUN_ID` with the run ID from the listing. To retry publication, list
305
+ runs with `--workflow package.yml` instead. When the token expires, create a
306
+ replacement and run `gh secret set` again to update the same secret.
307
+
308
+ GitHub requires fine-grained PAT creation in the browser; `gh` can store the
309
+ resulting token as a secret but cannot create that PAT. See [GitHub's token setup
310
+ documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#pre-filling-fine-grained-personal-access-token-details-using-url-parameters)
311
+ and the [`gh secret set` reference](https://cli.github.com/manual/gh_secret_set).
312
+
313
+ This policy trusts GitHub to enforce the checked settings when merging. It does
314
+ not retain historical proof of the tested file tree if administrators change
315
+ settings between CI and merge. Keep the `package.yml` filename unchanged: npm
316
+ trusted publishing is configured for that workflow file.
317
+
318
+ Release merge selection reads the calling repository's root
319
+ `peacekeeper.config.json`, not the installed package's configuration:
320
+
321
+ - If GitHub allows only one merge method, that method is used regardless of the preference.
322
+ - If multiple methods are allowed, optional `git.releaseMergeMethod` selects
323
+ `"merge"` (merge commit), `"rebase"`, or `"squash"`. A configured method that
324
+ GitHub disallows causes the release to stop before creating a branch or PR.
325
+ - Without a preference, merge commit is chosen first, then rebase.
326
+
327
+ For example, add `"releaseMergeMethod": "rebase"` inside the existing `git` object.
328
+ A caller without a merge preference uses the same defaults.
329
+ The chosen method is saved with release state and reused on retries; unfinished
330
+ releases created by older versions retain their original squash behavior.
331
+
332
+ From a clean repository root, run `npm run release -- patch`, `minor`, or `major`.
333
+ The command fetches the default branch and uses a temporary detached worktree to
334
+ increment the package and lockfile versions. Your current branch and local default
335
+ branch stay unchanged, including when the default branch is checked out elsewhere.
336
+ It pushes a `release/v<version>` branch and opens a release PR. The merge method
337
+ follows the calling repository's configuration and GitHub settings. The command
338
+ waits up to 20 minutes for GitHub to accept the merge,
339
+ respecting required checks, reviews, resolved conversations, and branch freshness.
340
+ It never uses an administrator bypass or pushes directly to the default branch.
341
+
342
+ After merge, the command verifies the package files at the PR's actual merge
343
+ commit, tags that commit with `v<version>`, and pushes the tag. The existing
344
+ workflow publishes through npm trusted publishing. The command then waits for
345
+ the publish job and for the version to appear on npm. Git and an authenticated
346
+ GitHub CLI (`gh`) with Contents read/write, Pull requests read/write, Actions
347
+ read and the policy-reading access described above are required. Registry checks
348
+ use a temporary npm cache and request current metadata.
349
+
350
+ Retry the same command and release type after an interruption: release state is
351
+ stored in `peacekeeper-release.json` in the shared Git directory, so retries
352
+ from another worktree of the same checkout continue the same PR and version.
353
+ Already-merged PRs and already-pushed matching tags are reused. A local lock
354
+ prevents concurrent commands in that checkout; an open release PR from a different
355
+ checkout blocks a new release. Do not delete the state or manually create a new
356
+ version commit or tag to retry. If publishing failed, fix the cause and rerun the
357
+ tag workflow on GitHub before retrying the command.
358
+
359
+ If merge requirements remain unmet, the command stops without tagging. Resolve
360
+ those requirements and retry. Conflicts, a changed PR head, or a changed base
361
+ require inspection before continuing; the command refuses to silently publish
362
+ an altered release. Temporary worktrees are removed when the command exits
363
+ normally or with a handled error; a killed process may leave a temporary worktree
364
+ that needs manual cleanup.
365
+
366
+ Other npm projects can use this command through a package script such as
367
+ `"release": "peacekeeper release"`. The project must use a root package
368
+ with a stable version, a `peacekeeper.config.json` with
369
+ `git.releaseRequiredChecks`, and the supported tag-triggered GitHub Actions
370
+ publish job. Add `peacekeeper verify-release-protection` to a required PR
371
+ test job and `peacekeeper verify-release` before publishing, with `GH_TOKEN`
372
+ and `RELEASE_TAG` set for release verification. Both commands are shipped with
373
+ the package; no verifier script or artifact upload needs to be copied.
374
+ Set optional `git.releaseTestWorkflow` to the PR workflow filename (for this
375
+ repository: `"test.yml"`). If omitted, verification uses the publish workflow
376
+ filename for compatibility with combined workflows. All configured check names
377
+ must be jobs in the selected PR workflow's run.
378
+ The `publish` job must contain a step whose command is exactly `npm publish`.
379
+ Its `if` can be omitted when the workflow triggers only on pushes of
380
+ `v`-prefixed tags (for example, `on.push.tags: ['v*']`), with no branch triggers.
381
+ For combined workflows or other triggers, the job must use the exact condition
382
+ `${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}`.
383
+ Explicit alternative job conditions are not supported. Workspaces are not supported.
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+ const path = require('node:path');
5
+
6
+ const COMMANDS = new Map([
7
+ ['check-project', 'Validate local toolchain, hooks, CI and publication conventions.'],
8
+ ['install-hooks', 'Install or update project Codex hooks while preserving other hooks.'],
9
+ ['verify-release', 'Verify release versions, required CI and strict branch protection.'],
10
+ ['verify-release-protection', 'Require strict mandatory release checks without bypass.'],
11
+ ['verify-release-version', 'Check a release tag against package and lockfile versions.'],
12
+ ['release', 'Publish a version through a GitHub Actions npm workflow.'],
13
+ ['run-tests', 'Run configured tests, reusing a valid test receipt.'],
14
+ ['check-test-receipt', 'Check whether a valid test receipt exists.'],
15
+ ['record-test-receipt', 'Record a test receipt without running tests.'],
16
+ ['checkpoint-codex-changes', 'Commit a linked worktree and push according to project configuration.'],
17
+ ['checkpoint-codex-session-start', 'Load checkpoint instructions for a linked Codex worktree.'],
18
+ ['checkpoint-codex-stop', 'Handle the Codex Stop hook and request a checkpoint if needed.'],
19
+ ['cleanup-ended-session', 'Queue cleanup for a merged Codex session worktree.'],
20
+ ['queue-merged-worktree-cleanup', 'Queue cleanup for the current merged worktree.'],
21
+ ['cleanup-merged-branches', 'Preview or delete merged worktrees and branches.'],
22
+ ['remove-empty-directories', 'Preview or remove directories containing only ignored files.'],
23
+ ]);
24
+
25
+ function helpText() {
26
+ const width = Math.max(...[...COMMANDS.keys()].map((command) => command.length));
27
+ const commands = [...COMMANDS]
28
+ .map(([command, description]) => ` ${command.padEnd(width)} ${description}`)
29
+ .join('\n');
30
+ return `Usage: peacekeeper <command> [arguments]\n\nCommands:\n${commands}\n\nOptions:\n -h, --help Show this help.\n`;
31
+ }
32
+
33
+ function main() {
34
+ const [command, ...args] = process.argv.slice(2);
35
+ if (command === '--help' || command === '-h') {
36
+ process.stdout.write(helpText());
37
+ return;
38
+ }
39
+ if (!COMMANDS.has(command)) {
40
+ const detail = command ? `Unknown command: ${command}` : 'A command is required.';
41
+ process.stderr.write(`${detail}\n\n${helpText()}`);
42
+ process.exitCode = 2;
43
+ return;
44
+ }
45
+ const script = path.resolve(__dirname, '..', 'scripts', `${command}.js`);
46
+ const result = spawnSync(process.execPath, [script, ...args], {
47
+ cwd: process.cwd(),
48
+ stdio: 'inherit',
49
+ });
50
+ if (result.error) throw result.error;
51
+ process.exitCode = result.status ?? 1;
52
+ }
53
+
54
+ try {
55
+ main();
56
+ } catch (error) {
57
+ console.error(`Error: ${error.message}`);
58
+ process.exitCode = 1;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@jakobm/peacekeeper",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.12.2",
4
+ "description": "Shared repository test, Git worktree, and Codex hook commands",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jakobmattsson/peacekeeper.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org/"
13
+ },
14
+ "bin": {
15
+ "peacekeeper": "bin/peacekeeper.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "scripts/"
20
+ ],
21
+ "scripts": {
22
+ "pretest": "node bin/peacekeeper.js check-project",
23
+ "test": "node bin/peacekeeper.js run-tests",
24
+ "release": "node bin/peacekeeper.js release",
25
+ "cleanup:branches": "node bin/peacekeeper.js cleanup-merged-branches",
26
+ "cleanup:empty-directories": "node bin/peacekeeper.js remove-empty-directories"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "dependencies": {
32
+ "semver": "^7.8.5",
33
+ "yaml": "^2.9.1"
34
+ },
35
+ "volta": {
36
+ "node": "22.14.0",
37
+ "npm": "11.21.0"
38
+ }
39
+ }