{"id":"4c2d9084-b751-463a-a07b-5407cc9ed178","slug":"semantic-linefeeds","kind":"skill","name":"semantic-linefeeds","summary":"One sentence per line, enforced as your AI agent writes it — a one-word edit stays a one-line diff. A post-edit hook checks code comments, doc comments, docstrings, and Markdown for semantic line brea","owner":"arloliu","repoUrl":"https://github.com/arloliu/semantic-linefeeds","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":3,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-08-08T13:54:17.000Z","addedAt":"2026-10-21T23:37:10.336Z","logoUrl":"https://avatars.githubusercontent.com/u/1687357?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","files-documents"],"tags":["ai-agents","claude-code","code-comments","git-diff","linter","markdown","opencode","semantic-line-breaks"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T19:39:08.014Z","sample":false,"url":"https://conduits.dev/tools/semantic-linefeeds","language":"Python","transports":["stdio"],"install":{"pypi":"semlf"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# semantic-linefeeds\n\n[![PyPI](https://img.shields.io/pypi/v/semlf?color=0a7bbb&label=semlf)](https://pypi.org/project/semlf/)\n[![CI](https://github.com/arloliu/semantic-linefeeds/actions/workflows/ci.yml/badge.svg)](https://github.com/arloliu/semantic-linefeeds/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/pypi/pyversions/semlf)](https://pypi.org/project/semlf/)\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/arloliu/semantic-linefeeds/blob/main/LICENSE)\n\n**You told your agent \"one sentence per line\" — it didn't listen.**<br>\n*Line breaks are for meaning, not margins.*\n\nInstructions don't survive generation time, however prominently you state them.\nA deterministic check at the tool boundary does:\nthe moment your agent writes a comment, a doc comment, a docstring, or Markdown,\na hook reads what it wrote and stops the edit when two sentences land on one line.\n\n![An agent writes two sentences on one line; the hook reports a fused finding, blocks the edit, and suggests the split; the agent rewrites it as two lines](https://raw.githubusercontent.com/arloliu/semantic-linefeeds/main/docs/assets/hook-blocks-an-edit.gif)\n\n**It never rewrites the files you already have.**\nLegacy column-wrapped prose is not flagged into a noisy rewrap:\nthe hook judges the text your agent just wrote and nothing else,\nand only two sentences sharing one line ever block an edit.\nA long line comes back as advice, because leaving it long is often the right answer.\n\nBecause each line holds one sentence,\nan edit touches only the line whose meaning changed,\nand `git blame` keeps pointing at the sentence that changed.\nThe same dependency-free Python detector wires into Claude Code, Codex CLI, opencode,\nand — through a portable AGENTS.md snippet — any agent that reads instruction files.\n\n## Quickstart\n\n```bash\nuv tool install semlf        # or: pipx install semlf\nsemlf install                # detects agents, lists every path it would write, asks y/N\nsemlf doctor                 # replays the payloads end to end\n```\n\nClaude Code stays on its own plugin marketplace and is never touched by `semlf`:\n\n```bash\nclaude plugin marketplace add arloliu/semantic-linefeeds\nclaude plugin install semantic-linefeeds@semantic-linefeeds\n```\n\nA local checkout works the same way — point the first command at its path instead:\n\n```bash\nclaude plugin marketplace add /path/to/semantic-linefeeds\nclaude plugin install semantic-linefeeds@semantic-linefeeds\n```\n\nThe plugin carries the hook,\nso a user-scope install is live in every project on the machine from the next session on\nand a new repository needs no setup step of its own.\n`.semlf.ini` tunes the checker where a project wants a different limit or an exclude;\nit is never what turns the hook on, and most projects need none.\n\n`semlf install`, `semlf status`, and `semlf doctor` cover Codex CLI and opencode only.\nA green `doctor` therefore says nothing about whether Claude Code's plugin is installed and enabled —\n`claude plugin list` is where that answer lives.\n\n| Agent | Command |\n|---|---|\n| Codex CLI | `semlf install codex` |\n| opencode | `semlf install opencode` |\n| Claude Code | the marketplace pair above |\n| Anything else (AGENTS.md) | `semlf install agentsmd PATH` |\n\n`semlf install` with no target detects which agents are present\nand proposes one plan covering all of them, then asks y/N.\nNaming a target instead — `codex`, `opencode`, or `agentsmd PATH` — applies that one target immediately.\n\nPick one channel for the `semlf` command itself.\nA zipapp left at `~/.local/bin/semlf` and a `uv tool install`/`pipx install` shim would otherwise shadow each other;\n`semlf install`'s end-of-run `PATH` check, and `semlf doctor`, both name the collision when they see one.\n\n## Why one sentence per line\n\n[SemBr](https://sembr.org) breaks lines by meaning instead of by column:\none sentence per line,\nand a long sentence splits only at a real clause boundary.\nThe rendered output doesn't change —\nMarkdown still joins the lines back into one paragraph.\n\nThe difference shows up in diffs.\nAdd two words near the top of a column-wrapped paragraph (wrapped at 72 characters here),\nand the rewrap ripples through every line below it:\n\n```diff\n-The exporter batches metrics in memory, flushes them once per minute,\n-and retries failed uploads with exponential backoff until the queue\n-drains.\n+The exporter batches metrics and traces in memory, flushes them once per\n+minute, and retries failed uploads with exponential backoff until the\n+queue drains.\n```\n\nThe same edit under semantic line breaks touches only the one line whose meaning changed:\n\n```diff\n-The exporter batches metrics in memory,\n+The exporter batches metrics and traces in memory,\n flushes them once per minute,\n and retries failed uploads with exponential backoff until the queue drains.\n```\n\nReviewers see exactly the word that changed,\nand blame on any sentence finds the commit that wrote it.\n\n## Letting the Agent Set It Up\n\nOnce any agent on the machine has been set up, the rest is something you can just ask for.\nA `setup-semlf` skill installs alongside the judgment layer,\nso an agent asked to install, repair, or reconfigure `semlf` follows a fixed procedure rather than guessing at a package name or hand-editing `hooks.json`.\n\nIn opencode, type the command:\n\n```\n/setup-semlf\n```\n\nIn Codex CLI or Claude Code, ask in prose — \"set up semlf in this project\" is enough to load it.\n\nIt installs the CLI when it is missing, walking `uv`, `pipx`, `pip --user`, and the checkout door in that order.\nIt repairs an install left stale by an upgrade, which is the failure that actually recurs.\nAnd it offers to write the project's `.semlf.ini`, though most projects need none.\n\nThree things it will not do, whatever it is asked:\n\n- **Overwrite one of your files.**\n  A refusal from `semlf install` is shown to you with the difference, and `--force` stays yours to type.\n- **Add an `exclude` line on its own authority.**\n  Excludes suppress discovery, so the agent may transcribe one you dictate and never propose one itself.\n- **Run anything before showing you the command.**\n\nThe first install on a fresh machine is still yours to run —\nan agent cannot fetch a skill it does not yet have.\nUse the Quickstart above, or the [checkout door](#air-gapped-and-mirror-installs) where there is no package index.\n\n## What Gets Installed\n\n| Component | Location | Purpose | Who needs it |\n|---|---|---|---|\n| Checker | `${XDG_DATA_HOME:-~/.local/share}/semlf/check_linefeeds.py` | The enforcement core every installed hook and skill runs | Codex CLI, opencode |\n| README | published beside the checker, at the same neutral root | Resolves the installed skill's suppression-rules link, even on an air-gapped machine | Codex CLI, opencode |\n| Codex hook entry | `$CODEX_HOME/hooks.json` (default `~/.codex/hooks.json`) | Runs the checker after every edit; blocks `fused`, reports `long`, and reports `wrap` on a blocked line or when opted in | Codex CLI |\n| Judgment skill | `~/.agents/skills/semantic-linefeeds/SKILL.md` | The judgment layer: clause-boundary calls, suppression syntax, the disagreement rule | Codex CLI, opencode |\n| Setup skill | `~/.agents/skills/setup-semlf/SKILL.md` | Lets an agent install, repair, or configure `semlf` without improvising the commands | Codex CLI, opencode |\n| opencode plugin | the opencode plugins directory (`$XDG_CONFIG_HOME/opencode/plugins`, default `~/.config/opencode/plugins`) | Wires the checker into opencode's edit/write/apply_patch tool output | opencode |\n| opencode's checker copy | the same opencode plugins directory, beside the plugin | The plugin resolves its checker next to itself, not at the neutral root | opencode |\n| opencode setup command | `$XDG_CONFIG_HOME/opencode/commands/setup-semlf.md` (default `~/.config/...`) | Makes `/setup-semlf` typeable: opencode offers skills to the model, commands to you | opencode |\n| AGENTS.md snippet | the file you name with `semlf install agentsmd PATH` | The judgment layer for any agent with no native skill mechanism | Any AGENTS.md-reading agent |\n\nThe skill is published once, to `~/.agents/skills`, which Codex CLI and opencode both read.\nopencode 1.18.18 or newer is required, and `OPENCODE_DISABLE_EXTERNAL_SKILLS` disables that scan —\nwith it set, opencode sees no skill regardless of where it is installed.\n\nOn the package channel, the `semlf` package is the installer and cannot be skipped —\nbut its check commands (`semlf check`, `semlf --staged`, and so on) stay optional after that.\nA guardrail-only machine that wants no CLI at all is the checkout door's offer instead:\n`install.sh --codex`, with no `--cli`, writes the hook, the skill, and the neutral-root payloads, and stops there\n(see [Air-gapped and mirror installs](#air-gapped-and-mirror-installs)).\n\n## Why Telling an Agent Isn't Enough\n\nWriting the rule into AGENTS.md, CLAUDE.md, or any other instruction file doesn't hold up at generation time:\nagents keep producing non-semantic line breaks with the rule in plain sight.\n\nSo this kit backs the rule with three layers instead of trusting the model to remember it:\n\n1. **Hook** —\n   a post-edit hook runs a deterministic detector over the text just written to any supported file.\n   Violations come back as feedback the moment they happen,\n   which is the one channel that survives a long session.\n   That boundary is also the limit:\n   the hook sees the agent's own edit tools,\n   so text a session writes some other way — a shell heredoc, `sed`, a generator script, `git apply` —\n   never reaches it, and `semlf check` after the fact is what covers that text.\n   Only `fused` stops the edit;\n   a long line comes back as advice, because leaving it long is often the right answer.\n   `wrap` is not reported to the model at all,\n   because a labeled corpus measured its false positives and the number was not small enough to act on.\n   Set `SEMLF_EXPERIMENTAL_WRAP=1` to see it anyway, as advice that never blocks,\n   or set `experimental-wrap = true` in `.semlf.ini` to opt a whole project in.\n   So a silent edit means nothing blocking was found, not that the text is clean.\n2. **Skill** —\n   `semantic-linefeeds` carries the judgment calls the detector can't make on its own:\n   what counts as a clause boundary, the compound-object `and` test, the never-break list,\n   and the bounded disagreement rule —\n   judge a finding before rewriting, and a believed false positive\n   or a finding that survives one repair goes to the user instead of another rewrite.\n   Claude Code ships it as a plugin skill;\n   `semlf install codex` and `semlf install opencode` publish the same native copy to a root both agents read;\n   an agent with no native skill mechanism falls back to the AGENTS.md snippet.\n   Hook feedback names the skill only when a usable copy is present at a location\n   Codex resolves skills from.\n3. **Detector** —\n   `scripts/check_linefeeds.py`, dependency-free Python 3.\n   Three precision-tuned heuristics:\n   `fused` (two sentences on one line), `wrap` (a line severed mid-clause),\n   and `long` (over a configurable limit, default 120 chars, reported whether or not a clause boundary is found).\n   `--file` audits report all three, because an audit is read by the person who asked for it.\n   The hook reads a stable snapshot of the edited file for context,\n   so it can report a real line number and skip findings an edit didn't touch,\n   and it falls back to checking only the text just written when that mapping fails.\n   Either way, legacy column-wrapped files are never flagged into a noisy rewrap.\n\n## When the Hook Seems Not to Fire\n\nFour causes account for nearly every report of a silent guardrail,\nand the first two are not failures:\n\n1. **The text never went through an edit tool.**\n   The hook matches the agent's file-writing tools;\n   a file written by a shell heredoc, `sed`, a generator script, or `git apply` bypasses it.\n   Audit that text by hand instead: `semlf check <paths>`.\n2. **Nothing blocking was found.**\n   Only `fused` blocks an edit.\n   `long` arrives as advice, and `wrap` is withheld from the model unless you opt in,\n   so a pass is a pass on `fused` alone.\n3. **The agent you are using has no integration installed.**\n   Each agent has its own channel, and installing the `semlf` command installs none of them:\n   `claude plugin list` answers for Claude Code, `semlf status` for Codex CLI and opencode.\n4. **The installed copy is behind the checker you run by hand.**\n   Claude Code's plugin is versioned separately from the `semlf` command\n   and upgrades on its own pair of commands (see [Lifecycle](#lifecycle)).\n\nTo settle it either way, ask the agent to write a file whose first line holds two sentences.\nA live hook blocks that edit and names `fused`.\nWrite it to a real source or docs path.\nThe checker skips the platform temp directory,\nany path with a `tmp/`, `vendor/`, `node_modules/`, `dist/`, `build/`, `testdata/`, or `fixtures/` component,\nand anything an `exclude` pattern matches;\na skipped file passes in silence.\n\n## Suppressing a finding\n\nTwo directives, each scoped to exactly one line:\n\n- `semlf-ignore` — withholds every finding anchored on the line carrying it.\n- `semlf-ignore-next` — withholds every finding anchored on the next line.\n\nThe directive rides in a comment, so it is written the way the file's own language writes comments.\n\n| Language | Standalone | Trailing |\n|---|---|---|\n| Markdown | `<!-- semlf-ignore-next -->` | `… judged text. <!-- semlf-ignore long -->` |\n| Go, Rust, C-family, JavaScript | `// semlf-ignore-next` | `// … judged text.  // semlf-ignore long` |\n| Python, Ruby, shell | `# semlf-ignore-next` | `# … judged text.  # semlf-ignore long` |\n| Lua, SQL | `-- semlf-ignore-next` | `-- … judged text.  -- semlf-ignore long` |\n\nStandalone, on a line of its own:\n\n```markdown\n<!-- semlf-ignore-next -->\nA line the checker will leave alone.\n```\n\n```go\n// semlf-ignore-next\n// A line the checker will leave alone.\n```\n\nTrailing, after the line it judges:\n\n```markdown\nA long judged line that runs on past the limit. <!-- semlf-ignore long -->\n```\n\n```python\n# A long judged line that runs on past the limit.  # semlf-ignore long\n```\n\nA trailing carrier is the rightmost comment marker to the end of the line,\nso the judged text is whatever stands to its left.\n\nA two-line `wrap` finding anchors on its upper line,\nso a trailing carrier suppressing it must sit on that first line, not the second.\n\nEach directive takes zero or more kind arguments (`fused`, `wrap`, `long`);\nno arguments suppresses every kind on that line.\nA recognized directive name with any unrecognized argument is malformed and inert —\nit suppresses nothing, and the findings it would have hidden stay visible.\n\nSuppression is user-directed.\nAn agent never adds a `semlf-ignore` or `semlf-ignore-next` directive on its own authority:\nwhen it judges a finding to be a false positive,\nit leaves the text as written and raises the disagreement with you instead.\n\n## Suggested replacements\n\nA blocking `fused` report on `!`, `?`, or `.` carries an exact two-line replacement\nwhen the split is unambiguous (ADR-0007; periods since ADR-0028):\n\n```\nSuggested replacement for line 12:\n    Stop now!\n    Go on.\n```\n\nPeriod boundaries earned the suggestion in v0.9:\na sealed holdout round scored the class at 34 of 35 acceptable against a preregistered 0.80 floor,\nand only then was it admitted (ADR-0028).\nIt is never applied for you —\napply it as your next edit, after judging the finding like any other.\n\n## Configuration\n\n`.semlf.ini` at the repository root holds every project setting the checker reads,\none `[semlf]` section, three optional keys:\n\n```ini\n[semlf]\nlong-limit = 100\nexperimental-wrap = true\nexclude =\n    docs/legacy/\n    *.gen.md\n```\n\n- `long-limit` — the long-line advisory threshold, in characters (`0` disables the advisory).\n  Precedence is the `--long-limit` flag, then `$SEMLF_LONG_LINE`, then this key,\n  then the built-in default of 120.\n  Fused and wrap findings are never affected — only the advisory threshold moves.\n- `experimental-wrap` — opts `wrap` findings back into hook feedback,\n  arriving as advice that never blocks an edit (ADR-0017).\n  `$SEMLF_EXPERIMENTAL_WRAP` decides outright whenever it is set to a non-empty value —\n  `0`, `false`, `no`, or `off` reads as off, anything else as on —\n  so it can force the kind on for a repo that never opted in, or off for one that did.\n- `exclude` — one glob pattern per line, matched per path segment;\n  a trailing `/` names a folder, excluded at any depth unless an inner `/` anchors a chain at the config root\n  (see \"Excluding paths\" below).\n\nThe flag and the environment variable always win over the file (ADR-0012).\nThe core discovers `.semlf.ini` by walking upward from the checked file\nand stops at the first directory holding either the file or a `.git` entry,\nso a config never leaks across a repository boundary.\nA missing, malformed, or unparseable file is inert —\nthe checker falls back to the next precedence leg rather than failing the run,\nand an invalid value in one key never silences another.\n\nHook mode skips paths under the platform temp directory and any `tmp/` component,\nso agent scratch files are never flagged.\n`--file` mode always checks exactly the paths you name.\n\n### Excluding paths\n\n`exclude` takes one pattern per line;\nthe three shapes below are worth seeing side by side:\n\n```ini\n[semlf]\nexclude =\n    vendor/\n    docs/generated/\n    *.generated.md\n```\n\nA trailing `/` names a folder — a bare name like `vendor/` excludes it at any depth,\nwhile an inner `/` before the trailing one anchors a chain at the config root —\ntoday the repository root, where `.semlf.ini` lives (ADR-0012) —\nso `docs/generated/` excludes only that path, not `plugins/docs/generated/`.\nA pattern without a trailing `/` is a glob:\nwith a `/` it must match the whole relative path, segment by segment;\nwithout one, like `*.generated.md`, it matches any single path component at any depth.\nMatching is case-sensitive everywhere.\n\nExcludes filter **discovery only** — hook mode and the three git modes below.\nA path you name explicitly on `--file` or `check` is always checked,\nexclude or no exclude, because naming a path is the judgment call excludes exist to encode.\nAn agent never adds an `exclude` line on its own authority (ADR-0010's principle);\nit raises the disagreement with you instead.\n\n## Checking git snapshots\n\nThree more modes check a git snapshot instead of files you name:\n\n- `semlf --staged` checks the index — what `git commit` would record —\n  reading each staged blob by its own object id.\n- `semlf --diff` checks the worktree copy of every unstaged change against the index.\n- `semlf --changed` checks the worktree copy of everything different from `HEAD`,\n  staged and unstaged together.\n\nAll three accept `--json` and `--long-limit N`, the same as `--file`.\n\nA fourth verb answers a different question about the same snapshots.\n`semlf reflow [REF]` verifies that the worktree differs from `REF` (default `HEAD`)\nonly in where its prose breaks:\nno word changed, no code or markup changed, no paragraph appeared or disappeared.\nA reflow-only diff looks like a content change in review —\nlines vanish and reappear —\nand this is the one-line proof that it is not,\nso a commit that only moves breaks can say so and be believed.\nAny other change — a word, a code line, a deleted file,\na file outside prose scope — fails it with the first difference named.\n\nOnly tracked changes are enumerated;\nan untracked file needs `git add` before any mode can see it.\nSymlinks are never checked, in any mode.\n\nTwo more selection modes exist for CI, where a clean checkout has no local delta:\n`semlf --base REF` checks the files changed since `merge-base(REF, HEAD)`\nand reports only the diagnostics the changed lines own,\nbecause a CI run annotates someone's pull request\nand a finding that predates the branch is not that author's to answer;\n`semlf --all` checks every tracked file under the configured excludes.\n`semlf render sarif|github` turns a captured `--json` documents list into a SARIF report or GitHub workflow annotations,\nre-analyzing nothing.\n\n## Exit codes\n\nEvery mode answers with the same small vocabulary,\nand from 1.0 the meaning of each code is a compatibility promise:\n\n| Mode | 0 | 1 | 2 | 64 |\n|---|---|---|---|---|\n| `check` / `--file`, `--staged`, `--diff`, `--changed`, `--base`, `--all` | clean, or advisory `long` only | a `fused`/`wrap` violation, or an unreadable file or git source | — | bad usage |\n| `--hook claude`, `--hook codex` | advisory-only output, a payload that is not the hook's to judge, or a malformed payload | — | a blocking finding, reported on stderr | bad usage |\n| `render` | rendered, however many findings the documents hold | unreadable or invalid input | — | bad usage |\n| `reflow` | pure reflow, or nothing differs | not a pure reflow, or a git source error | — | bad usage |\n| `doctor` | every probe passed | a probe failed | — | arguments given |\n| `install`, `status`, `uninstall` | applied or reported | refused or failed | — | bad usage |\n| `--version`, `--help` | printed | — | — | — |\n\nTwo of these rows are choices worth naming.\nA hook exits `0` on a malformed payload — fail-open is deliberate,\nbecause a hook must not block editing on its own malfunction.\nAnd `render` exits `0` regardless of findings:\nwhat a finding means for a build is the gate's decision, not the renderer's.\n\n## Compatibility\n\nFrom 1.0, `semlf` follows semantic versioning over its public surfaces:\nthe CLI modes and flags, the `.semlf.ini` schema, the `--json` document schema,\nthe suppression directives, the exit codes above,\nthe GitHub Action's inputs and outputs, and the names you install by —\nthe `semlf` package and command, the pre-commit hook id, the Claude plugin name —\nall on Python 3.9 or newer.\nA change that would break any of them costs a major version;\nnew capability arrives in minor versions.\nTwo things are explicitly not promised:\nwhich lines draw findings (precision keeps improving between majors),\nand the wording of messages — the text is for people, the structure is for tools.\n\n## The GitHub Action\n\nThe Action installs the checker it shipped with —\npinning the Action pins everything —\nchecks the pull request against its base,\nannotates the changed lines,\nand fails only on the kinds you ask for.\n\n| input | default | meaning |\n|---|---|---|\n| `fail-on` | `fused` | kinds that fail the build: a comma-separated list of `fused` and `wrap` in either order, at least one; whitespace is trimmed, empty segments and duplicates are ignored; `long` is refused, it never fails a build |\n| `mode` | `base` | `base` checks changed files against the PR base; `all` checks the whole tree |\n| `base` | the PR's base | the ref for `mode: base`; required on non-PR events |\n| `sarif-file` | none | write a SARIF report here and expose it as the `sarif-file` output; never uploaded |\n\nThe default, annotations only:\n\n```yaml\non: pull_request\njobs:\n  prose:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0   # mode \"base\" needs history reaching the merge base\n      - uses: arloliu/semantic-linefeeds@main\n```\n\nWith a SARIF report, whose upload — and its permission — belongs to your workflow:\n\n```yaml\non: pull_request\npermissions:\n  security-events: write\njobs:\n  prose:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: arloliu/semantic-linefeeds@main\n        id: semlf\n        with:\n          sarif-file: semlf.sarif\n      - uses: github/codeql-action/upload-sarif@v3\n        if: always() && steps.semlf.outputs.sarif-file != ''\n        with:\n          sarif_file: ${{ steps.semlf.outputs.sarif-file }}\n```\n\nPolicy — `.semlf.ini`, including `exclude` — is always read from the working tree,\neven for `--staged`: its content is the index, but its policy is the checkout it runs in.\nA config that is staged but not yet on disk does not yet govern `--staged` (ADR-0013).\n\n### Pre-commit\n\n```yaml\nrepos:\n  - repo: https://github.com/arloliu/semantic-linefeeds\n    rev: <tag>\n    hooks:\n      - id: semlf\n```\n\n`language: python` lets pre-commit build this repository into its own environment,\nso `semlf` does not need to be installed or on `PATH` beforehand.\n\n## Supported Languages\n\n| Languages | Extensions |\n|---|---|\n| C, C++, Objective-C | `.c` `.h` `.cc` `.cpp` `.hpp` `.hh` `.m` `.mm` |\n| Java, Kotlin, Scala, Groovy | `.java` `.kt` `.kts` `.scala` `.groovy` `.gradle` |\n| JS/TS | `.js` `.jsx` `.ts` `.tsx` `.mjs` `.cjs` |\n| C#, VB.NET | `.cs` `.vb` |\n| Go, Rust, Zig | `.go` `.rs` `.zig` |\n| Python | `.py` `.pyi` |\n| Swift, Dart, PHP | `.swift` `.dart` `.php` |\n| shell, PowerShell | `.sh` `.bash` `.ps1` `.psm1` `.psd1` |\n| Ruby, Perl, Lua | `.rb` `.rake` `.pl` `.pm` `.lua` |\n| SQL, R, Haskell, Elixir | `.sql` `.r` `.R` `.hs` `.ex` `.exs` |\n| Markdown | `.md` `.markdown` `.mdx` |\n\n### What is not analyzed\n\nThe checker reads English prose.\nIts two sentence-reading kinds each depend on an ASCII signal —\na lowercase word before the stop, and a lowercase opener on the following line —\nso CJK text is passed over rather than checked.\nA Chinese, Japanese, or Korean paragraph therefore draws no `fused` and no `wrap`,\nhowever it is broken.\nThe `long` advisory is the exception:\nit counts characters rather than reading the sentence,\nso a CJK line past the limit draws one like any other line.\nIts message says the checker found no boundary it recognizes,\nwhich is the honest answer for CJK text and leaves the split to your judgment.\nA full-width terminator is recognized as ending a line,\nso a Chinese sentence above an English one is not accused at the boundary.\nA CJK line that ends without punctuation can still draw a `wrap`\nwhen the line under it opens on a lowercase English word,\nbecause that judgment is read off the English line.\nIn none of these cases is the CJK text itself read as prose:\nwhat fires is a rule about the English line beside it, or about width.\n\n## Recommended Instructions Companion\n\nThe one habit that survives inline generation belongs in your agent's global instruction file —\nAGENTS.md for most agents, CLAUDE.md for Claude Code.\nEverything else lives in the skill and the hook:\n\n```markdown\n## Comment and doc formatting\n\nOne sentence per line in all comments and Markdown (semantic linefeeds).\nAfter writing any text block, act on whatever the linefeeds hook reports.\nA blocked edit must be fixed; an advisory is a judgment call, and leaving the line alone can be right.\n```\n\n## Appendix\n\n### Air-gapped and mirror installs\n\nThe package channel's `git+URL` form still works when a machine can't reach PyPI:\n\n```bash\npipx install git+https://github.com/arloliu/semantic-linefeeds\n# or\nuv tool install git+https://github.com/arloliu/semantic-linefeeds\n```\n\nFor a fully offline or mirrored network, the checkout door covers every artifact this kit installs.\nOne command clones (or updates) a checkout under `${XDG_DATA_HOME:-~/.local/share}/semantic-linefeeds`\nand hands the remaining arguments to `scripts/install.py`,\nthe same shared lifecycle engine behind `semlf install`:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/arloliu/semantic-linefeeds/main/install.sh | sh -s -- --codex --cli\n```\n\nPass the flag for your agent to the one-liner above, or to `python3 scripts/install.py` inside a checkout.\nPassing no flag prints a status report of what's installed where.\n\n| Agent | Installer flag |\n|---|---|\n| Codex CLI | `--codex` |\n| opencode | `--opencode` |\n| Anything else | `--agentsmd PATH` |\n\nAdd `--cli` to build the `semlf` zipapp and install it as `~/.local/bin/semlf` —\nthe one channel that needs no package index at all,\nand the one the package door deliberately has no equivalent for:\npipx, uv, and the zipapp all want the same `~/.local/bin/semlf` shim,\nso building and removing the zipapp stays exclusive to the checkout door.\n\n`install.sh` reads its clone source from `--repo` or the `SEMLF_REPO` env var,\nand the checker itself never touches the network.\nMirror this repo to your internal git host,\ncurl the script from the mirror's raw endpoint,\nand point `SEMLF_REPO` back at the mirror:\n\n```bash\n# GitLab raw path shown; Gitea/Forgejo use /raw/branch/main/ instead of /-/raw/main/\ncurl -fsSL https://git.internal/you/semantic-linefeeds/-/raw/main/install.sh |\n  SEMLF_REPO=git@git.internal:you/semantic-linefeeds.git sh -s -- --codex\n```\n\nExport `SEMLF_REPO` once in your shell profile,\nand every later re-run installs and updates from the mirror without repeating it.\n`--ref`/`SEMLF_REF` pins a tag or branch;\n`--home`/`SEMLF_HOME` moves the checkout.\n\nFor Claude Code,\n`claude plugin marketplace add git@git.internal:you/semantic-linefeeds.git` covers the same case.\n\nThe [Vercel `skills` CLI](https://www.npmjs.com/package/skills) can fetch the judgment-layer skill on its own,\nas a supplement rather than an install path.\nIt copies `SKILL.md` and nothing else,\nso it cannot install the hook —\nthis kit's load-bearing layer, the one that surfaces findings at generation time —\nand an agent that only has the skill this way still needs `semlf install` or the checkout door for the hook.\n\n### Lifecycle\n\n```bash\nsemlf install [TARGET...]     # detect agents and propose a plan, or apply one named target\nsemlf status [agentsmd PATH]  # report every discoverable or recorded artifact's state\nsemlf uninstall TARGET...     # preflight-then-apply removal of a target's artifacts\nsemlf doctor                  # replay a payload end to end, report evidence\n```\n\nUpgrading is a two-command pair:\n\n```bash\nuv tool upgrade semlf && semlf install\n# or: pipx upgrade semlf && semlf install\n```\n\nThe first command updates the `semlf` command itself, its embedded payloads included;\nthe second re-applies the current payload set over whatever is already installed.\nUpgrades are provenance-aware:\nre-running an installer replaces an untouched older release silently.\nA release newer than the one already published is a downgrade and refuses by default;\n`--force` states the intent and replaces it, no backup, since a managed file is never the only copy of anything.\nAn edited or unrecorded file always refuses first;\n`--force` there takes an exclusive backup to `<name>.bak` before replacing it,\nand an occupied backup slot refuses either way.\n\nClaude Code's plugin sits outside that pair, since `semlf` never writes to it:\n\n```bash\nclaude plugin marketplace update semantic-linefeeds\nclaude plugin update semantic-linefeeds@semantic-linefeeds   # restart Claude Code to apply\n```\n\nA marketplace added from a local directory tracks that directory, so its installed version can move on its own;\nrun the pair above when `claude plugin list` shows a version behind the source.\n\nUninstalling one integration never deletes the neutral root's published checker or README:\ntheir independence from any single integration is the point of the neutral root,\nso they are left in place once the last integration that used them is gone,\nand `semlf status` lists the specific leftover file paths in one line for manual removal.\n\nA zipapp left over from before this redesign is a migration case:\n`semlf install` runs a `PATH` check at the end of every run\nand warns when `semlf` on `PATH` is not the artifact that just ran;\n`semlf status` and `semlf doctor` repeat the warning,\nwith the checkout-door removal pointer (`install.py --uninstall --cli`).\n\n### Testing\n\n```bash\npython3 -m pytest tests/ -q                        # full suite: CLI, detector, extractor, install/doctor\npython3 scripts/check_linefeeds.py --file <files>   # audit files by hand\nbun test adapters/opencode/                         # opencode plugin's own unit tests (needs bun)\n```\n\nDetector fixtures live under `tests/fixtures/<language>/`;\ninline `{fused}`, `{wrap}`, or `{long}` markers mark the lines that should be flagged.\n`good_*` fixtures must carry zero markers, and a dedicated test enforces it.\n\nThe extractor has golden tests under `tests/extractor/`.\nAfter changing extraction logic,\nregenerate with `python3 -m pytest tests/test_extractor.py --update-golden` and diff-review the result.\n"}