@vasilvestre/opencode-plain-style 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ // plain-style — OpenCode plugin, supports opencode v1.x and v2.x.
2
+ //
3
+ // Injects the plain-language rulebook (plain-style.md, beside this file) into
4
+ // every chat's system prompt on every agent-loop LLM request, primary agent
5
+ // and subagents alike.
6
+ //
7
+ // The default export is one object with both entrypoints (v2 migration guide:
8
+ // https://opencode.ai/v2/docs/build/plugins/migrate-v1):
9
+ // - v2 calls setup() and registers a "context" session hook; system parts are
10
+ // objects ({ type: "text", text }) there.
11
+ // - v1 (object entrypoints need opencode >= 1.18.29) calls server() and gets
12
+ // the experimental.chat.system.transform hook; system entries there are
13
+ // plain strings. The hook must mutate output.system in place (push), because v1
14
+ // ignores a reassignment of output.system.
15
+ //
16
+ // Auto-discovered from .opencode/plugins/ in both versions, so no
17
+ // opencode.json entry is needed. To register it by hand instead:
18
+ // { "plugin": ["./.opencode/plugins/plain-style.js"] } (opencode v1)
19
+ // { "plugins": ["./.opencode/plugins/plain-style.js"] } (opencode v2)
20
+
21
+ import fs from 'fs';
22
+ import path from 'path';
23
+ import { fileURLToPath } from 'url';
24
+
25
+ // Read once at load; a missing file throws here so startup fails loud instead
26
+ // of silently injecting nothing.
27
+ const rulebook = fs.readFileSync(
28
+ path.join(path.dirname(fileURLToPath(import.meta.url)), 'plain-style.md'),
29
+ 'utf8',
30
+ );
31
+
32
+ export default {
33
+ id: 'vasilvestre.opencode-plain-style',
34
+ async setup(ctx) {
35
+ await ctx.session.hook('context', (event) => {
36
+ event.system.push({ type: 'text', text: rulebook });
37
+ });
38
+ },
39
+ async server() {
40
+ return {
41
+ 'experimental.chat.system.transform': async (_input, output) => {
42
+ output.system.push(rulebook);
43
+ },
44
+ };
45
+ },
46
+ };
@@ -14,14 +14,14 @@ descriptions, and your explanations in chat. Code itself is exempt.
14
14
  3. Use present tense. "The function returns", not "the function will return".
15
15
  4. Keep sentences short: about 25 words or fewer. Split longer ones.
16
16
  5. Give one action per step. Never chain two actions with "and" in one step.
17
- 6. Use the imperative for steps. "Run the tests", not "you should run the tests".
18
- 7. Use "must" for requirements. Do not use "should", "may", or "might" for them.
17
+ 6. Use the imperative for steps. "Run the tests", not "running the tests".
18
+ 7. State requirements as facts, with "must". Do not soften requirements.
19
19
  8. Define every term and abbreviation on first use, unless it is common in
20
20
  software (for example: API, URL, JSON).
21
21
  9. Use one term per concept, and one concept per term. Do not alternate between
22
22
  synonyms like "workspace" and "project" for the same thing.
23
- 10. Do not use vague intensifiers: very, really, quite, just, simply, actually,
24
- basically, obviously. Delete them or state the fact they hide.
23
+ 10. Do not use vague intensifiers or words that signal laziness or condescension.
24
+ State the fact directly.
25
25
  11. Prefer lists and headings over walls of text. If a paragraph has more than
26
26
  four sentences, consider splitting it or making it a list.
27
27
  12. Put the most important information first: conclusion before explanation,
@@ -32,12 +32,19 @@ descriptions, and your explanations in chat. Code itself is exempt.
32
32
  14. Format code, commands, flags, file names, and paths in backticks.
33
33
  15. Be concrete. Replace "handle errors appropriately" with what to do:
34
34
  "log the error and return a 400 response".
35
+ 16. End with one line starting with "Next:". Name one concrete action the
36
+ reader can do.
37
+ 17. Do not use em dashes (—) or en dashes (–) as punctuation. Use commas,
38
+ colons, or parentheses. En dashes in numeric ranges like 2020–2026 are
39
+ the exception.
40
+ 18. Do not write "please" in instructions. Do not write "etc." or
41
+ "and so on". Complete the list or name the count.
35
42
 
36
43
  ## Code comments
37
44
 
38
45
  - Explain why the code exists, not what it does. The code shows what.
39
46
  - Do not leave commented-out code. Delete it. Git keeps the history.
40
- - Write TODO comments as `TODO(name): action — reason`, one line.
47
+ - Write TODO comments as `TODO(name): action, reason`, one line.
41
48
 
42
49
  ## Commit messages
43
50
 
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # opencode-plain-style
2
2
 
3
+ [![CI](https://github.com/vasilvestre/opencode-plain-style/actions/workflows/ci.yml/badge.svg)](https://github.com/vasilvestre/opencode-plain-style/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@vasilvestre/opencode-plain-style)](https://www.npmjs.com/package/@vasilvestre/opencode-plain-style)
5
+ [![license](https://img.shields.io/npm/l/@vasilvestre/opencode-plain-style)](LICENSE)
6
+
3
7
  Plain-language writing rules for opencode agents, injected into every system prompt.
4
8
 
5
9
  **Measured effect: fully-compliant prose rises from 66.7% to 93.3% of runs (+26.6 points).**
@@ -17,16 +21,18 @@ Rules include:
17
21
  - Active voice, second person, present tense.
18
22
  - Sentences of 25 words or fewer.
19
23
  - Imperative mood for steps, one action per step.
20
- - `must` for requirements, never hedging modal verbs.
21
- - A banned list of vague intensifiers.
24
+ - `must` for requirements, stated as facts without softening.
25
+ - No vague intensifiers or words that signal laziness or condescension.
22
26
  - Lists and headings over walls of text, conclusion first.
27
+ - One "Next:" line at the end naming a concrete action.
28
+ - No em dashes as punctuation, no "please" in instructions, no "etc.".
23
29
 
24
30
  Because the injection happens per request, the rules survive context compaction.
25
31
  AGENTS.md instructions, by contrast, get summarized away during compaction.
26
32
 
27
33
  ## The rules, by example
28
34
 
29
- Five of the sixteen rules, before and after.
35
+ Five of the nineteen rules, before and after.
30
36
  Full rulebook: [.opencode/plugins/plain-style.md](.opencode/plugins/plain-style.md).
31
37
 
32
38
  ```text
@@ -53,27 +59,98 @@ Rule: one action per step
53
59
 
54
60
  The pattern: every "before" hides a fact or an action. Every "after" states it.
55
61
 
62
+ ## Rule decisions
63
+
64
+ Every rule in the rulebook earned its place through an A/B benchmark.
65
+ Rules enter only if their compliance lift beats the measured dilution cost
66
+ (~2.2 points per added rule) and they leave only if the model stays at ≥86.7%
67
+ fully compliant without them. Ablations on two models found no rule cuttable.
68
+ Full experiment history: [bench/README.md](bench/README.md).
69
+
70
+ Decisions from the English Language proposal (2026-08-02):
71
+
72
+ | Proposed rule | Decision | Why |
73
+ | ---------------------------------------------------------- | -------- | ---------------------------------------------- |
74
+ | Avoid belittling words (obviously, just, simply, ...) | Adopted | Folded into Rule 10, without the word list |
75
+ | Avoid passive voice | Kept | Already covered by Rule 1 |
76
+ | American English, title case, no Oxford commas, | Not | Not checkable by the scorer, and each injected |
77
+ | gender-neutral pronouns, contractions allowed | injected | rule costs ~2.2 points of dilution |
78
+
79
+ One design lesson shapes the phrasing: never list banned words in the rulebook.
80
+ Weaker models echo the list and produce more violations, not fewer.
81
+ Rules state what to do, not what to avoid by name.
82
+
56
83
  ## Install
57
84
 
58
- Add the package to the `plugin` array in `~/.config/opencode/opencode.json`:
85
+ ### Requirements
59
86
 
60
- ```json
61
- {
62
- "plugin": ["@vasilvestre/opencode-plain-style"]
63
- }
64
- ```
87
+ - opencode installed and runnable (`opencode --version` prints a version).
88
+ - A config file at `~/.config/opencode/opencode.json`. Create it if it does not exist.
89
+
90
+ ### Steps
91
+
92
+ 1. Open `~/.config/opencode/opencode.json`.
93
+ 2. Add the package name to the plugin array. opencode v2 uses the `plugins` key, opencode v1 uses `plugin`:
94
+
95
+ ```json
96
+ {
97
+ "plugins": ["@vasilvestre/opencode-plain-style"]
98
+ }
99
+ ```
100
+
101
+ ```json
102
+ {
103
+ "plugin": ["@vasilvestre/opencode-plain-style"]
104
+ }
105
+ ```
106
+
107
+ opencode downloads the package from npm on the next start. You do not need to run `npm install`.
108
+
109
+ 3. Restart opencode.
110
+ 4. Verify the install. Ask the agent to describe a process. The answer must follow the rules: numbered steps, active voice, sentences of 25 words or fewer.
65
111
 
66
- Restart opencode. The plugin loads at startup and works for the primary agent and every subagent.
112
+ The plugin loads at startup and works for the primary agent and every subagent.
113
+
114
+ ### Install from a local copy
115
+
116
+ Use this to test changes before publishing, or to run a fork.
117
+
118
+ 1. Clone the repository:
119
+
120
+ ```bash
121
+ git clone https://github.com/vasilvestre/opencode-plain-style.git
122
+ ```
123
+
124
+ 2. Point the config at the local plugin file:
125
+
126
+ ```json
127
+ {
128
+ "plugins": ["file:///absolute/path/to/opencode-plain-style/.opencode/plugins/plain-style.js"]
129
+ }
130
+ ```
131
+
132
+ opencode v1 uses the same URL under the `plugin` key. The path must be absolute and must point at the `.js` file, not the directory.
133
+
134
+ 3. Restart opencode.
135
+
136
+ ### Uninstall
137
+
138
+ 1. Remove the entry from the plugin array (`plugins` in opencode v2, `plugin` in v1) in `~/.config/opencode/opencode.json`.
139
+ 2. Restart opencode.
67
140
 
68
141
  ## How it works
69
142
 
70
- The plugin registers one hook: `experimental.chat.system.transform`.
71
- On each request it pushes the rulebook text into the system prompt array.
143
+ The plugin exports one object with two entrypoints; opencode picks the one matching its version:
144
+
145
+ - opencode v1 calls `server()`. It hooks `experimental.chat.system.transform` and pushes the rulebook text (a string) into the system prompt array on each request.
146
+ - opencode v2 calls `setup()`. It registers a `context` session hook and appends a `{ type: "text" }` part to the system prompt.
147
+
72
148
  The rulebook is read from disk once at module load, so each request costs one array push and zero I/O.
73
- Total plugin code: about 25 lines, no dependencies.
149
+ The plugin has no dependencies.
74
150
 
75
- Note: the hook is marked experimental in opencode.
76
- If opencode renames it, the plugin fails loudly at startup rather than injecting nothing.
151
+ In opencode v1 the hook must mutate `output.system` in place: a reassignment is silently ignored.
152
+
153
+ Note: v1 support needs opencode 1.18.29 or later (object-style plugin entrypoints).
77
154
 
78
155
  ## Example output
79
156
 
@@ -126,6 +203,9 @@ A/B design: same agent, same pinned model, two config replicas identical except
126
203
  Injection shifts probability; it does not guarantee compliance.
127
204
  For a hard guarantee, add deterministic linting (for example Vale) to CI.
128
205
 
206
+ The table shows the original 16-rule baseline. The current 19-rule rulebook
207
+ re-validated on the same model at +26.7 points (53.3% to 80.0%, 2026-08-03).
208
+
129
209
  Full details: [bench/BENCHMARK.md](bench/BENCHMARK.md).
130
210
 
131
211
  ## Customize the rules
@@ -135,10 +215,12 @@ Fork the repo, edit the file, and point your config at the local copy:
135
215
 
136
216
  ```json
137
217
  {
138
- "plugin": ["file:///path/to/your/fork/.opencode/plugins/plain-style.mjs"]
218
+ "plugins": ["file:///path/to/your/fork/.opencode/plugins/plain-style.js"]
139
219
  }
140
220
  ```
141
221
 
222
+ opencode v1 uses the `plugin` key instead.
223
+
142
224
  ## Development
143
225
 
144
226
  The `bench/` directory holds the full benchmark suite: harness, scorer, aggregator, raw results.
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@vasilvestre/opencode-plain-style",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Plain-language writing rules for opencode agents, injected into every system prompt. Enforces ISO 24495-1 principles on commits, comments, docs, and chat prose.",
5
- "keywords": ["opencode-plugin", "opencode", "plain-language", "writing", "style-guide"],
5
+ "keywords": [
6
+ "opencode-plugin",
7
+ "opencode",
8
+ "plain-language",
9
+ "writing",
10
+ "style-guide"
11
+ ],
6
12
  "license": "MIT",
7
13
  "author": {
8
14
  "name": "vasilvestre",
@@ -17,10 +23,13 @@
17
23
  "url": "https://github.com/vasilvestre/opencode-plain-style/issues"
18
24
  },
19
25
  "type": "module",
20
- "main": "./.opencode/plugins/plain-style.mjs",
26
+ "main": "./.opencode/plugins/plain-style.js",
21
27
  "exports": {
22
- ".": "./.opencode/plugins/plain-style.mjs",
23
- "./plugin": "./.opencode/plugins/plain-style.mjs"
28
+ ".": "./.opencode/plugins/plain-style.js",
29
+ "./plugin": "./.opencode/plugins/plain-style.js"
30
+ },
31
+ "scripts": {
32
+ "test": "node scripts/check-plugin.mjs"
24
33
  },
25
34
  "files": [
26
35
  ".opencode/",
@@ -1,25 +0,0 @@
1
- // plain-style — OpenCode plugin.
2
- //
3
- // Injects the plain-language rulebook (plain-style.md, beside this file) into
4
- // every chat's system prompt on every LLM request — primary agent and
5
- // subagents alike — so the rules survive context compaction. Pattern cloned
6
- // from the ponytail plugin (experimental.chat.system.transform).
7
- //
8
- // Registered in opencode.jsonc: { "plugin": [..., "./plugins/plain-style.mjs"] }
9
-
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { fileURLToPath } from 'url';
13
-
14
- // Read once at load; a missing file throws here so startup fails loud instead
15
- // of silently injecting nothing.
16
- const rulebook = fs.readFileSync(
17
- path.join(path.dirname(fileURLToPath(import.meta.url)), 'plain-style.md'),
18
- 'utf8',
19
- );
20
-
21
- export default async () => ({
22
- 'experimental.chat.system.transform': async (_input, output) => {
23
- output.system.push(rulebook);
24
- },
25
- });