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
26 changes: 16 additions & 10 deletions .changeset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

> Release intent store and automation contract for `@systemfsoftware/claude-code-comment-checker`.

This directory holds change-intent files consumed by the release pipeline on pushes to `master`. Every consumer-observable change must record its intent here so the automated release pipeline can bump package versions, generate changelogs, and publish platform binaries.
This directory holds change-intent files consumed by the shared release
toolchain ([systemfsoftware/pnpm-release-management](https://github.com/systemfsoftware/pnpm-release-management))
on pushes to `master`. Every consumer-observable change must record its intent
here so the automation can bump the version surfaces, generate changelogs, tag
the release, and cut its GitHub Release. `release.jsonc` at the repository root
configures the toolchain.

```mermaid
flowchart TD
Push[Push to master] --> Plan[plan-release.ts]
Plan -->|Pending .changeset/*.md| Version[phase: version<br/>release-version.ts updates package.json + CHANGELOG.md<br/>Opens changeset-release/master PR]
Plan -->|Untagged manifest version| Publish[phase: publish<br/>Builds platform binaries & publishes npm package<br/>Tags Git release vX.Y.Z]
Push[Push to master] --> Plan[release plan]
Plan -->|Pending .changeset/*.md| Version[phase: version<br/>bump every version surface + write changelogs<br/>Open changeset-release/master PR]
Plan -->|Untagged manifest version| Release[phase: release<br/>Tag vX.Y.Z + cut GitHub Release]
Plan -->|No intents & version already tagged| None[phase: none<br/>No-op]
```

Expand All @@ -32,21 +37,22 @@ Single paragraph in consumer voice explaining what is now observable or fixed.

## Intent Rules

- **Scope includes Rust core changes:** A PR that touches the Rust binary (`crates/comment-checker`) **must** include an intent. The crate compiles into the binary executed by the published npm launcher package; a change in the crate is directly observable by the package consumer.
- **Scope includes Rust core changes:** A PR that touches the Rust binary (`crates/comment-checker`) **must** include an intent. The crate compiles into the binary the launcher spawns; a change in the crate is directly observable by the consumer.
- **Consumer voice:** Describe what the user of the hook or package observes. Never cite internal file paths, pull request numbers, or test names.
- **Single paragraph body:** The release script ([`scripts/tools/release-version.ts`](../scripts/tools/release-version.ts)) joins all lines in the summary body with spaces into a single changelog bullet item. Do not use multi-paragraph text or markdown sub-bullets.
- **Single paragraph body:** The toolchain joins all lines in the summary body with spaces into a single changelog bullet. Do not use multi-paragraph text or markdown sub-bullets.
- **`--bump none` for internal maintenance:** Use `none` only when no observable behavior changed (e.g., devDependency bumps, script edits, workflow refactoring).

## Release Pipeline Contract

Release automation is state-driven and runs on push to `master`:
Release automation is state-driven and runs on push to `master`; the phase is
derived from repository state, not from a pull-request ref:

1. **`phase: version`** — When pending intents exist in `.changeset/`, [`.github/workflows/release.yml`](../.github/workflows/release.yml) executes [`scripts/tools/release-version.ts`](../scripts/tools/release-version.ts), deletes the consumed intents, updates [`npm/packages/comment-checker/package.json`](../npm/packages/comment-checker/package.json) and [`npm/packages/comment-checker/CHANGELOG.md`](../npm/packages/comment-checker/CHANGELOG.md), and creates/updates a release pull request (`changeset-release/master`).
2. **`phase: publish`** — Merging the release PR updates `package.json` on `master` with an untagged version. The subsequent push to `master` enters the publish phase: `release.yml` builds cross-platform artifacts, attaches provenance attestations, publishes to npm, and creates the GitHub tag `vX.Y.Z`.
1. **`phase: version`** — When pending intents exist in `.changeset/`, the toolchain bumps every version surface declared in `release.jsonc`, writes the changelogs, deletes the consumed intents, and creates or updates the release pull request (`changeset-release/master`).
2. **`phase: release`** — Merging the release PR lands an untagged version on `master`. The next push tags `vX.Y.Z` and creates its GitHub Release from the authored changelog. Distribution is this repository's Nix flake (built from source) at the tag — nothing is published to a registry.
3. **`phase: none`** — When all intents are consumed and the current manifest version is already tagged, the pipeline exits clean with nothing to do.

> [!WARNING]
> Merging a pull request without an intent means `plan-release.ts` sees `phase: none`. The changes land on `master` but will never be published to npm or tagged as a release.
> Merging a pull request without an intent leaves the plan at `phase: none`. The changes land on `master` but are never tagged or released.

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions .changeset/unify-release-tooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/claude-code-comment-checker": none
---

Move releases onto the shared toolchain and drop npm-registry publishing; no change to the package's observable behavior.
12 changes: 0 additions & 12 deletions .github/actions/setup-pnpm-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ inputs:
description: Node version
required: false
default: '24'
registry-url:
description: npm registry URL for setup-node; omit when empty
required: false
default: ''

runs:
using: composite
Expand All @@ -21,14 +17,6 @@ runs:
version: 11.21.0

- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
if: inputs.registry-url == ''
with:
node-version: ${{ inputs.node-version }}
cache: pnpm

- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
if: inputs.registry-url != ''
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
cache: pnpm
18 changes: 18 additions & 0 deletions .github/workflows/changeset-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Thin caller. A pull request that changes a publishable package must carry a
# .changeset intent naming it; the shared toolchain
# (systemfsoftware/pnpm-release-management) enforces that against release.jsonc.
name: Changeset Check

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: read

jobs:
check:
uses: systemfsoftware/pnpm-release-management/.github/workflows/changeset-check.yml@prm/toolchain
with:
tools-ref: prm/toolchain
168 changes: 10 additions & 158 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,168 +1,20 @@
# Release pipeline. Both phases hang off pushes to master and are selected by
# repository state, never by a pull-request ref.
#
# The publish used to trigger on `pull_request: closed` for the release PR.
# Merging that PR with branch deletion destroys refs/pull/<n>/merge, so GitHub
# cancelled the queued run with zero jobs and nothing published -- the trigger
# was destroyed by the act of merging, silently. plan-release.ts reads durable
# state instead: pending .changeset intents mean "version", an untagged
# manifest version means "publish". A half-finished release resumes on the next
# push because the missing tag still says "publish".
#
# Platform build/stage/bundle lives in platform.yml. This file only decides
# when to call it.
# Thin caller. The release lifecycle lives in the shared toolchain
# (systemfsoftware/pnpm-release-management); this file supplies only the trigger
# and the permissions. The phase decision, the version bump, the tagging and
# the GitHub Releases all run inside the reusable workflow against release.jsonc.
# tools-ref pins the toolchain revision the release runs from.
name: Release

on:
push:
branches: [master]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions: {}
permissions:
contents: write
pull-requests: write

jobs:
plan:
name: plan
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
phase: ${{ steps.plan.outputs.phase }}
version: ${{ steps.plan.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
fetch-tags: true

- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2

- id: plan
name: Decide release phase from repository state
run: ./scripts/tools/plan-release.ts >> "$GITHUB_OUTPUT"

version:
needs: plan
if: needs.plan.outputs.phase == 'version'
name: version · open release PR
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# checkout v5 defaults persist-credentials to false; the release PR
# branch is pushed with plain git, which needs the stored credential.
persist-credentials: true

- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2

- name: Consume pending change intents
run: ./scripts/tools/release-version.ts

- name: Open or update the Release PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/tools/create-or-update-release-pr.ts

rust-gate:
needs: plan
if: needs.plan.outputs.phase == 'publish'
uses: ./.github/workflows/rust-gate.yml
permissions:
contents: read

js-gate:
needs: plan
if: needs.plan.outputs.phase == 'publish'
uses: ./.github/workflows/js-gate.yml
permissions:
contents: read

tools:
needs: plan
if: needs.plan.outputs.phase == 'publish'
uses: ./.github/workflows/tools.yml
permissions:
contents: read

mutation:
needs: plan
if: needs.plan.outputs.phase == 'publish'
uses: ./.github/workflows/mutation.yml
permissions:
contents: read

release:
needs: [plan, rust-gate, js-gate, tools, mutation]
if: needs.plan.outputs.phase == 'publish'
uses: ./.github/workflows/platform.yml
uses: systemfsoftware/pnpm-release-management/.github/workflows/release.yml@prm/toolchain
with:
mode: release
permissions:
contents: read

publish:
needs: [plan, release, rust-gate, js-gate, tools, mutation]
if: needs.plan.outputs.phase == 'publish'
name: publish · oidc · tag
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# checkout v5 defaults persist-credentials to false; tag-released-packages
# pushes tags with plain git, which needs the stored credential.
persist-credentials: true

- uses: ./.github/actions/setup-pnpm-node
with:
registry-url: https://registry.npmjs.org

- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2

- name: Preflight — every package must already exist on npm
run: ./scripts/tools/check-publish.ts --preflight

- name: Build launcher
run: |
pnpm install --frozen-lockfile --registry https://registry.npmjs.org
pnpm -r build

- name: Sync root version
run: ./scripts/tools/sync-root-version.ts

- name: Publish root launcher
run: pnpm --filter @systemfsoftware/claude-code-comment-checker publish --provenance --access public --no-git-checks

- name: Download staged platform packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: platform-stage-*
path: stages

- name: Publish platform packages
run: ./scripts/tools/publish-platform-stages.ts

- name: Tag released packages
run: ./scripts/tools/tag-released-packages.ts

- name: Download platform artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-*
path: release-assets

- name: Create GitHub release with binaries and changelog
run: ./scripts/tools/create-github-release.ts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tools-ref: prm/toolchain
17 changes: 0 additions & 17 deletions .github/workflows/tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ jobs:
runs-on: ${{ matrix.os }}
permissions:
contents: read
env:
# gh does not read GITHUB_TOKEN; check-versions.ts downloads release
# assets via the gh CLI and needs an explicit GH_TOKEN.
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
matrix:
Expand All @@ -31,18 +27,5 @@ jobs:
shell: bash
run: |
set -euo pipefail
out="$(./scripts/tools/plan-release.ts)"
printf '%s\n' "$out"
grep -q '^phase=' <<<"$out" || {
echo "shebang invocation produced no output on ${{ matrix.os }}" >&2
exit 1
}
# Same cwd and argv shape as release.yml publish (minus --dry-run).
sync="$(./scripts/tools/sync-root-version.ts --dry-run)"
printf '%s\n' "$sync"
grep -q '^+++' <<<"$sync" || {
echo "sync-root-version produced no unified patch on ${{ matrix.os }}" >&2
exit 1
}
./scripts/tools/check-versions.ts
./scripts/tools/check-matrix.ts
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# AGENTS.md

A high-quality, mutation-tested Rust implementation of a Claude Code `PostToolUse` hook that classifies code comments as justified or unnecessary. SOTA engineering: 100% mutation on the core classifier, property-based tests, constitution-aligned, with GitHub releases and npm distribution.
A high-quality, mutation-tested Rust implementation of a Claude Code `PostToolUse` hook that classifies code comments as justified or unnecessary. SOTA engineering: 100% mutation on the core classifier, property-based tests, constitution-aligned, distributed via GitHub Releases and this repository's Nix flake.

The npm distribution layer uses Effect v4 RC. Never install, import, or pin `effect@3.*` in the JS side.
The JS launcher layer uses Effect v4 RC. Never install, import, or pin `effect@3.*` in the JS side.

## Directory map

Expand Down Expand Up @@ -45,7 +45,7 @@ Treat repo files as one of four surfaces; read any, mutate only the assigned cla
| **Locked** | This file, evaluation scripts, merge policy, release workflows | Read and propose changes, never edit to make verification pass. |
| **Editable** | Project code (`crates/`, `tests/`), config, Cargo.toml, npm wrapper | Edit freely within the active task. |
| **Append-only** | `THREAD.md`, experiment logs, rejected ideas, `mutants.out*` artifacts (when tracked) | Append only; never rewrite or delete entries. |
| **Human-controlled** | Main-branch merge, production deploy, credentials, destructive ops, publishing to npm/GitHub under systemfsoftware | Ask the user before acting. |
| **Human-controlled** | Main-branch merge, production deploy, credentials, destructive ops, releasing (tags/GitHub Releases) under systemfsoftware | Ask the user before acting. |

## Definition of Done

Expand Down Expand Up @@ -119,7 +119,7 @@ Before adding any rule anywhere, run the placement escalation order: (1) delete
| Directory | Leaf | Why |
|-----------|------|-----|
| `crates/` | no | Rust core governed by root rules and tests |
| `npm/` | no (governed by root) | npm distribution layer (can contain multiple packages/apps under packages/ or apps/) — simple wrapper today |
| `npm/` | no (governed by root) | JS launcher layer (can contain multiple packages/apps under packages/ or apps/) — simple wrapper today |
| `tests/` | no | test harness governed by root verification |

## Git and Branch Discipline (Project Specific)
Expand Down
23 changes: 13 additions & 10 deletions CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ A per-kind/precision-recall gate on the corpus that trips when a kind's
classifier weakens — including a single-case kind that goes wrong — so a
weakness in one kind cannot hide inside an aggregate F1 score.

## npm distribution
## Distribution

### Launcher
The root npm package (`@systemfsoftware/claude-code-comment-checker`) whose
The root package (`@systemfsoftware/claude-code-comment-checker`) whose
`bin` is the `comment-checker` shim. It resolves the host platform package by
identity at runtime and spawns the binary — the only package that declares a
bin.
Expand All @@ -57,14 +57,17 @@ manifest (`os`/`cpu`/`libc` fields, no `bin`). The launcher's
`optionalDependencies` pins all five to the release version.

The committed launcher manifest never lists these packages as
`optionalDependencies` — pnpm cannot lock unpublished platform packages
(pnpm#3960), so the pins are injected from the targets table at publish
time; absence in-tree is expected, not a defect.

### Release lane
A matrix row in the release workflow: one platform/arch build, gate, smoke,
and publish run on its native runner. Platforms publish before the launcher,
and the release cannot proceed if any lane fails.
`optionalDependencies` — pnpm cannot lock the platform packages
(pnpm#3960), so the pins are injected from the targets table when the launcher
is packaged; absence in-tree is expected, not a defect.

### Release
Releases run through the shared toolchain
(`systemfsoftware/pnpm-release-management`), configured by `release.jsonc`: a
version with no `vX.Y.Z` git tag is owed a tag and a GitHub Release, so the
phase is derived from repository state, not a pull-request ref. Consumers take
the package from this repository's own Nix flake (built from source) at the
tag; nothing is published to a registry.

## Mutation gate

Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Contributing

See [AGENTS.md](AGENTS.md) for development rules, branch discipline, and verification gates.
For one-time npm OIDC bootstrap and trust configuration, run `cd scripts && deno task publish:unpublished`.

Releases run through the shared toolchain
([systemfsoftware/pnpm-release-management](https://github.com/systemfsoftware/pnpm-release-management)):
a `.changeset` intent on a pull request, then on merge the toolchain opens a
release PR, and merging that tags the version and cuts its GitHub Release.
`release.jsonc` configures it. Distribution is this repository's Nix flake at
the tag — nothing is published to a registry.
Loading
Loading