From 1ad5c76b44d4946709a4dace5dace6951a27c1fe Mon Sep 17 00:00:00 2001 From: Ryan Yannelli Date: Thu, 8 Oct 2026 15:53:54 +0000 Subject: [PATCH 1/2] feat(patterns): pass unblanked text to detect packs as ctx.raw stripCode blanks code spans, fences, HTML comments, and URLs before packs run, so no pack could see a backticked word. proseSpans now keeps the original span as raw, and styleFindings passes it to scanAiWriting as ctx.raw. Offsets match the scanned text. --- plugins/concise/docs/packs.md | 1 + plugins/concise/hooks/lib/prose.mjs | 4 ++-- plugins/concise/hooks/lib/style-check.mjs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/concise/docs/packs.md b/plugins/concise/docs/packs.md index c7871f7..35e5a84 100644 --- a/plugins/concise/docs/packs.md +++ b/plugins/concise/docs/packs.md @@ -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.`. | +| `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. | diff --git a/plugins/concise/hooks/lib/prose.mjs b/plugins/concise/hooks/lib/prose.mjs index 2b6b5eb..3108149 100644 --- a/plugins/concise/hooks/lib/prose.mjs +++ b/plugins/concise/hooks/lib/prose.mjs @@ -51,8 +51,8 @@ export function extOf(filePath) { export const isProsePath = (filePath) => PROSE_EXTENSIONS.includes(extOf(filePath)); export function proseSpans(text, path) { - if (isProsePath(path)) return [{ text: stripCode(text), line: 1 }]; - return scanComments(text, path).map((run) => ({ text: run.text, line: run.startLine })); + if (isProsePath(path)) return [{ text: stripCode(text), raw: text, line: 1 }]; + return scanComments(text, path).map((run) => ({ text: run.text, raw: run.text, line: run.startLine })); } const MESSAGE_FLAG = /(?:^|\s)(?:--message(?:=|\s+)|-[A-Za-z]*m\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g; diff --git a/plugins/concise/hooks/lib/style-check.mjs b/plugins/concise/hooks/lib/style-check.mjs index c054a7f..30a4380 100644 --- a/plugins/concise/hooks/lib/style-check.mjs +++ b/plugins/concise/hooks/lib/style-check.mjs @@ -135,7 +135,7 @@ export function styleFindings(text, path, config, scope = "files", hook = null) if (keep(at(hit.line), hit.match)) dictionary.push({ ...hit, line: at(hit.line) }); } if (packs.length === 0) continue; - const hits = scanAiWriting(span.text, { packs, allow: resolved.allow, ctx: { path, scope }, problems: runtime }); + const hits = scanAiWriting(span.text, { packs, allow: resolved.allow, ctx: { path, scope, raw: span.raw }, problems: runtime }); for (const hit of hits) { if (keep(at(hit.line), hit.match)) aiWriting.push({ ...hit, line: at(hit.line) }); } From 7d60f99bbb18b678ee7aeb10890e36c6ab486f40 Mon Sep 17 00:00:00 2001 From: Ryan Yannelli Date: Thu, 8 Oct 2026 15:53:59 +0000 Subject: [PATCH 2/2] feat(patterns): add Claude 5.5 tell and hidden-character packs Add three aiWriting categories, measured on 1.29M words of Opus 5.5 Claude Code replies against npm READMEs and pre-2023 commits: - claude-tells: artifact verbs, status-label tails, size words, vague path, trailing tags, user-note asides, earns its keep, a closing offer (closing-offer.json), and backticked branch names (backtick-branches.mjs, reads ctx.raw). Presets default, ryan, technical, all. - british-spelling: -our, -lled, -re, -ise, -yse forms and a few words. Presets ryan, all. - hidden-characters: zero-width, tag, and bidi characters, soft and non-breaking hyphens, narrow no-break spaces, chat citation residue. Presets default, ryan, technical, git, all. ai-attribution now flags the Claude-Session, Made-with: Cursor, and Agent-Logs-Url trailers. Four notes and one fixture that the new rows flag are reworded. Docs list 47 categories. --- README.md | 4 +- plugins/concise/README.md | 10 +- plugins/concise/docs/INDEX.md | 6 +- plugins/concise/docs/categories.md | 7 +- plugins/concise/docs/packs.md | 2 +- .../lib/patterns/ai/backtick-branches.mjs | 28 ++++ .../lib/patterns/ai/british-spelling.json | 28 ++++ .../hooks/lib/patterns/ai/claude-tells.json | 23 +++ .../hooks/lib/patterns/ai/closing-offer.json | 12 ++ .../lib/patterns/git/ai-attribution.json | 5 +- .../hooks/lib/patterns/git/ai-identity.json | 2 +- .../hooks/lib/patterns/git/benefit-tail.json | 2 +- .../lib/patterns/prose/hidden-characters.json | 20 +++ .../lib/patterns/prose/smart-punctuation.json | 2 +- .../references/ai-speak-patterns-2.md | 124 +++++++-------- .../references/ai-speak-patterns-3.md | 128 +++++++-------- .../references/ai-speak-patterns-4.md | 81 +++++++++- .../references/ai-speak-patterns.md | 147 +++++++++--------- plugins/concise/skills/concise-tune/SKILL.md | 2 +- plugins/concise/test/packs-hooks-tests.mjs | 11 ++ .../concise/test/patterns-cases/claude.json | 30 ++++ .../test/patterns-cases/rhetorical.json | 2 +- 22 files changed, 460 insertions(+), 216 deletions(-) create mode 100644 plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs create mode 100644 plugins/concise/hooks/lib/patterns/ai/british-spelling.json create mode 100644 plugins/concise/hooks/lib/patterns/ai/claude-tells.json create mode 100644 plugins/concise/hooks/lib/patterns/ai/closing-offer.json create mode 100644 plugins/concise/hooks/lib/patterns/prose/hidden-characters.json create mode 100644 plugins/concise/test/patterns-cases/claude.json diff --git a/README.md b/README.md index 3e13895..afc8e75 100644 --- a/README.md +++ b/README.md @@ -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-//`, 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. @@ -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. diff --git a/plugins/concise/README.md b/plugins/concise/README.md index f11da3e..6529fdc 100644 --- a/plugins/concise/README.md +++ b/plugins/concise/README.md @@ -88,7 +88,7 @@ To keep it, send the identical write again to confirm. To fix it, read / | 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. @@ -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: @@ -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). diff --git a/plugins/concise/docs/INDEX.md b/plugins/concise/docs/INDEX.md index e30639e..798be36 100644 --- a/plugins/concise/docs/INDEX.md +++ b/plugins/concise/docs/INDEX.md @@ -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. diff --git a/plugins/concise/docs/categories.md b/plugins/concise/docs/categories.md index 2e5b707..b837ab3 100644 --- a/plugins/concise/docs/categories.md +++ b/plugins/concise/docs/categories.md @@ -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). @@ -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` | @@ -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` | diff --git a/plugins/concise/docs/packs.md b/plugins/concise/docs/packs.md index 35e5a84..660cca0 100644 --- a/plugins/concise/docs/packs.md +++ b/plugins/concise/docs/packs.md @@ -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. diff --git a/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs b/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs new file mode 100644 index 0000000..1ce9561 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs @@ -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}(?\`(?:${names})\`)|(?\`(?:${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; + }, +}; diff --git a/plugins/concise/hooks/lib/patterns/ai/british-spelling.json b/plugins/concise/hooks/lib/patterns/ai/british-spelling.json new file mode 100644 index 0000000..435fa25 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/british-spelling.json @@ -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" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/ai/claude-tells.json b/plugins/concise/hooks/lib/patterns/ai/claude-tells.json new file mode 100644 index 0000000..ecf3ec0 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/claude-tells.json @@ -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}(?,\\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 (?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 )?(?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`" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/ai/closing-offer.json b/plugins/concise/hooks/lib/patterns/ai/closing-offer.json new file mode 100644 index 0000000..6b094a6 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/closing-offer.json @@ -0,0 +1,12 @@ +{ + "id": "closing-offer", + "feature": "aiWriting", + "category": "claude-tells", + "source": "Anthropic's Opus 5.5 prompting guide lists \"an offer to carry on with something unless the user would prefer otherwise\" as a habit to suppress; 1.93 per 10,000 words in Opus 5.5 replies, none in the baselines", + "presets": ["default", "ryan", "technical", "all"], + "scope": ["gh", "reply"], + "notes": "Fires only when the offer is the last sentence of the text. A question followed by a reason, such as `Do you want me to delete it? It has two unmerged commits.`, stays unflagged. A setup that asks before every test run can list `closing-offer` in `excludePacks`.", + "patterns": [ + { "regex": "(?:^|[.!?]\\s+)(?(?:Do you |Would you )?(?:want|like) me to\\b[^?\\n]{0,160}\\?)\\s*$", "flags": "gi", "fix": "cut the offer, or name the step that needs approval", "show": "`Want me to ...?` or `Do you want me to ...?` at the end of the text" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/git/ai-attribution.json b/plugins/concise/hooks/lib/patterns/git/ai-attribution.json index 4eee664..3716923 100644 --- a/plugins/concise/hooks/lib/patterns/git/ai-attribution.json +++ b/plugins/concise/hooks/lib/patterns/git/ai-attribution.json @@ -2,7 +2,7 @@ "id": "ai-attribution", "feature": "aiWriting", "category": { "id": "ai-attribution", "label": "AI attribution trailer", "description": "A commit or PR trailer credits an AI tool as co-author, generator, or signer.", "tags": ["git"] }, - "source": "Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai", + "source": "Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai, code.claude.com settings reference (attribution.sessionUrl), Cursor forum #154494, GitHub changelog 2026-03-20", "presets": ["default", "ryan", "technical", "minimal", "git", "all"], "scope": ["commit", "gh", "command"], "notes": "The two `Co-authored-by` patterns sit at tier 2. A trailer that pairs an AI tool name with an AI email reports; a co-author named Claude with a personal address stays quiet.", @@ -15,6 +15,9 @@ { "regex": "^Generated-By:.*$", "flags": "gim", "fix": "drop the trailer", "show": "a `Generated-By:` trailer" }, { "phrase": "This commit was generated by", "fix": "drop the disclosure sentence" }, { "regex": "^Assisted-by:.*$", "flags": "gim", "fix": "drop the trailer", "show": "an `Assisted-by:` trailer" }, + { "regex": "^Claude-Session:.*$", "flags": "gim", "fix": "drop the session trailer", "show": "a `Claude-Session:` trailer" }, + { "regex": "^Made-with:\\s*Cursor\\b.*$", "flags": "gim", "fix": "drop the trailer", "show": "a `Made-with: Cursor` trailer" }, + { "regex": "^Agent-Logs-Url:.*$", "flags": "gim", "fix": "drop the trailer", "show": "an `Agent-Logs-Url:` trailer" }, { "regex": "^Signed-off-by:.*\\[bot\\].*$", "flags": "gim", "fix": "drop the bot sign-off", "show": "`Signed-off-by:` plus a `[bot]` account" }, { "regex": "^Signed-off-by:.*(?:noreply@anthropic\\.com|noreply@openai\\.com|all-hands\\.dev|cursor\\.com)>?\\s*$", "flags": "gim", "fix": "drop the bot sign-off", "show": "`Signed-off-by:` plus an AI tool email" } ] diff --git a/plugins/concise/hooks/lib/patterns/git/ai-identity.json b/plugins/concise/hooks/lib/patterns/git/ai-identity.json index d0d003f..4f9d0cf 100644 --- a/plugins/concise/hooks/lib/patterns/git/ai-identity.json +++ b/plugins/concise/hooks/lib/patterns/git/ai-identity.json @@ -5,7 +5,7 @@ "source": "Group G research: claude-code#72477, VS Code #314311, Cursor forum #168258, gemini-cli #11447", "presets": ["default", "ryan", "technical", "minimal", "git", "all"], "scope": ["command"], - "notes": "Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that commits as a bot on purpose lists `ai-identity` in `excludePacks`.", + "notes": "Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that intentionally commits as a bot lists `ai-identity` in `excludePacks`.", "patterns": [ { "regex": "--author=(['\"])[^'\"]*(?:Claude Code|OpenAI Codex|GitHub Copilot|Cursor Agent|OpenHands)[^'\"]*\\1", "fix": "use a human author name", "show": "`--author=` naming an AI tool" }, { "regex": "--author=(['\"])[^'\"]*(?:noreply@anthropic\\.com|noreply@openai\\.com|codex@openai\\.com|copilot@github\\.com|cursoragent@cursor\\.com|openhands@all-hands\\.dev)[^'\"]*\\1", "fix": "use a human email address", "show": "`--author=` holding an AI tool email" }, diff --git a/plugins/concise/hooks/lib/patterns/git/benefit-tail.json b/plugins/concise/hooks/lib/patterns/git/benefit-tail.json index 0c6267d..3a61121 100644 --- a/plugins/concise/hooks/lib/patterns/git/benefit-tail.json +++ b/plugins/concise/hooks/lib/patterns/git/benefit-tail.json @@ -5,7 +5,7 @@ "source": "Group G research: AI-generated PR summary and commit body conventions", "presets": ["ryan", "technical", "git", "all"], "scope": ["commit", "gh", "comments", "reply"], - "notes": "The noun list stays abstract on purpose. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match.", + "notes": "The noun list is intentionally abstract. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match.", "patterns": [ { "regex": ",\\s*(?:which|thereby|thus|to|for|ensuring|so that it's) [^.!?\\n]{0,40}?\\b(?:improves?|increases?|reduces?|ensures?|enhances?|makes?)\\b[^.!?\\n]{0,30}?\\b(?:readability|maintainability|reliability|performance|consistency|robustness|clarity|scalability|efficiency|usability|stability|testability|extensibility|security|quality|correctness)\\b", "fix": "cut the justification clause", "show": "`, which improves readability` and the like" }, { "regex": ",\\s*making it (?:easier|simpler|harder|more \\w+) to \\w+", "fix": "cut the justification clause", "show": "`, making it easier to maintain`" }, diff --git a/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json b/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json new file mode 100644 index 0000000..10822db --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json @@ -0,0 +1,20 @@ +{ + "id": "hidden-characters", + "feature": "aiWriting", + "category": { "id": "hidden-characters", "label": "hidden characters", "description": "Invisible or look-alike characters that model output and chat UIs leave in text.", "tags": ["punctuation", "watermark"] }, + "source": "Opus 5.5 replies (U+2011 at 0.33 per 10,000 words), Microsoft known issue on LLM hyphens (Apr 2026), OpenAI U+202F reports (2025), Trojan Source bidi controls, Wikipedia:Signs of AI writing", + "presets": ["default", "ryan", "technical", "git", "all"], + "scope": ["files", "comments", "gh", "commit", "command", "reply"], + "notes": "Anthropic's text watermark adds no characters, so this pack cannot see it. U+00A0 is left out: npm READMEs carry it 5.6 times per 10,000 words. A zero-width joiner inside an emoji sequence and tag characters after the black flag (subdivision flags) stay unflagged. A right-to-left project lists `hidden-characters` in `excludePacks` or drops the bidi row with its own pack.", + "patterns": [ + { "regex": "[\\u200B\\u200C\\u2060\\u180E]", "flags": "gu", "fix": "remove the zero-width character", "show": "a zero-width space, zero-width non-joiner, word joiner, or Mongolian vowel separator" }, + { "regex": "(?