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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ New here? Start with [plugins/concise/README.md](plugins/concise/README.md).
- Comment blocks longer than 2 lines in `Write`, `Edit`, `MultiEdit`, and Codex `apply_patch` additions
- New files longer than 300 lines
- Inline `gh pr` and `gh issue` bodies with more than 1 prose paragraph or more than 3 sentences in a paragraph
- Em dashes and 44 categories of AI writing patterns, both off until you turn them on
- Em dashes and 47 categories of AI writing patterns, both off until you turn them on
- Output from pytest, `go test`, npm test, Jest, and Vitest; each run's full output is kept in its own log under `$TMPDIR/concise-test-filter-<uid>/<session>/`, and the path is printed with the filtered output

Structured GitHub bodies that use headings and lists are allowed. A denied call includes the exact limit and location. After 2 denied retries for the same target, Concise permits the next attempt and flags it.
Expand Down Expand Up @@ -71,7 +71,7 @@ Start a new omp session. omp loads the hooks as an extension at session start, s

- [plugins/concise/README.md](plugins/concise/README.md): the entry point. Quick start, what each check does, the 8 presets, and the escape hatches.
- [plugins/concise/docs/configuration.md](plugins/concise/docs/configuration.md): every config key with its default, the 5 config layers, `mode`, `allowList`, `bypass`, and logging.
- [plugins/concise/docs/categories.md](plugins/concise/docs/categories.md): all 44 AI writing categories with an example, a fix, the presets, and the scopes.
- [plugins/concise/docs/categories.md](plugins/concise/docs/categories.md): all 47 AI writing categories with an example, a fix, the presets, and the scopes.
- [plugins/concise/docs/environment.md](plugins/concise/docs/environment.md): every `BEC_` variable and 3 worked scenarios, including a cloud agent with no config file.
- [plugins/concise/docs/packs.md](plugins/concise/docs/packs.md): the pack file format, the 3 pattern kinds, `detect(text, ctx)`, and the validator and renderer commands.
- [plugins/concise/docs/tools.md](plugins/concise/docs/tools.md): the `concise-config` CLI, the MCP tools, the settings skills, and the writing-sample tuner.
Expand Down
10 changes: 5 additions & 5 deletions plugins/concise/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ To keep it, send the identical write again to confirm. To fix it, read <plugin>/
| File length | A new file over 300 lines, on `Write` or `*** Add File` | `[concise] /tmp/demo/big.js would be 305 lines (limit 300).` |
| PR and issue bodies | A `gh` body over 1 prose paragraph, or a paragraph over 3 sentences | `[concise] PR/issue body is too verbose: a paragraph has 4 sentences (limit 3)` |
| Em dashes | `U+2014`, plus `U+2013` when `enDash` is true, plus `--` between word characters when `doubleHyphen` is true | `The parser runs first — then it rejects the file.` |
| AI writing patterns | 44 categories of phrasing, commit hygiene, punctuation, and text statistics | `We delve into the parser.` |
| AI writing patterns | 47 categories of phrasing, commit hygiene, punctuation, and text statistics | `We delve into the parser.` |
| Dictionary | Terms you list in `features.dictionary.entries` | `[concise:dictionary:blacklist] 1 match at line 3: "blacklisted" (denylist).` |

A bulleted `## Summary` body is never flagged as a verbose PR body. Only unstructured prose is.
Expand All @@ -110,7 +110,7 @@ After 2 denied retries on the same target, the hook allows the action and flags
| `minimal` | A team that only wants chat boilerplate gone | 4 categories: `chatbot`, `sycophancy`, `ai-identity`, and `ai-attribution`. |
| `git` | Commit messages, PR bodies, and reviews | 7 commit, PR, and review categories. |
| `statistical` | A writing audit over long documents | 10 text statistics categories. |
| `all` | Every rule, `ste` included | 44 categories. |
| `all` | Every rule, `ste` included | 47 categories. |

Pick one:

Expand All @@ -128,14 +128,14 @@ Setting `features.aiWriting.categories` to a list of category ids replaces the p

## Categories

The 44 categories fall into 5 groups.
The 47 categories fall into 5 groups.

| Group | Count | Flagged example | Fix |
|---|---|---|---|
| Phrase categories | 14 | `In conclusion` (`closers`) | End on the last fact. |
| Phrase categories | 16 | `In conclusion` (`closers`) | End on the last fact. |
| Rhetorical categories | 11 | `studies show` (`vague-attribution`) | Cite the study. |
| Commit and review categories | 7 | `Co-authored-by: Claude` (`ai-attribution`) | Drop the trailer. |
| Punctuation categories | 2 | an arrow character (`unicode-glyphs`) | Use `->`. |
| Punctuation categories | 3 | an arrow character (`unicode-glyphs`) | Use `->`. |
| Text statistics categories | 10 | passive voice over 30% of sentences (`passive-voice`) | Name the actor. |

The full table, with every category id, its presets, and the scopes it runs in, is in [docs/categories.md](docs/categories.md). The flagged text and the replacement for every pattern are in [skills/concise-rules/references/ai-speak-patterns.md](skills/concise-rules/references/ai-speak-patterns.md).
Expand Down
6 changes: 3 additions & 3 deletions plugins/concise/docs/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Docs index

Created: 2026-10-05
Last updated: 2026-10-07
Last updated: 2026-10-08

| Doc | Read it to | Created | Last updated |
|---|---|---|---|
| [configuration.md](configuration.md) | Find a config key, its default, the layer order, the merge rules, `mode`, and the dictionary. | 2026-09-04 | 2026-10-06 |
| [tools.md](tools.md) | Change settings with the `concise-config` CLI or the MCP tools, or tune them from writing samples. | 2026-10-05 | 2026-10-05 |
| [environment.md](environment.md) | Control a session with `BEC_` variables. | 2026-09-04 | 2026-10-05 |
| [categories.md](categories.md) | Look up an AI writing category, its presets, and its scopes. | 2026-09-04 | 2026-09-29 |
| [packs.md](packs.md) | Write or check a pattern pack. | 2026-09-04 | 2026-09-05 |
| [categories.md](categories.md) | Look up an AI writing category, its presets, and its scopes. | 2026-09-04 | 2026-10-08 |
| [packs.md](packs.md) | Write or check a pattern pack. | 2026-09-04 | 2026-10-08 |
| [host-features.md](host-features.md) | Check which Claude Code, Codex, and omp hook, skill, and MCP features the manifests and the omp extension use, with the host doc sources and the live test results. | 2026-10-05 | 2026-10-07 |

Read [host-features.md](host-features.md) and its sources before you change `hooks/hooks.json`, `hooks/codex.json`, `.mcp.json`, `.codex-mcp.json`, `package.json`, `omp/`, or a plugin manifest.
7 changes: 5 additions & 2 deletions plugins/concise/docs/categories.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Categories

The `aiWriting` feature ships 44 categories. Each category is one pack file under `hooks/lib/patterns/`. This page lists every category, the presets that turn it on, and the scopes it runs in.
The `aiWriting` feature ships 47 categories. Each category is one pack file under `hooks/lib/patterns/`, except `claude-tells`, which spans `claude-tells.json`, `closing-offer.json`, and `backtick-branches.mjs`. This page lists every category, the presets that turn it on, and the scopes it runs in.

Pick categories with `features.aiWriting.preset`, or replace the preset list with `features.aiWriting.categories`. Both keys are described in [configuration.md](configuration.md). The flagged text and the replacement for every pattern are in [../skills/concise-rules/references/ai-speak-patterns.md](../skills/concise-rules/references/ai-speak-patterns.md).

Expand All @@ -12,7 +12,9 @@ Scopes across this group: `files`, `comments`, `gh`, `commit`, `reply`. The scop

| id | What it catches | Flagged example | Fix | Presets | Scopes |
|---|---|---|---|---|---|
| `british-spelling` | British spellings in text that otherwise uses American English. | `behaviour` | use the American spelling | `ryan`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
| `chatbot` | Assistant boilerplate that belongs to a chat window. | `Great question` | delete the line | `default`, `ryan`, `technical`, `minimal`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
| `claude-tells` | Phrasing that Claude 5.x models write far more often than people do. | `still not committed, on purpose` | `still intentionally not committed` | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
| `closers` | Endings that restate the text instead of stopping. | `In conclusion` | end on the last fact | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
| `contrast` | Frames that define the subject by what it is not. | `it's not a config change, it's a rewrite` | state the positive claim | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
| `copula` | Verbs that interpret the subject instead of naming what it does. | `serves as` | say what it does | `ryan`, `ste`, `all` | `files`, `comments`, `gh`, `commit`, `reply` |
Expand Down Expand Up @@ -61,10 +63,11 @@ Scopes across this group: `comments`, `gh`, `commit`, `command`, `reply`. The sc

## Punctuation categories

Scopes across this group: `comments`, `gh`, `commit`, `command`, `reply`. The scope column below gives the scopes for each category.
Scopes across this group: `files`, `comments`, `gh`, `commit`, `command`, `reply`. The scope column below gives the scopes for each category.

| id | What it catches | Flagged example | Fix | Presets | Scopes |
|---|---|---|---|---|---|
| `hidden-characters` | Invisible or look-alike characters that model output and chat UIs leave in text. | a zero-width space | remove the character | `default`, `ryan`, `technical`, `git`, `all` | `files`, `comments`, `gh`, `commit`, `command`, `reply` |
| `smart-punctuation` | Curly quotes and the ellipsis character typical of text pasted from a chat UI. | a curly apostrophe | use a straight apostrophe | `default`, `ryan`, `technical`, `all` | `comments`, `commit`, `gh`, `command`, `reply` |
| `unicode-glyphs` | Arrows, box drawing, dingbats, and geometric symbols used in place of plain ASCII or words. | an arrow character | use `->` | `default`, `ryan`, `technical`, `all` | `commit`, `gh`, `command`, `reply` |

Expand Down
3 changes: 2 additions & 1 deletion plugins/concise/docs/packs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pattern packs

Every category is one pack file. The built-in packs live under `hooks/lib/patterns/`: `ai/` for writing patterns, `git/` for commit, PR, and review checks, `prose/` for punctuation. A project adds its own packs the same way.
Most categories are one pack file. A pack whose `category` is a string joins that category, as `closing-offer.json` and `backtick-branches.mjs` join `claude-tells`. The built-in packs live under `hooks/lib/patterns/`: `ai/` for writing patterns, `git/` for commit, PR, and review checks, `prose/` for punctuation. A project adds its own packs the same way.

A `.mjs` pack executes code from the checkout. The hook imports it, so its top-level code and its `detect` function run in the hook process with your permissions on every tool call. Read a `.mjs` pack before you add it. A `.json` pack holds data only.

Expand Down Expand Up @@ -167,6 +167,7 @@ export default {
| `ctx.path` | The file path, or `null` for a reply, commit message, `gh` body, or command. |
| `ctx.scope` | The scope name the text came from. |
| `ctx.options` | The pack's `options`, merged with `features.aiWriting.options.<packId>`. |
| `ctx.raw` | The text before code spans, fences, comments, and URLs are blanked. It has the same length as `ctx.stats.text`, so an index into one is an index into the other. |
| `ctx.stats.text` | The text being scanned. |
| `ctx.stats.words()` | `{ text, start, end }` per word. |
| `ctx.stats.sentences()` | `{ text, start, end }` per sentence. |
Expand Down
28 changes: 28 additions & 0 deletions plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// stripCode blanks code spans before packs run, so this check reads ctx.raw.
// Without the engine change in engine.patch, ctx.raw is undefined and the pack reports nothing.
const LEAD = String.raw`\b(?:from|into|onto|against|off|to|on|of|rebased? on|merged? into|checkout|switch to|pushed? to)\s+`;

function branchPattern(branches) {
const names = branches.map((b) => b.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|");
return new RegExp(`${LEAD}(?<lead>\`(?:${names})\`)|(?<tail>\`(?:${names})\`)(?=\\s+branch(?:es)?\\b)`, "gi");
}

export default {
id: "backtick-branches",
feature: "aiWriting",
category: "claude-tells",
scope: ["gh", "commit", "reply"],
presets: ["default", "ryan", "technical", "all"],
options: { branches: ["main", "master", "dev", "develop", "development", "staging", "stage", "production", "prod", "release", "trunk", "next", "beta", "canary"] },
notes: "Flags a branch name in backticks after `from`, `into`, `onto`, `against`, or a similar word, or before `branch`. Prose files stay out of scope: docs often set branch names as code by convention. Set `options.branches` to match the repo's branch names.",
detect(text, ctx) {
if (typeof ctx.raw !== "string" || ctx.raw.length !== text.length) return [];
const re = branchPattern(ctx.options.branches || []);
const out = [];
for (const m of ctx.raw.matchAll(re)) {
const hit = m.groups.lead || m.groups.tail;
out.push({ index: m.index + m[0].indexOf(hit), match: hit, fix: "drop the backticks around the branch name" });
}
return out;
},
};
28 changes: 28 additions & 0 deletions plugins/concise/hooks/lib/patterns/ai/british-spelling.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"id": "british-spelling",
"feature": "aiWriting",
"category": { "id": "british-spelling", "label": "British spelling", "description": "British spellings in text that otherwise uses American English.", "tags": ["phrase", "claude"] },
"source": "Measured on Claude Code transcripts: Opus 5.5 wrote `behaviour` in 31% of uses, `labelled` in 58%, and `grey` in 77%; npm READMEs used them in 11%, 20%, and 30%",
"presets": ["ryan", "all"],
"scope": ["files", "comments", "gh", "commit", "reply"],
"notes": "A repo that writes British English lists `british-spelling` in `excludePacks`. `cancelled`, `judgement`, `acknowledgement`, `towards`, and `dialogue` are left out because American writers use them too. `analyses` is left out because it is also the plural of `analysis`. Elixir's `@behaviour` attribute sits in code, which the scan skips.",
"patterns": [
{ "regex": "\\bbehaviour(?:s|al|ally)?\\b", "fix": "behavior", "show": "`behaviour`, `behaviours`, `behavioural`" },
{ "regex": "\\bcolour(?:s|ed|ing|ful)?\\b", "fix": "color", "show": "`colour`, `colours`, `coloured`" },
{ "regex": "\\b(?:fav|hon|neighb|lab|flav|hum|rum|harb|arm|vap)our(?:s|ed|ing|ite|ites|able|hood)?\\b", "fix": "drop the `u`: `favor`, `honor`, `neighbor`", "show": "`favour`, `honour`, `neighbour`, `labour`, `flavour`, `humour`, `rumour`, `harbour`, `armour`, `vapour`" },
{ "regex": "\\b(?:label|model|travel|signal|fuel|level|tunnel|channel)l(?:ed|ing)\\b", "fix": "one `l`: `labeled`, `modeling`", "show": "`labelled`, `modelling`, `travelled`, `signalled`, `levelled`, `channelled`" },
{ "regex": "\\b(?:cent|met|lit|fib|theat)re(?:s|d)?\\b", "fix": "`-er`: `center`, `meter`", "show": "`centre`, `metre`, `litre`, `fibre`, `theatre`" },
{ "regex": "\\b(?:de)?(?:organi|recogni|reali|initiali|seriali|normali|optimi|minimi|maximi|prioriti|customi|finali|synchroni|saniti|authori|summari|categori|tokeni|visuali|standardi|generali|characteri|memori|parameteri|containeri|virtuali|centrali|capitali|emphasi|apologi|materiali|speciali|stabili|anonymi|randomi|locali|internationali|critici|digiti|moderni|personali)s(?:e|es|ed|ing|ation|ations|er|ers)\\b", "fix": "`-ize`, `-ization`", "show": "`-ise` and `-isation` forms such as `normalise`, `initialised`, `organisation`" },
{ "regex": "\\b(?:analy|paraly|cataly)s(?:e|ed|ing)\\b", "fix": "`-yze`: `analyze`", "show": "`analyse`, `analysed`, `analysing`" },
{ "phrase": "grey(?:s|ed|ing)?", "fix": "gray" },
{ "phrase": "whilst", "fix": "while" },
{ "phrase": "amongst", "fix": "among" },
{ "phrase": "programmes?", "fix": "program" },
{ "phrase": "licence[ds]?", "fix": "license" },
{ "phrase": "defence|offence", "fix": "defense, offense" },
{ "phrase": "catalogue[ds]?", "fix": "catalog" },
{ "phrase": "artefacts?", "fix": "artifact" },
{ "phrase": "spelt|learnt", "fix": "spelled, learned" },
{ "phrase": "per cent", "fix": "percent" }
]
}
23 changes: 23 additions & 0 deletions plugins/concise/hooks/lib/patterns/ai/claude-tells.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"id": "claude-tells",
"feature": "aiWriting",
"category": { "id": "claude-tells", "label": "Claude tells", "description": "Phrasing that Claude 5.x models write far more often than people do.", "tags": ["phrase", "claude"] },
"source": "Measured on 1.29M words of Opus 5.5 Claude Code replies against 1.13M words of npm READMEs and 87K words of pre-2023 commits; Anthropic's Opus 5.5 and Fable 5.1 prompting guides",
"presets": ["default", "ryan", "technical", "all"],
"scope": ["files", "comments", "gh", "commit", "reply"],
"notes": "Every tier-1 row fired at most 0.02 times per 10,000 words in the README and commit baselines. The status-label row needs a verbless fragment at the start of a line or sentence, so `This is a known limitation, not a bug` stays with the `contrast` pack. The closing offer lives in `closing-offer.json` so it can be excluded alone.",
"patterns": [
{ "regex": "\\b(?:the |this |that |its )?(?:docs?|documentation|README|comment|changelog|spec|plan|proposal|code|tests?|commit|PR|diff|issue|ticket|log|output|page|guide|report|schema|config)s? (?:itself )?(?:admits?|concedes?|confess(?:es)?|lies|pretends?|swears?)\\b", "fix": "say what the text states: `the doc says X`", "show": "`the doc admits`, `the code pretends`, `the test lies`" },
{ "regex": "\\b(?:admits|concedes|confesses) (?:it|as much)\\b", "fix": "say what the text states", "show": "`admits it`, `concedes as much`" },
{ "regex": "(?:^|[.!?:]\\s+|[-*]\\s+(?:\\*\\*)?)(?:Pre-?existing|Existing|Intentional|Deliberate|Expected|Cosmetic|Unrelated|Harmless|Benign|Separate|Known|Upstream|Flaky|Latent)\\b[^.!?\\n,]{0,30}(?<hit>,\\s+not\\s+(?:a |an |the )?[\\w-]+)", "flags": "gim", "fix": "cut the `not X` tail, the label already says it", "show": "`Existing bug, not a regression`" },
{ "regex": "\\bthe (?<hit>big|huge|giant|scary) (?:one|migration|refactor|rewrite|change|fix|bug|risk|issue|problem|question|PR|diff|win|gap|piece|part|item|table|query|file|job|deploy|release|merge|commit|cleanup|update|upgrade)s?\\b", "fix": "cut the size word, or give the size", "show": "`the big migration`, `the huge refactor`" },
{ "regex": "\\b(?:covers?|covered|exercises?|exercised|hits?|reach(?:es|ed)?|tests?|tested|guards?|skips?|suspect|traced?) (?:this|that|the same|either|each|both|the other) (?:code )?(?<hit>path)\\b", "fix": "name the branch: the function, condition, or flag", "show": "`covers this path`, `hits that path`" },
{ "phrase": "on purpose", "fix": "put `intentionally` before the verb, or give the reason" },
{ "regex": ",\\s*(?:by design|as intended|as designed)(?=\\s*[.!?;:)]|\\s*$)", "flags": "gim", "fix": "cut the tag, or give the reason", "show": "`, by design.`, `, as intended.`" },
{ "regex": ",\\s*(?:with|given|plus) your [^.!?\\n,]{0,40}?\\b(?:note|caveat|point|comment|context|feedback|concern|flag|constraint|correction|request|ask|framing|numbers?|rule|instructions?)\\b", "fix": "put the aside in parentheses, or make it a clause", "show": "`, with your low call volume note`" },
{ "phrase": "quietly", "fix": "cut it, or say what is not logged or reported" },
{ "regex": "\\bearns? (?:its|their) (?:keep|place)\\b", "fix": "say what it does", "show": "`earns its keep`, `earns its place`" },
{ "regex": "\\b(?:a|one) real (?:bug|risk|problem|issue|gap|concern|regression|win|finding)\\b|\\bthe real (?:fix|bug|risk|issue|problem|gap|cause|culprit|cost|blocker|story)\\b", "fix": "cut `real`", "tier": 2, "show": "`a real bug`, `the real gap`, `the real story`" },
{ "regex": "\\breads (?:well|cleanly|naturally)\\b", "fix": "say what works", "tier": 2, "show": "`reads well`, `reads cleanly`" }
]
}
Loading
Loading