Repository navigation
Releases: Bubblegunn/product-engineer
Release list
v0.9.0
0.9.0 (2026-11-02)
check --diff reads the direction of an operation, and says what no diff can show. It now warns when a message says
a file was added and the change deletes it (or the reverse), when a backticked name or one of six security words
(authentication, permission, CSRF, rate limiting, sanitization, encryption) is said to be added or removed and the changed
lines show it only on the other side, when a bug fix is claimed over a change that is documentation only, and when "no breaking
changes" is claimed over a removed export (JavaScript and TypeScript, Python, Go, Rust). Every claim in a message about the
kind of file changed is checked, not only the first. Claims a diff cannot hold (tests ran or passed, tried by hand, a speed
figure) are listed at info level as claimed only. --strict turns the findings the change proves into errors; the default is
unchanged: warnings, exit code 0. Outside a git repository --diff says to run it inside one instead of printing a git command
line. Messages in other languages still get the block and readability checks and no claim checks (#15).
False alarms were measured on public commits and fixed. The cross-check was run on the last 600 non-merge commits of 20
public repositories in three corpora (8,394 + 3,600 commits). On the first corpus path reports fell from 251 to 9 and test reports
from 11 to 1. The third corpus, six repositories chosen before the run and not tuned against, gave 45 reports (1.25% of 3,600
commits) on its first run; seven shapes were fixed with a regression test and the same run now gives 9 (0.25%). That corpus is no
longer held out, and the second one was not either; the remaining shapes are listed in docs/check-reference.md.
scripts/false-alarm-corpus.mjs repeats a run on clones you provide.
The first screen says what is measured. The README opens with one command, separates the checker (deterministic, no model) from
the agent skill (depends on the model), and lists per model what the skill changed: the For-the-customer block appears on
claude-opus-5 (8 of 8) and claude-sonnet-5-5 (13 of 24) and not on claude-haiku-4-5 (0 of 24). It no longer says the skill
"makes" an agent do anything, and it says that no lead over the bare model was found in a larger comparison on a development
branch. docs/install.md#what-has-been-run lists, per install route, what was actually run and what was not (Codex and Cursor
manifests, other agents' rule files and third-party use of the Action are not verified). docs/proof.md shows six catches and two
misses from real checker runs.
The checker comparison was rerun. docs/COMPARISON.md was regenerated on 2026-11-02 from this branch (0.8.0 plus these
changes) against commitlint 21.2.4 run with an isolated HOME: the outcome is the same on all 12 messages, so the three rows where
product-engineer loses (no type, subject over 100 characters, body line over 100) stay. The 2026-10-21 result on 0.7.0 is kept in
evals/comparison/. The skill comparison needs a model and was not rerun.
New eval runs record what the agent did. evals/run.sh stores the event stream, every tool call, whether the skill was called
or read (skill_loaded in meta.json) and the final diff, so that skill loading can be read instead of inferred from cost. Tested
on a fake stream only; no model run has used it yet. evals/WEAK-MODELS.md reads, from the existing runs, why the skill shows no
effect on claude-haiku-4-5; it is an inference and a blind four-arm evaluation is proposed there, not run.
What's Changed
Everything else
- Fix/check commits by @Aaqibhafeezkhan in #49
- ci: separate lint, test and type workflows with one badge each by @Bubblegunn in #50
- fix: fewer false reports from check, and claims that match the measurements by @Bubblegunn in #52
Full Changelog: v0.8.0...v0.9.0
v0.8.0
0.8.0 (2026-10-26)
product-engineer hook install sets up the commit check from npm. It writes the generated commit-msg hook (the
same file as scripts/commit-msg, so the headings cannot drift) into the directory git actually uses, including a
core.hooksPath such as husky's. --prefill also installs a prepare-commit-msg hook that starts each new message
with the empty block; hook uninstall removes only hooks carrying the generated marker. An existing hook that is not
ours is never overwritten without --force, and installing twice changes nothing. Written by
@Aaqibhafeezkhan (#44,
closes #30).
product-engineer notes <range> writes customer release notes from the commit blocks. Each commit's What changed line in the range becomes a bullet, with Why it matters nested beneath it. Commits without a block are
counted at the end instead of dropped, headings set in .product-engineer.json are honoured, and a missing range, a
range starting with -, or one git cannot read exits 2 with one line saying why. Written by
@Aaqibhafeezkhan (#46,
closes #31).
A gallery of before and after pairs from the eval runs. site/gallery.html quotes, for every committed run under
evals/results*, the commit the agent made without the skill and the one it made with it, scores both with
evals/score.mjs and links both transcripts. Pairs where the skill changed nothing or made the message worse are
shown at the same size as the wins. scripts/generate-gallery.mjs writes the page, and test/gallery.test.mjs
regenerates it and fails when the committed page and the transcripts disagree. Written by
@Nitjsefnie (#47, closes
#21).
What's Changed
Everything else
- feat: add npm hook install command by @Aaqibhafeezkhan in #44
- feat(site): a gallery of before and after pairs, generated from the eval transcripts by @Nitjsefnie in #47
- feat(notes): generate customer changelog from commit blocks by @Aaqibhafeezkhan in #46
New Contributors
- @Aaqibhafeezkhan made their first contribution in #44
- @Nitjsefnie made their first contribution in #47
Full Changelog: v0.7.0...v0.8.0
v0.7.0
0.7.0 (2026-10-20)
check --format json. Prints one JSON object, { ok, findings: [{ level, code, message }], readability }, and
nothing else on standard output; the exit code is the one text mode gives. Every finding carries a stable code
(block-missing, diff-tests-missing, references-broken, and so on), set where the finding is made.
The Action can read the change and skips dependency bots. New inputs: diff (a revision range, runs
check --diff), references (off, warn or error) and skip-authors (default dependabot[bot],renovate[bot],
an empty string checks every pull request). A diff that is not plain revision text exits 2 before anything runs.
Codex and Cursor plugin manifests. .codex-plugin/plugin.json and .cursor-plugin/plugin.json point at
./skills/. Their shape and the skills they load are tested; installing through each app's plugin search is not yet
verified. THIRD_PARTY_NOTICES.md carries the licence text of the manifests they were adapted from.
A generated comparison. docs/COMPARISON.md runs check and commitlint on the same messages, from competitors
pinned in scripts/comparison/pins.json and fetched into the gitignored _upstream/. It also compares the skill with
two other skills on the eight eval tasks, one run each. The rows where product-engineer loses are printed like the
others.
The eval can install a skill from outside the repository and stop at a budget. PE_EVAL_SKILL_PATH,
PE_EVAL_SKILL_SOURCE and PE_EVAL_BUDGET_USD for evals/run.sh; node evals/by-model.mjs prints one section per
model and never averages them.
The skill was run on other models. Three runs per task on claude-haiku-4-5 and claude-sonnet-5-5, every transcript
published in evals/results-<model>-2026-10-20/, and the per-model report in evals/RESULTS-models.md, including the
models and metrics where the effect was not clear.
The README opens with the problem, one command and a recording. The first line is the sentence a user would say, the next paragraph is the one category sentence, and docs/demo-diff.gif (made by vhs docs/demo-diff.tape, real output) shows check --diff warning that a message claims tests the change does not contain. The model table (claude-opus-5, claude-sonnet-5-5, claude-haiku-4-5) is on the first screen, including the Haiku result, "no clear effect". "How this differs" shows only rows product-engineer wins or ties in docs/COMPARISON.md. The star badge, the DOI badge in the top badge row and the star-history chart are gone while the repository has one star; the DOI badge in the Cite this section stays. The cited-paths and readability paragraphs moved to docs/check-reference.md.
Corrections to published claims. The README said the diff baseline was 200 commits deep (it is 64 non-merge commits with file changes at 25dcce7), quoted a result for the last five commits that no longer reproduces, and gave four years as a founding engineer where the CV gives one role of about a year. The site and llms.txt carried the 5 September table, a "60% more turns, 45% more cost" figure and an agent count with no source. All are corrected, and test/readme-claims.test.mjs rebuilds each figure from evals/baseline/RESULTS.md, evals/RESULTS-models.md and evals/comparison/*.json.
Findable by search and by assistants. One category sentence is the package description and appears word for word in the README, the site meta description, schema.org SoftwareSourceCode JSON-LD, llms.txt and the FAQ. The site has a topic title, a canonical link, sitemap.xml, robots.txt, llms.txt and llms-full.txt (generated by scripts/build-llms.mjs, which has a --check mode). New keywords: claude-code-skill, commit-message-lint, pull-request-description, ai-coding-agent, github-action, diff-check, non-technical-stakeholders.
Docs written for readers who compare tools. docs/FAQ.md, one comparison page each for commitlint, verification-before-completion and changelog-generator (with the rows product-engineer loses and what was not measured), docs/GEO-PROMPTS.md (five fixed prompts to run before and after launch) and docs/LISTINGS.md (store text, nothing submitted).
Full Changelog: v0.6.2...v0.7.0
v0.6.2
0.6.2 (2026-10-16)
customer-block and done-means-observed are measured on their own. With PE_EVAL_SKILL from 0.6.1, each
was run alone against a fresh bare arm on the eight tasks (17 October, claude-opus-5, one run per task, 32 runs
with the core skill alongside, $15.87). Each moved the metric its rule is about and not the other: the customer
block went from 0 to 8 of 8 with customer-block, and the observation row from 1 to 6 of 8 with
done-means-observed. Both drop their measuredGap; release-notes keeps its own, since no task writes release
notes. The runs are in evals/results-2026-10-17/ and the table is in evals/RESULTS.md.
node evals/score.mjs accepts an absolute directory. It joined the argument to the working directory, so an
absolute path failed with ENOENT.
Full Changelog: v0.6.1...v0.6.2
v0.6.1
0.6.1 (2026-10-16)
Measure one skill at a time. PE_EVAL_SKILL=customer-block sh evals/run.sh installs only that skill in the
skill arm, meta.json records which skill ran, and evals/score.mjs refuses to table runs of different skills
together. The paid rerun, and removing the measuredGap notes it would settle, are still to do. Written by
@jay23606 (#43, closes
#33).
What's Changed
Everything else
New Contributors
Full Changelog: v0.6.0...v0.6.1
v0.6.0
0.6.0 (2026-10-14)
product-engineer template prints the block to fill in. In English by default, in a shipped language
with --lang tr|ja|zh, or in the heading set from .product-engineer.json; an unknown --lang exits 2.
product-engineer template > .gitmessage.txt plus git config commit.template .gitmessage.txt makes git
open every commit with the block. check now reads a heading followed only by an unfilled
<placeholder> as empty, so a half-filled template fails: on 0.5.2 a leftover
Automation effect: <…> line passed with no errors, and now it is reported. A line that merely starts
with an angle bracket, such as What changed: <video> embeds play again., still passes. Written by
@Enard52 (#41, closes
#25).
What's Changed
Everything else
- ci(release): give the registry ten minutes before the smoke test gives up by @Bubblegunn in #40
- feat: add template command to generate starter spec (#25) by @Enard52 in #41
- docs: credit @Enard52 on the day #41 merged by @Bubblegunn in #42
New Contributors
Full Changelog: v0.5.2...v0.6.0
v0.5.2
0.5.2 (2026-10-14)
A long run of ., ! or ? no longer stalls the check. The sentence splitter in
bin/readability.mjs used [.!?]+(\s|$), which retried a run of punctuation from every position
inside it. A block holding 40,000 exclamation marks between two letters took 2,051 ms to score, and
time grew with the square of the run, so a pull request body could hold the Action for as long as
its author liked. The split now happens at the whitespace after a punctuation mark: the same input
takes 0.3 ms. Readability results are byte-identical before and after on 101 texts (the test
fixtures, examples/, every commit message in this repository and eight punctuation edge cases).
test/readability.test.mjs fails if 50,000 exclamation marks take 250 ms or more; before the fix
they took 3,244 ms.
The release workflow's smoke job, which installs the published package on three operating
systems, no longer restores an npm cache: package-manager-cache: false on its setup-node, as the
publish job already had. A cache written by another workflow run could otherwise reach a job that
runs on a release tag. zizmor 1.30.1 reported it as cache-poisoning and reports nothing now.
Dependabot waits seven days after a release before proposing it, for both GitHub Actions and npm
(cooldown.default-days: 7), so a version that is pulled soon after publishing is less likely to
reach this repository.
What's Changed
Everything else
- docs: link the GitHub Marketplace listing from the badge row by @Bubblegunn in #37
- fix: close code-scanning alerts (0.5.2), and a smoke check that could not fail by @Bubblegunn in #38
Full Changelog: v0.5.1...v0.5.2
v0.5.1
0.5.1 (2026-10-14)
The Action can be listed on GitHub Marketplace. Marketplace accepts a description of fewer than 125 characters; this
one was 138. It is now 119, and a test fails on a description of 125 characters or more. Nothing a workflow uses changes.
What's Changed
Everything else
- ci(release): make a second run for the same tag safe by @Bubblegunn in #35
- fix(action): a name and description GitHub Marketplace accepts by @Bubblegunn in #36
Full Changelog: v0.5.0...v0.5.1
v0.5.0
0.5.0 (2026-10-14)
Breaking: Node 22 or newer. engines.node moves from >=20 to >=22, because Node 20 reached
end of life on 2026-04-30 and receives no security fixes. The code did not change for it, and the
suite still passes on Node 20.20.2 today (106 tests, run once in node:20), but no workflow here has
ever run on Node 20, so nothing would notice when that stops being true. npm prints an engine
warning on Node 20 and installs anyway unless engine-strict is set. The skills themselves are text
and are not affected; the GitHub Action runs on whatever Node the runner provides.
The version a file states is now checked everywhere the release sets it. The package was 0.4.0
while skills/product-engineer/SKILL.md said 0.3.2, the other three skills said 0.3.0, and the
pre-commit snippet in docs/install.md pinned rev: v0.3.0, so a reader copying it installed the
hook as it was in 0.3.0. product-engineer doctor printed the 0.3.2 as the installed version. Nothing
failed, because neither scripts/release.mjs nor scripts/release-gate.mjs knew those files
existed. Both now read one list, scripts/versioned-files.json; the release sets every file on it
and the gate fails on any one that disagrees with package.json. A test changes each listed file in
a copy of the repository in turn and requires the gate to refuse it. doctor reports the package's
own version.
action.yml has not been valid YAML since 0.3.0, and nothing ran it. The comment input's
description said pull-requests: write without quotes, and YAML reads the second : as a nested
mapping. PyYAML, yaml@2 and actionlint (Go YAML) all refuse the file at line 17; GitHub's own
parser was not observed here, but a workflow using Bubblegunn/product-engineer@v0 should expect the
same refusal until this ships. The description is quoted now. CI gains an action job that runs the
Action through uses: ./ on a description with the block, one without, one without but with warn,
and a non-numeric pr, and requires each outcome; test/action.test.mjs runs the Action's shell step
with bash and pins the exit codes 0, 1 and 2, and fails on any unquoted value containing : .
New Action input body-file: check a file instead of fetching the pull request. Nothing is fetched
and no comment is posted. It exists so CI can run the Action on fixed text.
The commit-msg hook refused a Chinese block when no UTF-8 locale was set. The heading pattern
put the ASCII and fullwidth colons in one bracket, [::], and without a UTF-8 locale grep reads
: as three bytes, of which a bracket matches one. 给客户: therefore failed under LC_ALL=C with
both BSD grep (macOS) and GNU grep (Debian). A Git Bash on Windows with no LANG set should behave
the same way; that was reasoned, not observed. The colon is an alternation now, (:|:), and the hook test runs every case a second
time under LC_ALL=C.
CI runs the tests on Windows as well as Ubuntu and macOS, in Git Bash, which is the shell Git for
Windows runs the hook with. A new .gitattributes checks every text file out with LF on all
platforms, since Git for Windows converts to CRLF by default and the hook, the shell tests and the
version patterns match line ends.
What's Changed
Everything else
- fix(release): give the registry longer than two minutes to serve a new version by @Bubblegunn in #23
- ci: verify main after a bot merge, and say how in the release refusal by @Bubblegunn in #24
- product-engineer 0.5.0: fix action.yml, test the Action in CI, one version list, Windows, Node 22 by @Bubblegunn in #34
Full Changelog: v0.4.0...v0.5.0
v0.4.0
0.4.0 (2026-09-21)
A message can cite a file that is not there, and until now nothing noticed. check resolves
every path/to/file.ext and GitHub Actions run URL in a message against the working tree. A missing
file, a directory where a file was claimed, or a line number past the end of a real file is a
warning; --references error makes it an error, --references off turns it off. A line number is
the part of a citation that rots while the sentence around it stays still, which is why it is checked
rather than trusted.
A message that cites nothing is reported as citing nothing, at info level, and passes in every mode.
That is the design rather than an omission: a check that required a link would be satisfied by a link
typed from memory, and the reader who followed it would be the one to find out. What is checkable is
the citation that is already there.
From outside the project: the request came from @raju_dandigam in the
dev.to comments on the skill, who asked whether the observed evidence could be made machine-checkable
in CI, each claim linked to a test, a screenshot or a trace artifact, without turning the block into
boilerplate. The second half of his question is what shaped the default.
Offline by construction: a run URL is checked for shape, never fetched, and a test holds that with a
fetch that throws. Ten tests cover the rest, including the false positives that would make the
check ignorable (v0.3.4, 1.5x, e.g., 3/4).
What's Changed
Everything else
- feat(check): resolve the files and run URLs a message cites by @Bubblegunn in #22
New Contributors
- @Bubblegunn made their first contribution in #22
Full Changelog: v0.3.4...v0.4.0