pi-simple-english 0.8.1 → 0.10.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 +10 -0
- package/extensions/se-lint.ts +128 -47
- package/package.json +1 -1
- package/skills/lint-simple-english/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -78,6 +78,16 @@ The `write-simple-english` skill carries the rules, so the agent
|
|
|
78
78
|
writes clean prose the first time. The `lint-simple-english` skill
|
|
79
79
|
tells the agent to lint the prose it writes and fix every finding.
|
|
80
80
|
|
|
81
|
+
## Toggle
|
|
82
|
+
|
|
83
|
+
Turn the linter off for the session with `/se off`. That hides the
|
|
84
|
+
`se_lint` tool and stops the edit enforcement. Turn it back on with
|
|
85
|
+
`/se on`, or run `/se` to flip the current state. The status line
|
|
86
|
+
shows `● simple-english` while it is on.
|
|
87
|
+
|
|
88
|
+
The toggle lasts for the session. To stop the linter for every
|
|
89
|
+
session, uninstall the package: `pi uninstall npm:pi-simple-english`.
|
|
90
|
+
|
|
81
91
|
## Versions
|
|
82
92
|
|
|
83
93
|
The package version tracks the gem version. The changelog lives in
|
package/extensions/se-lint.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* se_lint - lint Markdown prose and code comments with simple_english.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three layers. The se_lint tool lints on demand. Enforcement runs the
|
|
5
5
|
* same lint after every edit or write and appends the findings to the
|
|
6
6
|
* tool result, so the agent sees them in the turn that made them. It
|
|
7
|
-
* never blocks the edit. The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* never blocks the edit. The /se command gates both layers per session:
|
|
8
|
+
* off hides the tool from the model and skips enforcement. The contract
|
|
9
|
+
* mirrors the claude-code hook in integrations/claude-code/hooks/lint.sh:
|
|
10
|
+
* a clean file stays silent, a failed lint says so instead of reading as
|
|
11
|
+
* clean, and a missing binary names the install step.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { execFile } from "node:child_process";
|
|
@@ -15,6 +16,7 @@ import { Type } from "typebox";
|
|
|
15
16
|
import type {
|
|
16
17
|
AgentToolResult,
|
|
17
18
|
ExtensionAPI,
|
|
19
|
+
ExtensionContext,
|
|
18
20
|
ToolResultEvent,
|
|
19
21
|
ToolResultEventResult,
|
|
20
22
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -24,6 +26,9 @@ const OUTPUT_LIMIT = 4000;
|
|
|
24
26
|
// ever proves slow in practice, cut to 30s and pre-warm via `se serve`.
|
|
25
27
|
const TIMEOUT_MS = 120_000;
|
|
26
28
|
|
|
29
|
+
const STATE_ENTRY_TYPE = "se-lint-state";
|
|
30
|
+
const STATUS_KEY = "se";
|
|
31
|
+
|
|
27
32
|
interface SeResult {
|
|
28
33
|
stdout: string;
|
|
29
34
|
code: number;
|
|
@@ -36,6 +41,8 @@ interface SeDetails {
|
|
|
36
41
|
full?: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
type SeLintState = { enabled: boolean };
|
|
45
|
+
|
|
39
46
|
function seLint(path: string, signal?: AbortSignal): Promise<SeResult> {
|
|
40
47
|
return new Promise((resolve, reject) => {
|
|
41
48
|
execFile(
|
|
@@ -81,60 +88,131 @@ async function lintFeedback(path: string): Promise<string | undefined> {
|
|
|
81
88
|
return `se found ${findings} lint finding(s) in ${path}. Fix them, then lint again:\n${text}`;
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
// The last state entry on the branch wins. Earlier toggles are history,
|
|
92
|
+
// not the current state.
|
|
93
|
+
function savedEnabled(ctx: ExtensionContext): boolean | undefined {
|
|
94
|
+
let saved: boolean | undefined;
|
|
95
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
96
|
+
if (entry.type !== "custom" || entry.customType !== STATE_ENTRY_TYPE) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const data = entry.data as Partial<SeLintState> | undefined;
|
|
100
|
+
if (typeof data?.enabled === "boolean") {
|
|
101
|
+
saved = data.enabled;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return saved;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
export default function (pi: ExtensionAPI) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
let enabled = true;
|
|
109
|
+
|
|
110
|
+
const updateStatus = (ctx: ExtensionContext): void => {
|
|
111
|
+
if (!enabled) {
|
|
112
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const dot = ctx.ui.theme.fg("success", "●");
|
|
116
|
+
ctx.ui.setStatus(STATUS_KEY, `${dot} ${ctx.ui.theme.fg("accent", "simple-english")}`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const registerLintTool = (exposure: "direct" | "hidden"): void => {
|
|
120
|
+
pi.registerTool({
|
|
121
|
+
name: "se_lint",
|
|
122
|
+
label: "Lint prose",
|
|
123
|
+
description:
|
|
124
|
+
"Lint Markdown prose and code comments against plain-English rules (short sentences, active voice, no jargon, no filler). " +
|
|
125
|
+
"Call this after writing or editing any documentation file (.md) or prose-heavy text to verify it reads clean. " +
|
|
126
|
+
"Also call it on demand when asked to lint or simplify text. " +
|
|
127
|
+
"Input: a file or directory path. Returns findings with line numbers and suggested rewrites, or 'clean' when nothing is flagged. " +
|
|
128
|
+
"After fixing findings, re-run until it reports clean.",
|
|
129
|
+
parameters: Type.Object({
|
|
130
|
+
path: Type.String({ description: "File or directory path to lint, relative to the CWD or absolute" }),
|
|
131
|
+
}),
|
|
132
|
+
exposure,
|
|
133
|
+
|
|
134
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<SeDetails>> {
|
|
135
|
+
let stdout: string;
|
|
136
|
+
let code: number;
|
|
137
|
+
try {
|
|
138
|
+
({ stdout, code } = await seLint(params.path, signal));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const e = err as NodeJS.ErrnoException;
|
|
141
|
+
if (e.code === "ENOENT") {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"`se` not found. Install it first: `gem install simple_english`, then retry.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`se lint failed: ${e.message}`);
|
|
109
147
|
}
|
|
110
|
-
throw new Error(`se lint failed: ${e.message}`);
|
|
111
|
-
}
|
|
112
148
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
if (code !== 0 && code !== 1) {
|
|
150
|
+
throw new Error(`se lint exited with code ${code}: ${stdout.trim()}`);
|
|
151
|
+
}
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
const full = stdout.trim();
|
|
154
|
+
if (!full) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text" as const, text: `clean: no findings in ${params.path}` }],
|
|
157
|
+
details: { path: params.path, exitCode: code, findings: 0, full: undefined },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const findings = full.split("\n").length;
|
|
162
|
+
const text =
|
|
163
|
+
full.length > OUTPUT_LIMIT
|
|
164
|
+
? `${full.slice(0, OUTPUT_LIMIT)}\n... truncated (${full.length} chars, ${findings} findings). Full output: run \`se lint ${params.path}\` with bash.`
|
|
165
|
+
: `${findings} finding(s) in ${params.path}:\n${full}`;
|
|
119
166
|
return {
|
|
120
|
-
content: [{ type: "text" as const, text
|
|
121
|
-
details: { path: params.path, exitCode: code, findings
|
|
167
|
+
content: [{ type: "text" as const, text }],
|
|
168
|
+
details: { path: params.path, exitCode: code, findings, full },
|
|
122
169
|
};
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const setEnabled = (next: boolean, ctx: ExtensionContext): void => {
|
|
175
|
+
enabled = next;
|
|
176
|
+
pi.appendEntry(STATE_ENTRY_TYPE, { enabled } satisfies SeLintState);
|
|
177
|
+
registerLintTool(next ? "direct" : "hidden");
|
|
178
|
+
updateStatus(ctx);
|
|
179
|
+
ctx.ui.notify(`Prose lint ${next ? "on" : "off"}`, "info");
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const restoreState = (ctx: ExtensionContext): void => {
|
|
183
|
+
enabled = savedEnabled(ctx) ?? true;
|
|
184
|
+
registerLintTool(enabled ? "direct" : "hidden");
|
|
185
|
+
updateStatus(ctx);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
registerLintTool("direct");
|
|
189
|
+
|
|
190
|
+
pi.registerCommand("se", {
|
|
191
|
+
description: "Toggle prose linting (se_lint tool and edit enforcement) for this session",
|
|
192
|
+
handler: async (args, ctx) => {
|
|
193
|
+
const argument = args.trim().toLowerCase();
|
|
194
|
+
|
|
195
|
+
if (argument === "") {
|
|
196
|
+
setEnabled(!enabled, ctx);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (argument === "on") {
|
|
201
|
+
setEnabled(true, ctx);
|
|
202
|
+
return;
|
|
123
203
|
}
|
|
124
204
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
content: [{ type: "text" as const, text }],
|
|
132
|
-
details: { path: params.path, exitCode: code, findings, full },
|
|
133
|
-
};
|
|
205
|
+
if (argument === "off" || argument === "stop") {
|
|
206
|
+
setEnabled(false, ctx);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctx.ui.notify("Usage: /se [on|off]", "warning");
|
|
134
211
|
},
|
|
135
212
|
});
|
|
136
213
|
|
|
137
214
|
pi.on("tool_result", async (event: ToolResultEvent): Promise<ToolResultEventResult | undefined> => {
|
|
215
|
+
if (!enabled) return;
|
|
138
216
|
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
139
217
|
// A failed edit wrote nothing: there is nothing to lint.
|
|
140
218
|
if (event.isError) return;
|
|
@@ -144,4 +222,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
222
|
if (!feedback) return;
|
|
145
223
|
return { content: [...event.content, { type: "text" as const, text: feedback }] };
|
|
146
224
|
});
|
|
225
|
+
|
|
226
|
+
pi.on("session_start", async (_event, ctx) => restoreState(ctx));
|
|
227
|
+
pi.on("session_tree", async (_event, ctx) => restoreState(ctx));
|
|
147
228
|
}
|
package/package.json
CHANGED
|
@@ -7,7 +7,8 @@ description: Lint prose with simple_english after writing or editing documentati
|
|
|
7
7
|
|
|
8
8
|
Lint prose the linter can see. Call the lint tool after writing or
|
|
9
9
|
editing any documentation file or prose-heavy text. Read the findings.
|
|
10
|
-
Fix every finding
|
|
10
|
+
Fix every finding, also in prose you did not write. Re-run
|
|
11
|
+
until the result is clean.
|
|
11
12
|
|
|
12
13
|
The tool names differ by agent:
|
|
13
14
|
|