pi-simple-english 0.7.0 → 0.8.1
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 +64 -20
- package/extensions/se-lint.ts +84 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,42 +1,86 @@
|
|
|
1
1
|
# simple-english for `pi`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Write for human, not for agent.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
agent
|
|
9
|
-
|
|
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.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<th width="50%">Before</th>
|
|
14
|
+
<th width="50%">After</th>
|
|
15
|
+
</tr>
|
|
16
|
+
<tr>
|
|
17
|
+
<td valign="top">
|
|
12
18
|
|
|
13
|
-
|
|
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 -->
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```
|
|
21
|
+
</td>
|
|
22
|
+
<td valign="top">
|
|
18
23
|
|
|
19
|
-
|
|
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>
|
|
20
29
|
|
|
21
|
-
|
|
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).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
22
51
|
|
|
23
52
|
```bash
|
|
24
53
|
pi install npm:pi-simple-english
|
|
25
54
|
```
|
|
26
55
|
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
Install it for one project only by adding `-l` and running the
|
|
57
|
+
command from the project root.
|
|
58
|
+
|
|
59
|
+
The tool calls the `se` CLI. That CLI ships as a Ruby gem. Install
|
|
60
|
+
it once, before the first lint:
|
|
29
61
|
|
|
30
62
|
```bash
|
|
31
|
-
|
|
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.
|
|
32
75
|
```
|
|
33
76
|
|
|
34
|
-
The
|
|
35
|
-
|
|
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.
|
|
36
80
|
|
|
37
81
|
## Versions
|
|
38
82
|
|
|
39
|
-
The package version tracks the gem version. The changelog
|
|
40
|
-
|
|
83
|
+
The package version tracks the gem version. The changelog lives in
|
|
84
|
+
the repository:
|
|
41
85
|
|
|
42
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
|
}
|