forbid conventional-commit type prefixes in PR titles #25

Merged
jerome merged 10 commits from fix/enforce-pr-title-no-type-prefix into main 2026-07-19 08:36:50 +03:00
Collaborator

Goal: a PR title can never carry a conventional-commit type prefix -- feat:, fix:, or the scoped fix(module): -- because a deterministic gate strips it at write time, not merely because prose discourages it.

Value: med
Effort: 2h


Why

The rule already exists: task-new says the title carries no conventional-commit
type prefix
and task-implement/worktree-and-branches.md has a ## Title style
section saying the same. The type belongs in a label, which task-new Step 9 calls
mandatory precisely because the title dropped it.

It is still being ignored. PR #24 on this repo shipped as
feat: add prioritize-next-features plugin, and fj_write.py's own usage examples
demonstrated the anti-pattern (pr-title 9 "feat: ship it",
pr-create ... --title "feat: x") -- so the one artifact a model reads right before
writing a title taught it the wrong shape.

Three gaps explain the drift:

  1. No enforcement point. The rule is stated where a title is authored, never
    where one is written to the forge. Nothing catches a bad title after the fact.
  2. The scoped form is never named. feat(scope): / fix(module): -- the worst
    variant -- appears nowhere in the prose, so it does not pattern-match as forbidden.
  3. Downstream skills are silent. task-orchestrate and task-review handle the
    title (review rewrites it at mark-ready) but never check its shape, so a bad title
    survives all the way to merge.

Prose alone has already failed once. The fix layers a deterministic gate under it:
fj_write.py is the single chokepoint every PR title passes through, so normalising
there makes the invariant hold regardless of what the model typed.

Preserve the distinction: commit subjects stay conventional-commit style
(feat: add OAuth login) -- they are git history, not the forge-rendered title. Only
PR titles are affected. Issue titles are out of scope.


Steps

  • Add a strip_type_prefix(title) helper to
    plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py. It removes a
    leading conventional-commit type token -- feat|fix|docs|chore|refactor|test|perf|build|ci|style|revert,
    with an optional (scope), an optional !, then : and following space --
    matched case-insensitively and applied after any WIP: draft prefix so
    WIP: feat: x normalises to WIP: x. Strip repeatedly (a title is not
    allowed to smuggle feat: fix: x through). Return the cleaned title
    unchanged when no prefix is present.
  • Apply the helper in cmd_title (PR path only) and in cmd_create's PR
    branch, before the title reaches the fj argv. When it changes the title,
    print a one-line notice naming the stripped prefix and the resulting title,
    so the caller sees the normalisation rather than a silent rewrite. Leave
    issue-title and issue-create untouched.
  • Fix fj_write.py's own usage examples in the module docstring -- replace
    feat: ship it and feat: x with prefix-free titles so the reference stops
    teaching the anti-pattern.
  • Test tree mirrors the source tree exactly. Establish the convention: a
    source file at <path>/<name>.py is tested by tests/<path>/test_<name>.py,
    so the directory structure under tests/ is a byte-for-byte mirror of the
    source layout and each test file's name is derived from its source file's.
    Cover strip_type_prefix() at
    tests/plugins/forgejo-cli/skills/forgejo-cli/scripts/test_fj_write.py
    (plain form, scoped form, ! marker, case-insensitivity, WIP:
    preservation, doubled prefixes, idempotence, and the near-misses that must
    NOT be stripped -- fixture loader, feature flags:). Stdlib unittest,
    matching the dependency-free scripts under test. Document the mirroring rule
    in the test module's docstring.
  • Name the scoped form explicitly in the two places the rule is already
    stated: plugins/tasks/skills/task-new/SKILL.md (the Title: paragraph in
    "PR Body Shape" and the common-mistakes row) and
    plugins/tasks/skills/task-implement/worktree-and-branches.md
    (## Title style). Both must forbid feat:/fix: and feat(scope): /
    fix(module):, and both must keep the existing note that commit subjects
    stay conventional-commit style.
  • Document the normalisation in
    plugins/forgejo-cli/skills/forgejo-cli/SKILL.md under the write section:
    one line noting pr-title / pr-create strip a conventional-commit type
    prefix, and that the type belongs on a label.
  • Add a title-hygiene check to plugins/tasks/skills/task-review/SKILL.md:
    Step 12 already rewrites the title at mark-ready, so have it strip a
    conventional-commit type prefix alongside WIP: rather than only WIP:.
    Add a matching common-mistakes row.
  • Add a common-mistakes row to
    plugins/tasks/skills/task-orchestrate/SKILL.md covering the same rule, so
    the one skill that drives a PR to merge without going through task-new
    also states it.
  • Retitle PR #24 from feat: add prioritize-next-features plugin to
    add prioritize-next-features plugin and confirm it carries the
    enhancement label (the type signal the title gives up).
  • Add a ## Toolchain block to a repo-root CLAUDE.md declaring a test:
    command, so the new test tree is actually executed by /task-orchestrate
    and /task-review instead of sitting inert. Landed as
    test: uv run --with pytest pytest tests -q -- pytest, not
    unittest discover
    : plugin directories contain hyphens
    (forgejo-cli), so they are not importable packages and discovery cannot
    walk the src-mirroring layout. The same CLAUDE.md documents the
    mirroring convention and the PR-title rule.

Notes

Why the script and not a git hook. A hook fires on commits, not on forge API
calls, and PR titles are set through fj. fj_write.py is the one path every PR
title in this ecosystem passes through -- task-new Step 8, task-implement's
one-shot PR-create, and task-review Step 12 all route through it via the
forgejo-cli skill. Normalising there covers every caller with one change.

Why strip rather than reject. A non-zero exit mid-pass would strand the
resolver with a pushed branch and no PR, and the correct title is mechanically
derivable from the bad one -- there is nothing to ask the user. Stripping is
idempotent and always leaves the title in the legal state. The printed notice keeps
it visible instead of silent.

Why WIP: is handled first. WIP: is Forgejo's draft signal and must survive;
only the type token after it is stripped. fj's title edit re-derives the draft
flag from the title, so the order matters -- getting it wrong would undraft the PR.

Test-tree mirroring: scope of what was possible here. The requirement is that
the test tree mirror the source tree exactly. This repo had no test tree and no
source tree
to mirror when the work started: no src/, no tests/, no
pyproject.toml, no test runner, and a CI workflow that only runs gitleaks. Its
four Python files are standalone stdlib scripts scattered under
plugins/*/skills/*/scripts/. So the mirroring convention is established here and
applied to the one unit this PR adds, rather than retrofitted across the repo --
building out a repo-wide test tree for three pre-existing untested scripts is a
separate piece of work with its own verification, and folding it in would have made
this PR about test infrastructure rather than title hygiene. The convention and its
root (tests/) are now fixed, so later work extends the mirror by adding files, not
by redesigning the layout.

Toolchain: pytest, decided by the user. The repo had no CLAUDE.md at all, so
nothing in the task suite knew how to run the new tests -- /task-review Step 8
reads its commands from a ## Toolchain block and skips what it does not find. The
resolver left it unticked rather than invent one; the user chose pytest during the
orchestrate pass. The originally-suggested
unittest discover -s tests -t . turned out to be unworkable: discovery imports
each directory as a package, and forgejo-cli / herdr-cli contain hyphens, so it
fails with Start directory is not importable. Making it work would have meant
renaming test directories away from their source names -- destroying the exact
mirror this PR establishes. pytest collects by path, needs no __init__.py, and
keeps the mirror intact.

Rejected: a --allow-type-prefix escape hatch. forgejo-cli is a generic
Forgejo wrapper, so an opt-out is defensible in principle, but no caller in this
repo wants one and the goal is that a bad title is unrepresentable. YAGNI -- add it
if a real consumer ever needs it.

Rejected: only fixing the prose. That is the status quo. The rule has been
written in two places since the skills were authored and PR #24 still shipped with
feat:.

Scope boundary. Commit subjects keep conventional-commit prefixes
(task-implement Step 5, worktree-and-branches.md) and branch names keep their
feat/ / fix/ prefixes. Neither is touched. Issue titles are unaffected.

**Goal:** a PR title can never carry a conventional-commit type prefix -- `feat:`, `fix:`, or the scoped `fix(module):` -- because a deterministic gate strips it at write time, not merely because prose discourages it. **Value:** med **Effort:** 2h --- ## Why The rule already exists: `task-new` says the title carries **no conventional-commit type prefix** and `task-implement/worktree-and-branches.md` has a `## Title style` section saying the same. The type belongs in a label, which `task-new` Step 9 calls mandatory precisely because the title dropped it. It is still being ignored. PR #24 on this repo shipped as `feat: add prioritize-next-features plugin`, and `fj_write.py`'s own usage examples demonstrated the anti-pattern (`pr-title 9 "feat: ship it"`, `pr-create ... --title "feat: x"`) -- so the one artifact a model reads right before writing a title taught it the wrong shape. Three gaps explain the drift: 1. **No enforcement point.** The rule is stated where a title is *authored*, never where one is *written to the forge*. Nothing catches a bad title after the fact. 2. **The scoped form is never named.** `feat(scope):` / `fix(module):` -- the worst variant -- appears nowhere in the prose, so it does not pattern-match as forbidden. 3. **Downstream skills are silent.** `task-orchestrate` and `task-review` handle the title (review rewrites it at mark-ready) but never check its shape, so a bad title survives all the way to merge. Prose alone has already failed once. The fix layers a deterministic gate under it: `fj_write.py` is the single chokepoint every PR title passes through, so normalising there makes the invariant hold regardless of what the model typed. **Preserve the distinction:** *commit subjects* stay conventional-commit style (`feat: add OAuth login`) -- they are git history, not the forge-rendered title. Only PR titles are affected. Issue titles are out of scope. --- ## Steps - [x] Add a `strip_type_prefix(title)` helper to `plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py`. It removes a leading conventional-commit type token -- `feat|fix|docs|chore|refactor|test|perf|build|ci|style|revert`, with an optional `(scope)`, an optional `!`, then `:` and following space -- matched case-insensitively and applied *after* any `WIP:` draft prefix so `WIP: feat: x` normalises to `WIP: x`. Strip repeatedly (a title is not allowed to smuggle `feat: fix: x` through). Return the cleaned title unchanged when no prefix is present. - [x] Apply the helper in `cmd_title` (PR path only) and in `cmd_create`'s PR branch, before the title reaches the `fj` argv. When it changes the title, print a one-line notice naming the stripped prefix and the resulting title, so the caller sees the normalisation rather than a silent rewrite. Leave `issue-title` and `issue-create` untouched. - [x] Fix `fj_write.py`'s own usage examples in the module docstring -- replace `feat: ship it` and `feat: x` with prefix-free titles so the reference stops teaching the anti-pattern. - [x] **Test tree mirrors the source tree exactly.** Establish the convention: a source file at `<path>/<name>.py` is tested by `tests/<path>/test_<name>.py`, so the directory structure under `tests/` is a byte-for-byte mirror of the source layout and each test file's name is derived from its source file's. Cover `strip_type_prefix()` at `tests/plugins/forgejo-cli/skills/forgejo-cli/scripts/test_fj_write.py` (plain form, scoped form, `!` marker, case-insensitivity, `WIP:` preservation, doubled prefixes, idempotence, and the near-misses that must NOT be stripped -- `fixture loader`, `feature flags:`). Stdlib `unittest`, matching the dependency-free scripts under test. Document the mirroring rule in the test module's docstring. - [x] Name the scoped form explicitly in the two places the rule is already stated: `plugins/tasks/skills/task-new/SKILL.md` (the **Title:** paragraph in "PR Body Shape" and the common-mistakes row) and `plugins/tasks/skills/task-implement/worktree-and-branches.md` (`## Title style`). Both must forbid `feat:`/`fix:` *and* `feat(scope):` / `fix(module):`, and both must keep the existing note that commit subjects stay conventional-commit style. - [x] Document the normalisation in `plugins/forgejo-cli/skills/forgejo-cli/SKILL.md` under the write section: one line noting `pr-title` / `pr-create` strip a conventional-commit type prefix, and that the type belongs on a label. - [x] Add a title-hygiene check to `plugins/tasks/skills/task-review/SKILL.md`: Step 12 already rewrites the title at mark-ready, so have it strip a conventional-commit type prefix alongside `WIP:` rather than only `WIP:`. Add a matching common-mistakes row. - [x] Add a common-mistakes row to `plugins/tasks/skills/task-orchestrate/SKILL.md` covering the same rule, so the one skill that drives a PR to merge without going through `task-new` also states it. - [x] Retitle PR #24 from `feat: add prioritize-next-features plugin` to `add prioritize-next-features plugin` and confirm it carries the `enhancement` label (the type signal the title gives up). - [x] Add a `## Toolchain` block to a repo-root `CLAUDE.md` declaring a `test:` command, so the new test tree is actually executed by `/task-orchestrate` and `/task-review` instead of sitting inert. Landed as `test: uv run --with pytest pytest tests -q` -- **pytest, not `unittest discover`**: plugin directories contain hyphens (`forgejo-cli`), so they are not importable packages and discovery cannot walk the src-mirroring layout. The same `CLAUDE.md` documents the mirroring convention and the PR-title rule. --- ## Notes **Why the script and not a git hook.** A hook fires on commits, not on forge API calls, and PR titles are set through `fj`. `fj_write.py` is the one path every PR title in this ecosystem passes through -- `task-new` Step 8, `task-implement`'s one-shot PR-create, and `task-review` Step 12 all route through it via the `forgejo-cli` skill. Normalising there covers every caller with one change. **Why strip rather than reject.** A non-zero exit mid-pass would strand the resolver with a pushed branch and no PR, and the correct title is mechanically derivable from the bad one -- there is nothing to ask the user. Stripping is idempotent and always leaves the title in the legal state. The printed notice keeps it visible instead of silent. **Why `WIP:` is handled first.** `WIP:` is Forgejo's draft signal and must survive; only the type token after it is stripped. `fj`'s title edit re-derives the draft flag from the title, so the order matters -- getting it wrong would undraft the PR. **Test-tree mirroring: scope of what was possible here.** The requirement is that the test tree mirror the source tree exactly. This repo had **no test tree and no source tree** to mirror when the work started: no `src/`, no `tests/`, no `pyproject.toml`, no test runner, and a CI workflow that only runs gitleaks. Its four Python files are standalone stdlib scripts scattered under `plugins/*/skills/*/scripts/`. So the mirroring convention is *established* here and applied to the one unit this PR adds, rather than retrofitted across the repo -- building out a repo-wide test tree for three pre-existing untested scripts is a separate piece of work with its own verification, and folding it in would have made this PR about test infrastructure rather than title hygiene. The convention and its root (`tests/`) are now fixed, so later work extends the mirror by adding files, not by redesigning the layout. **Toolchain: pytest, decided by the user.** The repo had no `CLAUDE.md` at all, so nothing in the task suite knew how to run the new tests -- `/task-review` Step 8 reads its commands from a `## Toolchain` block and skips what it does not find. The resolver left it unticked rather than invent one; the user chose pytest during the orchestrate pass. The originally-suggested `unittest discover -s tests -t .` turned out to be **unworkable**: discovery imports each directory as a package, and `forgejo-cli` / `herdr-cli` contain hyphens, so it fails with `Start directory is not importable`. Making it work would have meant renaming test directories away from their source names -- destroying the exact mirror this PR establishes. pytest collects by path, needs no `__init__.py`, and keeps the mirror intact. **Rejected: a `--allow-type-prefix` escape hatch.** `forgejo-cli` is a generic Forgejo wrapper, so an opt-out is defensible in principle, but no caller in this repo wants one and the goal is that a bad title is unrepresentable. YAGNI -- add it if a real consumer ever needs it. **Rejected: only fixing the prose.** That is the status quo. The rule has been written in two places since the skills were authored and PR #24 still shipped with `feat:`. **Scope boundary.** Commit subjects keep conventional-commit prefixes (`task-implement` Step 5, `worktree-and-branches.md`) and branch names keep their `feat/` / `fix/` prefixes. Neither is touched. Issue titles are unaffected.
fix: scaffold PR title type-prefix enforcement
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 4s
29ad35291e
Author
Collaborator

Resolver started. Worktree /Users/jerome/code/skills/.worktrees/enforce-pr-title-no-type-prefix. (Repo has no task:active label -- skipping the label, one-time UI setup.)

Resolver started. Worktree /Users/jerome/code/skills/.worktrees/enforce-pr-title-no-type-prefix. (Repo has no task:active label -- skipping the label, one-time UI setup.)
fix: strip conventional-commit type prefixes from PR titles
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 3s
b5c05356be
test: cover strip_type_prefix in a test tree mirroring src
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 3s
ad06c2f8ae
docs: note PR title type-prefix stripping in forgejo-cli skill
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 3s
aa9f127bc7
docs: forbid scoped type prefixes in PR titles explicitly
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 4s
c0d660e2f9
docs: make task-review strip type prefixes at mark-ready
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 4s
3fba21375e
Author
Collaborator

2026-07-19 -- resolver pass on fix/enforce-pr-title-no-type-prefix at 3fba213.

  • Steps done: 9 of 10. strip_type_prefix() + clean_pr_title() in fj_write.py,
    wired into cmd_title (PR path only) and cmd_create's PR branch; docstring
    examples de-anti-patterned; mirrored test tree established with coverage for the
    new helper; scoped-form prohibition named in task-new and
    worktree-and-branches.md; task-review Step 12 now strips a type prefix
    alongside WIP:; task-orchestrate common-mistakes row added; forgejo-cli
    SKILL.md documents the normalisation. PR #24 retitled to
    add prioritize-next-features plugin (it had already merged, so the retitle is
    cosmetic there, but it exercised the rule).

  • Spec changed mid-pass: the test-tree-mirrors-source-tree requirement arrived
    after the body was written and is now Step 4 in the body. Worth flagging what it
    could and could not mean here -- the repo had no tests/ and no src/ to mirror
    (no test runner, no manifest, gitleaks-only CI, four standalone scripts under
    plugins/*/skills/*/scripts/). So the convention is established and applied to
    the one unit this PR adds (tests/<source path>/test_<name>.py) rather than
    retrofitted over three pre-existing untested scripts, which is separate work.
    Rationale is in the body's Notes.

  • Deferred to /task-orchestrate: nothing runnable was executed this pass (no
    install, lint, typecheck, or test run -- resolver is read-only). The new test
    file has not been run.

  • Open loose end, needs a human call: this repo has no ## Toolchain block in
    any CLAUDE.md/AGENTS.md, and no CLAUDE.md at all. Until one declares a test:
    command, /task-review Step 8 has nothing to run and the new test file is inert.
    Left as the one unticked Step rather than scaffolded silently, since writing a
    Toolchain block needs the user's confirmation.

2026-07-19 -- resolver pass on `fix/enforce-pr-title-no-type-prefix` at 3fba213. - Steps done: 9 of 10. `strip_type_prefix()` + `clean_pr_title()` in `fj_write.py`, wired into `cmd_title` (PR path only) and `cmd_create`'s PR branch; docstring examples de-anti-patterned; mirrored test tree established with coverage for the new helper; scoped-form prohibition named in `task-new` and `worktree-and-branches.md`; `task-review` Step 12 now strips a type prefix alongside `WIP:`; `task-orchestrate` common-mistakes row added; `forgejo-cli` SKILL.md documents the normalisation. PR #24 retitled to `add prioritize-next-features plugin` (it had already merged, so the retitle is cosmetic there, but it exercised the rule). - Spec changed mid-pass: the test-tree-mirrors-source-tree requirement arrived after the body was written and is now Step 4 in the body. Worth flagging what it could and could not mean here -- the repo had no `tests/` and no `src/` to mirror (no test runner, no manifest, gitleaks-only CI, four standalone scripts under `plugins/*/skills/*/scripts/`). So the convention is established and applied to the one unit this PR adds (`tests/<source path>/test_<name>.py`) rather than retrofitted over three pre-existing untested scripts, which is separate work. Rationale is in the body's Notes. - Deferred to /task-orchestrate: nothing runnable was executed this pass (no install, lint, typecheck, or test run -- resolver is read-only). The new test file has not been run. - Open loose end, needs a human call: this repo has **no `## Toolchain` block** in any CLAUDE.md/AGENTS.md, and no CLAUDE.md at all. Until one declares a `test:` command, `/task-review` Step 8 has nothing to run and the new test file is inert. Left as the one unticked Step rather than scaffolded silently, since writing a Toolchain block needs the user's confirmation.
docs: add Toolchain block so the mirrored test tree actually runs
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 3s
1687884c5a
pytest, not unittest discover -- plugin dirs contain hyphens and are not
importable packages, so discovery cannot walk the src-mirroring layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Collaborator

Orchestrate pass -- test loop green, last Step closed.

  • Toolchain decided (user call). Added a repo-root CLAUDE.md with a
    ## Toolchain block: test: uv run --with pytest pytest tests -q. The Step's
    originally-suggested unittest discover -s tests -t . does not work here --
    discovery imports each directory as a package and forgejo-cli contains a
    hyphen, so it dies with Start directory is not importable. Fixing that would
    have meant renaming test dirs away from their source names, destroying the exact
    mirror. pytest collects by path and keeps the mirror intact. Rationale folded into
    the body's Notes; Step 10 now ticked.
  • Tests: 11 passed (uv run --with pytest pytest tests -q).
  • Smoke, verified by hand: pr-title 99 "feat(module): ship it" -> ship it;
    pr-title 99 "WIP: fix: broken thing" -> WIP: broken thing (draft signal
    survives); issue-title 99 "feat: keep this" -> unchanged, as specified.
  • Lint / typecheck / build: none for this repo (prose + stdlib scripts, no manifest).

Ready for review.

Orchestrate pass -- test loop green, last Step closed. - **Toolchain decided (user call).** Added a repo-root `CLAUDE.md` with a `## Toolchain` block: `test: uv run --with pytest pytest tests -q`. The Step's originally-suggested `unittest discover -s tests -t .` does not work here -- discovery imports each directory as a package and `forgejo-cli` contains a hyphen, so it dies with `Start directory is not importable`. Fixing that would have meant renaming test dirs away from their source names, destroying the exact mirror. pytest collects by path and keeps the mirror intact. Rationale folded into the body's Notes; Step 10 now ticked. - **Tests: 11 passed** (`uv run --with pytest pytest tests -q`). - **Smoke, verified by hand:** `pr-title 99 "feat(module): ship it"` -> `ship it`; `pr-title 99 "WIP: fix: broken thing"` -> `WIP: broken thing` (draft signal survives); `issue-title 99 "feat: keep this"` -> unchanged, as specified. - Lint / typecheck / build: none for this repo (prose + stdlib scripts, no manifest). Ready for review.
Author
Collaborator

/task-review verdict: concerns

LOC: +213/-12

Every Step in the body lands in the diff, and the Goal is proven end-to-end: pr-title 1 "feat(x): y" -> y, WIP: fix: broken -> WIP: broken (draft signal survives), issue-title untouched, pr-create normalised. 11 tests pass. Two medium issues hold it back from ok: the branch is one commit behind main, and the new test module's docstring instructs readers to run the exact unittest discover command the PR's own Notes document as unworkable.

Findings

medium mismatch -- tests/plugins/forgejo-cli/skills/forgejo-cli/scripts/test_fj_write.py:10

The module docstring ends with Run: `uv run --no-project -m unittest discover -s tests -t .` from the repo root. That command is the one the PR body's Notes and CLAUDE.md both single out as impossible here -- running it fails with ImportError: Start directory is not importable: '.../tests', exactly as the Notes predict. Step 4 asked this docstring to document the mirroring rule, and it does; but it also ships a run instruction that contradicts the ## Toolchain block's uv run --with pytest pytest tests -q and does not work. Replace it with the pytest invocation.

medium stale -- branch

fix/enforce-pr-title-no-type-prefix is behind origin/main by one commit (8cd81a7 feat: add prioritize-next-features plugin (#24)). It merges cleanly (git merge-tree returns a tree with no conflicts), so the diff and the green test/smoke evidence above still reflect what will land -- but catch the branch up before merge so the gates run against the real post-merge state.

## /task-review verdict: `concerns` **LOC:** +213/-12 > Every Step in the body lands in the diff, and the Goal is proven end-to-end: `pr-title 1 "feat(x): y"` -> `y`, `WIP: fix: broken` -> `WIP: broken` (draft signal survives), `issue-title` untouched, `pr-create` normalised. 11 tests pass. Two medium issues hold it back from `ok`: the branch is one commit behind `main`, and the new test module's docstring instructs readers to run the exact `unittest discover` command the PR's own Notes document as unworkable. ## Findings ### `medium` mismatch -- `tests/plugins/forgejo-cli/skills/forgejo-cli/scripts/test_fj_write.py:10` The module docstring ends with ``Run: `uv run --no-project -m unittest discover -s tests -t .` from the repo root.`` That command is the one the PR body's Notes and `CLAUDE.md` both single out as impossible here -- running it fails with `ImportError: Start directory is not importable: '.../tests'`, exactly as the Notes predict. Step 4 asked this docstring to document the mirroring rule, and it does; but it also ships a run instruction that contradicts the `## Toolchain` block's `uv run --with pytest pytest tests -q` and does not work. Replace it with the pytest invocation. ### `medium` stale -- `branch` `fix/enforce-pr-title-no-type-prefix` is behind `origin/main` by one commit (`8cd81a7 feat: add prioritize-next-features plugin (#24)`). It merges cleanly (`git merge-tree` returns a tree with no conflicts), so the diff and the green test/smoke evidence above still reflect what will land -- but catch the branch up before merge so the gates run against the real post-merge state.
The docstring still named unittest discover, which CLAUDE.md and the PR
notes document as unworkable here -- it fails with ImportError because
plugin dirs contain hyphens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/enforce-pr-title-no-type-prefix
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 4s
75a018415d
Author
Collaborator

/task-review verdict: ok

LOC: +218/-12

Review pass 2. Both findings from the prior pass are resolved: the test module's docstring now names uv run --with pytest pytest tests -q and explains why unittest discover cannot walk a hyphenated plugin tree, and the branch has been merged up to origin/main (75a0184) and pushed, so the gates ran on the base that will actually land. 11 tests pass and the smoke proof holds end-to-end: pr-title 1 "feat(x): y" -> y, WIP: fix: broken thing -> WIP: broken thing (draft signal survives), feat!: bang normalised on pr-create, issue-title and the near-miss fixture loader both untouched. One cosmetic docstring nit noted below, not worth holding the merge.

Findings

low bloat -- plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py:28

The new "PR titles carry no conventional-commit type prefix" paragraph was inserted inside the Usage: example block rather than after it, orphaning the final example (fj_write.py --dry-run pr-merge 9) below the prose so it reads as a stray line detached from the list it belongs to. The content is correct and well-placed topically -- this is purely where the paragraph sits. Moving it below the last example line restores the block. Cosmetic; safe to fix on a later pass.

## /task-review verdict: `ok` **LOC:** +218/-12 > Review pass 2. Both findings from the prior pass are resolved: the test module's docstring now names `uv run --with pytest pytest tests -q` and explains why `unittest discover` cannot walk a hyphenated plugin tree, and the branch has been merged up to `origin/main` (`75a0184`) and pushed, so the gates ran on the base that will actually land. 11 tests pass and the smoke proof holds end-to-end: `pr-title 1 "feat(x): y"` -> `y`, `WIP: fix: broken thing` -> `WIP: broken thing` (draft signal survives), `feat!: bang` normalised on `pr-create`, `issue-title` and the near-miss `fixture loader` both untouched. One cosmetic docstring nit noted below, not worth holding the merge. ## Findings ### `low` bloat -- `plugins/forgejo-cli/skills/forgejo-cli/scripts/fj_write.py:28` The new "PR titles carry no conventional-commit type prefix" paragraph was inserted *inside* the `Usage:` example block rather than after it, orphaning the final example (`fj_write.py --dry-run pr-merge 9`) below the prose so it reads as a stray line detached from the list it belongs to. The content is correct and well-placed topically -- this is purely where the paragraph sits. Moving it below the last example line restores the block. Cosmetic; safe to fix on a later pass.
claude changed title from WIP: forbid conventional-commit type prefixes in PR titles to forbid conventional-commit type prefixes in PR titles 2026-07-19 08:34:55 +03:00
docs: move the type-prefix note out of the Usage example block
All checks were successful
gitleaks / gitleaks (pull_request) Successful in 3s
d975a5ad63
It was orphaning the final --dry-run example below prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jerome merged commit 85f098aac7 into main 2026-07-19 08:36:50 +03:00
jerome deleted branch fix/enforce-pr-title-no-type-prefix 2026-07-19 08:36:50 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
public/chiche-plugins!25
No description provided.