pi-simple-english 0.6.0 → 0.8.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.
package/README.md
CHANGED
|
@@ -1,38 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# simple-english for `pi`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Write for human, not for agent.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You can teach an agent plain-English prose with a skill or a
|
|
6
|
+
prompt. It writes clean prose in the first paragraph and slop again
|
|
7
|
+
by the third. This linter keeps the agent honest: it reads what the
|
|
8
|
+
agent wrote and names every broken rule. Send the findings back, and
|
|
9
|
+
the agent corrects its own text.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<th width="50%">Before</th>
|
|
14
|
+
<th width="50%">After</th>
|
|
15
|
+
</tr>
|
|
16
|
+
<tr>
|
|
17
|
+
<td valign="top">
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
> Your API keys are rotated by the service every 90 days — you will need to update them if you get a 401; don't panic, simply re-run the setup command, which regenerates the credentials without any downtime. <!-- se: ignore -->
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
</td>
|
|
22
|
+
<td valign="top">
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
> The service rotates your API keys every 90 days. If you get a 401, update them. Do not panic. Re-run the setup command. It regenerates the credentials without any downtime.
|
|
25
|
+
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</table>
|
|
29
|
+
|
|
30
|
+
The linter covers Markdown prose plus code comments in Python, Ruby,
|
|
31
|
+
JavaScript, TypeScript, Go, Rust, Java, C#, C++, Kotlin, bash, and
|
|
32
|
+
YAML.
|
|
33
|
+
|
|
34
|
+
## What it checks
|
|
35
|
+
|
|
36
|
+
- **Voice:** say who does the action.
|
|
37
|
+
- **Tense:** simple tenses only, no present perfect.
|
|
38
|
+
- **Modals:** `can`, `will`, `must` only.
|
|
39
|
+
- **Punctuation:** no em-dashes, no semicolons.
|
|
40
|
+
- **Contractions:** write every word in full.
|
|
41
|
+
- **Sentence shape:** condition before command, no `-ing` phrase after a comma.
|
|
42
|
+
- **Word choice:** about 50 substitution rules, from `leverage` to `in conclusion`.
|
|
43
|
+
- **Counts:** 20 words per sentence in list items, 25 in paragraphs, six sentences per paragraph at most.
|
|
44
|
+
|
|
45
|
+
It lints Markdown prose plus code comments in Python, Ruby,
|
|
46
|
+
JavaScript, TypeScript, Go, Rust, Java, C#, C++, Kotlin, bash, and
|
|
47
|
+
YAML. The full list, with a wrong and a right example for each rule,
|
|
48
|
+
is [docs/RULES.md](https://github.com/TonyCTHsu/simple-english/blob/master/docs/RULES.md).
|
|
18
49
|
|
|
19
50
|
## Install
|
|
20
51
|
|
|
21
|
-
|
|
52
|
+
```bash
|
|
53
|
+
pi install npm:pi-simple-english
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Install it for one project only by adding `-l` and running the
|
|
57
|
+
command from the project root.
|
|
22
58
|
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
The tool calls the `se` CLI. That CLI ships as a Ruby gem. Install
|
|
60
|
+
it once, before the first lint:
|
|
25
61
|
|
|
26
62
|
```bash
|
|
27
|
-
|
|
63
|
+
gem install simple_english
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What you get
|
|
67
|
+
|
|
68
|
+
The `se_lint` tool lints a file or a directory on demand. Every
|
|
69
|
+
file edit lints too. The findings land in the edit result, so the
|
|
70
|
+
agent fixes them in the same turn. Each finding carries a line
|
|
71
|
+
number, a rule ID, and a fix hint:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
README.md:12:31-32: [SE_NO_SEMICOLON] ";" - Write two sentences, or name the relation.
|
|
28
75
|
```
|
|
29
76
|
|
|
30
|
-
The
|
|
31
|
-
|
|
77
|
+
The `write-simple-english` skill carries the rules, so the agent
|
|
78
|
+
writes clean prose the first time. The `lint-simple-english` skill
|
|
79
|
+
tells the agent to lint the prose it writes and fix every finding.
|
|
32
80
|
|
|
33
81
|
## Versions
|
|
34
82
|
|
|
35
|
-
The package version tracks the gem version. The changelog
|
|
36
|
-
|
|
83
|
+
The package version tracks the gem version. The changelog lives in
|
|
84
|
+
the repository:
|
|
37
85
|
|
|
38
86
|
https://github.com/TonyCTHsu/simple-english/blob/master/CHANGELOG.md
|
package/extensions/se-lint.ts
CHANGED
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* se_lint - lint Markdown prose and code comments with simple_english.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two layers. The se_lint tool lints on demand. Enforcement runs the
|
|
5
|
+
* same lint after every edit or write and appends the findings to the
|
|
6
|
+
* tool result, so the agent sees them in the turn that made them. It
|
|
7
|
+
* never blocks the edit. The contract mirrors the claude-code hook in
|
|
8
|
+
* integrations/claude-code/hooks/lint.sh: a clean file stays silent,
|
|
9
|
+
* a failed lint says so instead of reading as clean, and a missing
|
|
10
|
+
* binary names the install step.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { execFile } from "node:child_process";
|
|
9
14
|
import { Type } from "typebox";
|
|
10
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
AgentToolResult,
|
|
17
|
+
ExtensionAPI,
|
|
18
|
+
ToolResultEvent,
|
|
19
|
+
ToolResultEventResult,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
21
|
|
|
12
22
|
const OUTPUT_LIMIT = 4000;
|
|
13
23
|
// ponytail: 120s allows a cold daemon boot (LanguageTool + JVM). If that
|
|
14
24
|
// ever proves slow in practice, cut to 30s and pre-warm via `se serve`.
|
|
15
25
|
const TIMEOUT_MS = 120_000;
|
|
16
26
|
|
|
27
|
+
interface SeResult {
|
|
28
|
+
stdout: string;
|
|
29
|
+
code: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SeDetails {
|
|
33
|
+
path: string;
|
|
34
|
+
exitCode: number;
|
|
35
|
+
findings: number;
|
|
36
|
+
full?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function seLint(path: string, signal?: AbortSignal): Promise<SeResult> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
execFile(
|
|
42
|
+
"se",
|
|
43
|
+
["lint", path],
|
|
44
|
+
{ timeout: TIMEOUT_MS, maxBuffer: 1024 * 1024, signal },
|
|
45
|
+
(error, stdout) => {
|
|
46
|
+
if (error && typeof error.code !== "number") {
|
|
47
|
+
reject(error);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
resolve({ stdout, code: (error?.code as number | undefined) ?? 0 });
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The feedback appended to an edit or write result. undefined means
|
|
57
|
+
// clean: nothing is appended, so a clean edit reads as a clean edit.
|
|
58
|
+
async function lintFeedback(path: string): Promise<string | undefined> {
|
|
59
|
+
let result: SeResult;
|
|
60
|
+
try {
|
|
61
|
+
result = await seLint(path);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const e = err as NodeJS.ErrnoException;
|
|
64
|
+
if (e.code === "ENOENT") {
|
|
65
|
+
return "`se` is not installed, so the file was not linted. Install it first: `gem install simple_english`, then retry.";
|
|
66
|
+
}
|
|
67
|
+
return `se lint failed, so the file was not linted. Run \`se lint ${path}\` to see the error.`;
|
|
68
|
+
}
|
|
69
|
+
if (result.code >= 2) {
|
|
70
|
+
return `se exited ${result.code} and the file was not linted. Run \`se lint ${path}\` to see the error.`;
|
|
71
|
+
}
|
|
72
|
+
const full = result.stdout.trim();
|
|
73
|
+
if (result.code === 0 || !full) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const findings = full.split("\n").length;
|
|
77
|
+
const text =
|
|
78
|
+
full.length > OUTPUT_LIMIT
|
|
79
|
+
? `${full.slice(0, OUTPUT_LIMIT)}\n... truncated (${full.length} chars, ${findings} findings). Full output: run \`se lint ${path}\` with bash.`
|
|
80
|
+
: full;
|
|
81
|
+
return `se found ${findings} lint finding(s) in ${path}. Fix them, then lint again:\n${text}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
17
84
|
export default function (pi: ExtensionAPI) {
|
|
18
85
|
pi.registerTool({
|
|
19
86
|
name: "se_lint",
|
|
@@ -28,24 +95,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
95
|
path: Type.String({ description: "File or directory to lint, relative to the CWD or absolute" }),
|
|
29
96
|
}),
|
|
30
97
|
|
|
31
|
-
async execute(_toolCallId, params, signal) {
|
|
98
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<SeDetails>> {
|
|
32
99
|
let stdout: string;
|
|
33
100
|
let code: number;
|
|
34
101
|
try {
|
|
35
|
-
({ stdout, code } = await
|
|
36
|
-
execFile(
|
|
37
|
-
"se",
|
|
38
|
-
["lint", params.path],
|
|
39
|
-
{ timeout: TIMEOUT_MS, maxBuffer: 1024 * 1024, signal },
|
|
40
|
-
(error, out) => {
|
|
41
|
-
if (error && typeof error.code !== "number") {
|
|
42
|
-
reject(error);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
resolve({ stdout: out, code: (error?.code as number | undefined) ?? 0 });
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
}));
|
|
102
|
+
({ stdout, code } = await seLint(params.path, signal));
|
|
49
103
|
} catch (err) {
|
|
50
104
|
const e = err as NodeJS.ErrnoException;
|
|
51
105
|
if (e.code === "ENOENT") {
|
|
@@ -64,7 +118,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
64
118
|
if (!full) {
|
|
65
119
|
return {
|
|
66
120
|
content: [{ type: "text" as const, text: `clean: no findings in ${params.path}` }],
|
|
67
|
-
details: { path: params.path, exitCode: code, findings: 0 },
|
|
121
|
+
details: { path: params.path, exitCode: code, findings: 0, full: undefined },
|
|
68
122
|
};
|
|
69
123
|
}
|
|
70
124
|
|
|
@@ -79,4 +133,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
79
133
|
};
|
|
80
134
|
},
|
|
81
135
|
});
|
|
136
|
+
|
|
137
|
+
pi.on("tool_result", async (event: ToolResultEvent): Promise<ToolResultEventResult | undefined> => {
|
|
138
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
139
|
+
// A failed edit wrote nothing: there is nothing to lint.
|
|
140
|
+
if (event.isError) return;
|
|
141
|
+
const path = event.input.path;
|
|
142
|
+
if (typeof path !== "string" || path === "") return;
|
|
143
|
+
const feedback = await lintFeedback(path);
|
|
144
|
+
if (!feedback) return;
|
|
145
|
+
return { content: [...event.content, { type: "text" as const, text: feedback }] };
|
|
146
|
+
});
|
|
82
147
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: simple-english
|
|
2
|
+
name: lint-simple-english
|
|
3
3
|
description: Lint prose with simple_english after writing or editing documentation,
|
|
4
4
|
READMEs, or code comments. Use when the agent writes prose, when asked to lint
|
|
5
5
|
or simplify text, or when a doc change lands.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-simple-english
|
|
3
|
+
description: Write plain English on the first pass. Use whenever the
|
|
4
|
+
output is prose, such as READMEs, docs, comments, replies, or any
|
|
5
|
+
other text a reader must understand. Use when asked for simple
|
|
6
|
+
English or no jargon.
|
|
7
|
+
metadata:
|
|
8
|
+
inspired-by: ASD-STE100 Simplified Technical English
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Write prose (READMEs, docs, comments) for a reader whose task is to
|
|
12
|
+
understand the idea behind the words. They value clarity and
|
|
13
|
+
conciseness. Their attention is limited, and every word competes for
|
|
14
|
+
it. Minimize their cognitive load: common words, short sentences,
|
|
15
|
+
one idea at a time.
|
|
16
|
+
|
|
17
|
+
These rules are a controlled language like ASD-STE100, the aerospace
|
|
18
|
+
standard for maintenance manuals. They bind: a sentence
|
|
19
|
+
outside them is an error, not a style choice.
|
|
20
|
+
|
|
21
|
+
## How to write
|
|
22
|
+
|
|
23
|
+
### Mechanics
|
|
24
|
+
|
|
25
|
+
Write the words in full. No contractions, no semicolons, no
|
|
26
|
+
em-dashes. Two short sentences beat one joined pair. Write "A or B,
|
|
27
|
+
or both", not and/or. Write "for example", not e.g., and "that is",
|
|
28
|
+
not i.e. Name the items instead of etc.
|
|
29
|
+
|
|
30
|
+
### Verbs
|
|
31
|
+
|
|
32
|
+
Use the active voice. Say who does the action. Use simple tenses.
|
|
33
|
+
State a requirement with can, will, or must. Put the condition
|
|
34
|
+
before the command: "If the port is taken, pick another." No -ing
|
|
35
|
+
phrase after a comma. Start a new sentence.
|
|
36
|
+
|
|
37
|
+
### Keep it short
|
|
38
|
+
|
|
39
|
+
Cut every word that carries no idea. One idea per sentence. One idea
|
|
40
|
+
per paragraph. A sentence carries at most 20 words in instructions,
|
|
41
|
+
25 in explanations. A paragraph carries at most 6 sentences.
|
|
42
|
+
|
|
43
|
+
### Protect the code
|
|
44
|
+
|
|
45
|
+
When you rewrite, leave code, identifiers, commands, flags, file
|
|
46
|
+
paths, quoted errors, and product names untouched. The rules govern
|
|
47
|
+
the prose around them.
|
|
48
|
+
|
|
49
|
+
## Swap slop for short words
|
|
50
|
+
|
|
51
|
+
The lists name the common offenders. When a word does what a heading
|
|
52
|
+
bans, swap it, listed or not.
|
|
53
|
+
|
|
54
|
+
### Filler
|
|
55
|
+
|
|
56
|
+
- Empty modifiers: just, very, really, extremely, vibrant, nuanced,
|
|
57
|
+
multifaceted
|
|
58
|
+
- Throat-clearing openers: it is worth noting, it is important to, is
|
|
59
|
+
designed to, aims to, in conclusion, in summary, at the end of the
|
|
60
|
+
day
|
|
61
|
+
- Sign-offs and invitations: "I hope this helps", "let's dive in"
|
|
62
|
+
|
|
63
|
+
### Wordiness
|
|
64
|
+
|
|
65
|
+
- to, not in order to
|
|
66
|
+
- because, not due to the fact that
|
|
67
|
+
- if, not in the event that
|
|
68
|
+
- for, not when it comes to
|
|
69
|
+
- before, not prior to
|
|
70
|
+
- by default, not out of the box
|
|
71
|
+
- state the condition, not "as needed"
|
|
72
|
+
|
|
73
|
+
### Corporate voice
|
|
74
|
+
|
|
75
|
+
- use, not leverage
|
|
76
|
+
- help, not facilitate
|
|
77
|
+
- help, not foster
|
|
78
|
+
- use, not harness
|
|
79
|
+
- make simpler, not streamline
|
|
80
|
+
- function, not functionality
|
|
81
|
+
- you can, not enables you to
|
|
82
|
+
- corrects, not addresses the issue
|
|
83
|
+
- show, not showcase
|
|
84
|
+
- has, not boasts
|
|
85
|
+
- make sure that, not ensure
|
|
86
|
+
- improve, not enhance
|
|
87
|
+
- increase, not elevate
|
|
88
|
+
|
|
89
|
+
### Hype
|
|
90
|
+
|
|
91
|
+
- new, not groundbreaking
|
|
92
|
+
- change, not revolutionize
|
|
93
|
+
- say what changes, not transformative
|
|
94
|
+
- complex, not intricate
|
|
95
|
+
- important, not pivotal
|
|
96
|
+
- careful, not meticulous
|
|
97
|
+
- full, not holistic
|
|
98
|
+
- model, not paradigm
|
|
99
|
+
- area, not realm or landscape
|
|
100
|
+
- interaction, not interplay
|
|
101
|
+
- read, not delve into
|
|
102
|
+
- many, not plethora
|
|
103
|
+
- delete tapestry, testament, and synergy
|
|
104
|
+
- internally, not under the hood
|
|
105
|
+
|
|
106
|
+
### Connectors
|
|
107
|
+
|
|
108
|
+
- also, not furthermore
|
|
109
|
+
- but, not notwithstanding or that being said
|
|
110
|
+
- start, not embark
|
|
111
|
+
- busy, not bustling
|
|
112
|
+
- give the location, not "nestled"
|
|
113
|
+
- say what it does, not "gracefully handles"
|
|
114
|
+
|
|
115
|
+
## Then lint
|
|
116
|
+
|
|
117
|
+
After the text exists, run the lint-simple-english skill on it. The
|
|
118
|
+
draft is done when the lint result is clean.
|
|
119
|
+
|
|
120
|
+
## In chat
|
|
121
|
+
|
|
122
|
+
The reply is prose too, and every rule above applies to it. Answer
|
|
123
|
+
in the first sentence, then support it. Headers and bold belong in
|
|
124
|
+
documents. A list only for steps in order, a code block only for
|
|
125
|
+
text the reader must copy. Define a term in a few words at first
|
|
126
|
+
use. The linter checks files, so the reply carries the rules on its
|
|
127
|
+
own.
|