Skip to content
Merged
72 changes: 72 additions & 0 deletions .claude/hooks/changelog-nudge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
#
# Claude Code PreToolUse hook.
#
# Catches the case where the agent is ABOUT to commit a change that
# bumps a packages/<pkg>/package.json version but has not also
# generated a matching changelog/<pkg>/<version>.md file. Fires on
# Bash tool calls whose command starts with `git commit`.
#
# Hard block: writes a JSON hookSpecificOutput with
# "permissionDecision": "deny" so the tool call is refused and the
# agent reads the included reason. The agent should then run:
#
# node scripts/backfill-changelog.js && git add changelog/
#
# and retry the commit.
#
# Skipped outside a git work tree and when there are no staged
# version bumps.

set -e

# Read the hook payload from stdin so we can inspect the command
# the model is about to run.
INPUT=$(cat)

# Only act on Bash tool calls whose command is `git commit ...`.
COMMAND=$(printf '%s' "$INPUT" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | sed 's/.*: *"\(.*\)".*/\1/')
case "$COMMAND" in
*git*commit*) ;;
*) exit 0 ;;
esac

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 0
fi

STAGED_PKG_BUMPS=$(git diff --cached --unified=0 -- 'packages/*/package.json' 2>/dev/null \
| awk '
/^diff --git/ { match($0, /packages\/[^\/]+\/package.json/); pkg = substr($0, RSTART+9, RLENGTH-22); next }
pkg && /^\+\s*"version":\s*"[^"]+"/ {
match($0, /"[0-9]+\.[0-9]+\.[0-9]+[^"]*"/); v = substr($0, RSTART+1, RLENGTH-2);
print pkg "@" v
}')

if [ -z "$STAGED_PKG_BUMPS" ]; then
exit 0
fi

MISSING=""
for bump in $STAGED_PKG_BUMPS; do
pkg="${bump%@*}"; ver="${bump#*@}"
if [ ! -f "changelog/$pkg/$ver.md" ]; then
MISSING="$MISSING changelog/$pkg/$ver.md"
fi
done

if [ -z "$MISSING" ]; then
exit 0
fi

# Emit a JSON denial so Claude Code refuses this tool call and
# surfaces the reason to the agent.
cat <<EOF
{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "[changelog-nudge] You are committing a packages/<pkg>/package.json version bump but the matching changelog file(s) are missing:$MISSING\n\nRun: node scripts/backfill-changelog.js && git add changelog/\nThen retry the commit. To bypass (rare; emergencies only): git commit --no-verify"
}
}
EOF
exit 0
9 changes: 9 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
"command": ".claude/hooks/block-prose-punctuation.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/changelog-nudge.sh"
}
]
}
],
"PostToolUse": [
Expand Down
37 changes: 37 additions & 0 deletions .hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,41 @@ if ! npm test --silent; then
exit 1
fi

# Gate 3: when a packages/<pkg>/package.json version bumped, require
# a matching changelog/<pkg>/<version>.md to land in the same commit.
# The "diff --cached" check pulls the new version line from the
# staged package.json; the directory existence test catches the
# matching changelog entry. Re-runs `scripts/backfill-changelog.js`
# so an author who forgets only loses time, not the commit.
STAGED_PKG_BUMPS=$(git diff --cached --unified=0 -- 'packages/*/package.json' 2>/dev/null \
| awk '
/^diff --git/ { match($0, /packages\/[^\/]+\/package.json/); pkg = substr($0, RSTART+9, RLENGTH-22); next }
pkg && /^\+\s*"version":\s*"[^"]+"/ {
match($0, /"[0-9]+\.[0-9]+\.[0-9]+[^"]*"/); v = substr($0, RSTART+1, RLENGTH-2);
print pkg "@" v
}')

if [ -n "$STAGED_PKG_BUMPS" ]; then
MISSING=""
for bump in $STAGED_PKG_BUMPS; do
pkg="${bump%@*}"; ver="${bump#*@}"
if [ ! -f "changelog/$pkg/$ver.md" ]; then
MISSING="$MISSING\n - changelog/$pkg/$ver.md (for @webjskit/$pkg $ver)"
fi
done
if [ -n "$MISSING" ]; then
echo ""
echo "ERROR: detected version bumps in staged changes but matching changelog files are missing:"
echo -e "$MISSING"
echo ""
echo "Run:"
echo " node scripts/backfill-changelog.js"
echo " git add changelog/"
echo ""
echo "Then re-commit. To bypass (emergencies only): git commit --no-verify"
echo ""
exit 1
fi
fi

exit 0
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ When editing the framework monorepo (this repo, not a scaffolded app): **`packag

See `agent-docs/framework-dev.md` for monorepo commands, workspace layout, reference codebases, and per-feature update checklists.

### Changelog: per-package, per-version, auto-generated

webjs ships per-package per-version changelogs under `changelog/<pkg>/<version>.md`. The model: **a version bump is the trigger**. When any commit on `main` changes the `version` field in `packages/<pkg>/package.json`, the scripts/backfill-changelog.js generator emits a new `changelog/<pkg>/<version>.md` summarising every conventional-commit (`feat:` / `fix:` / `breaking:` / `perf:`) that landed in that package since the prior bump. The website renders the union of all packages' files at `/changelog`.

**Mandatory rule for AI agents working in this monorepo:**

1. When you bump a `packages/<pkg>/package.json` `version`, you MUST run `node scripts/backfill-changelog.js` in the same commit (or the immediately following one), so the new `changelog/<pkg>/<version>.md` lands together with the bump.
2. Review the generated file. The script's body excerpts are the first lines of each commit message; if any are unclear, **edit the file in place** to add migration notes (especially for `breaking` entries) before committing.
3. Subsequent re-runs are safe: the script never overwrites an existing entry, so your hand-edits survive.
4. Never edit `changelog/<pkg>/<version>.md` for a version that has already been published. Edit `changelog/<pkg>/<next>.md` instead.

The `.hooks/pre-commit` git hook (and the Claude Code `PreToolUse` hook `.claude/hooks/changelog-nudge.sh`) catches version bumps that lack a matching changelog file and refuses the commit until both land together.

---

## What webjs is
Expand Down
128 changes: 128 additions & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Changelog

webjs ships **per-package, per-version** changelog files.

```
changelog/
README.md this file
core/
0.6.0.md
0.5.0.md
server/
0.7.1.md
cli/
0.7.0.md
ts-plugin/
0.4.0.md
ui/
0.2.0.md
```

The website (`website/app/changelog/page.ts`) reads every
`<pkg>/<version>.md` at SSR time, sorts by date descending, and
renders the unified release feed.

## How a release entry gets created

The model is "**a version bump produces a changelog file**". When a
package's `package.json` `version` field changes in a commit, the
entry script:

1. Identifies the prior version of the same package on `main`.
2. Walks the commits between the prior version's bump and the new
bump that touched files under `packages/<pkg>/`.
3. Filters to conventional-commit prefixes that matter to users:
`feat:`, `fix:`, `breaking:`, `perf:`.
4. Groups them under `Breaking` / `Features` / `Performance` /
`Fixes` and writes a `<version>.md` file with frontmatter
(`package`, `version`, `date`, `commit_count`) plus a bulleted
list of changes (PR link + commit SHA + body excerpt).

`chore:`, `refactor:`, `test:`, `docs:`, `style:`, `build:`, `ci:`
commits never appear in the changelog: those changes don't change the
package's user-facing contract.

## Entry format

```markdown
---
package: "@webjskit/core"
version: 0.6.0
date: 2026-05-21
commit_count: 4
---

# @webjskit/core 0.6.0

## Breaking

- **Title of the change** ([#NN](https://github.com/vivek7405/webjs/pull/NN)) [`abcd123`](https://github.com/vivek7405/webjs/commit/abcd123)
First four lines of the commit body, indented two spaces.

## Features

## Fixes
```

Frontmatter fields:

| Field | Required | Meaning |
|---|---|---|
| `package` | yes | The fully-qualified npm name (`@webjskit/<pkg>`). |
| `version` | yes | Semver string of the new release. |
| `date` | yes | Date of the version-bump commit, `YYYY-MM-DD`. |
| `commit_count` | yes | How many qualifying commits this version shipped. |

## Backfill + ongoing automation

A single script runs in both modes:

```sh
node scripts/backfill-changelog.js
```

It walks every package's `package.json` history, finds version
bumps, and writes a `<pkg>/<version>.md` for any version that does
not yet have one. **Files that already exist are left alone**, so
hand-curated entries survive subsequent runs (CI re-runs are safe).

Going forward, the same script runs:

- on every CI build of `main`, so a forgotten version bump
still produces an entry without manual intervention;
- locally when an agent edits a `packages/<pkg>/package.json` to
bump the version (the post-commit hook
`.hooks/post-version-bump` runs the script).

The AGENTS.md rule that AI agents follow on every code-commit:

> When you bump a `packages/<pkg>/package.json` `version`, run
> `node scripts/backfill-changelog.js` (or just commit; CI will
> regenerate). The generated `changelog/<pkg>/<version>.md` file
> should be reviewed and **edited in-place** for clarity, especially
> for `breaking` entries that need migration notes. Then commit the
> changelog file alongside the version bump.

## What if the same change ships across packages

A commit that touches more than one package (e.g. a refactor that
moves code from `core` to `server`) appears in **every** affected
package's release file when each of those packages bumps its
version. That is the right shape: the same change is a user-facing
event for every package that carries it.

## Migration to the new format

The pre-0.7.1 history was backfilled in one pass. The files in
`changelog/<pkg>/` are auto-generated but can (and should) be
hand-edited to add migration notes, examples, or links to docs.
Subsequent re-runs of `scripts/backfill-changelog.js` will not
overwrite hand edits because the script skips files that already
exist.
Loading