pi-simple-english 0.7.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,42 +1,86 @@
1
1
  # simple-english for `pi`
2
2
 
3
- A `pi` package that lints the prose the agent writes.
3
+ > Write for human, not for agent.
4
4
 
5
- The package ships the `lint-simple-english` skill, the
6
- `write-simple-english` skill, and the `se_lint` tool. The tool lints
7
- a file or a directory. The write skill carries the rules, so the
8
- agent writes clean prose the first time. The lint skill tells the
9
- agent to lint the prose it writes and fix every finding.
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
- ## Prerequisites
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
- The linter is a Ruby gem. Install it once, before the adapter:
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
- ```bash
16
- gem install simple_english
17
- ```
21
+ </td>
22
+ <td valign="top">
18
23
 
19
- ## Install
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
- For yourself, on every project:
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
- For one project, with the team, install into the project instead. Run
28
- the install with `-l` from the project root:
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
- pi install npm:pi-simple-english -l
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 project install writes `.pi/settings.json`, which you commit. Pi
35
- loads it only after you grant project trust.
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 is the
40
- repository changelog. See the section that matches the version:
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
@@ -1,19 +1,86 @@
1
1
  /**
2
2
  * se_lint - lint Markdown prose and code comments with simple_english.
3
3
  *
4
- * Shells out to the `se` CLI. Assumes the gem is installed; on a
5
- * missing binary the tool fails with install steps.
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 { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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 new Promise<{ stdout: string; code: number }>((resolve, reject) => {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-simple-english",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Lint prose and code comments with the simple_english plain-English rules.",
5
5
  "type": "module",
6
6
  "license": "MIT",