@evoclock/pi-agentic-driver 0.4.3

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,468 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import fs, { realpathSync } from "node:fs";
6
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
7
+
8
+ export const AIDR_SCHEMA = "agentic-driver.aidr.v1";
9
+ const MAX_TEXT_BYTES = 256 * 1024;
10
+ const MAX_DIFF_PREVIEW_BYTES = 12 * 1024;
11
+ const ALLOWED_FILE_EXTENSIONS = new Set([".md", ".mdx", ".txt", ".html", ".htm"]);
12
+ const CLUTTER_PHRASES = Object.freeze([
13
+ "in order to",
14
+ "due to the fact that",
15
+ "at this point in time",
16
+ "it is important to note that",
17
+ "in the event that",
18
+ "with regard to",
19
+ "subsequent to",
20
+ "a number of",
21
+ "utilize",
22
+ "facilitate",
23
+ "aforementioned",
24
+ ]);
25
+ const STE_WORD_GUIDANCE = Object.freeze({
26
+ utilize: "use",
27
+ facilitate: "help",
28
+ approximately: "about",
29
+ commence: "start",
30
+ terminate: "stop",
31
+ "prior to": "before",
32
+ "subsequent to": "after",
33
+ "in lieu of": "instead of",
34
+ numerous: "many",
35
+ sufficient: "enough",
36
+ });
37
+ const STE_PHRASAL_GUIDANCE = Object.freeze({
38
+ "carry out": "perform",
39
+ "find out": "determine",
40
+ "look at": "examine",
41
+ "set up": "configure or install",
42
+ });
43
+ const STE_MODAL_GUIDANCE = Object.freeze({
44
+ should: "use must for a requirement, or state the recommendation directly",
45
+ may: "use can for ability or must have permission language when needed",
46
+ might: "state the condition and result directly",
47
+ could: "state the condition and result directly",
48
+ would: "state the action and condition directly",
49
+ });
50
+ const STE_DOCUMENT_TYPES = new Set(["procedural", "descriptive"]);
51
+ const PASSIVE_PATTERN = /\b(?:is|are|was|were|be|been|being)\s+(?:\w+ed|\w+en)\b/gi;
52
+ const SENTENCE_PATTERN = /[^.!?\n]+(?:[.!?]+|$)/g;
53
+
54
+ function boundedText(value, label = "text") {
55
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > MAX_TEXT_BYTES) {
56
+ throw new Error(`${label} is missing or exceeds the bounded review size`);
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function words(value) {
62
+ return (value.match(/[A-Za-z0-9][A-Za-z0-9'/-]*/g) ?? []);
63
+ }
64
+
65
+ function visibleProse(value) {
66
+ const lines = value.replaceAll("\r\n", "\n").split("\n");
67
+ let inFence = false;
68
+ let frontmatter = false;
69
+ let seenContent = false;
70
+ const kept = [];
71
+ for (const line of lines) {
72
+ const trimmed = line.trim();
73
+ if (!seenContent && trimmed === "---") {
74
+ frontmatter = !frontmatter;
75
+ continue;
76
+ }
77
+ if (frontmatter) continue;
78
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
79
+ inFence = !inFence;
80
+ continue;
81
+ }
82
+ if (inFence) continue;
83
+ if (trimmed) seenContent = true;
84
+ kept.push(line.replace(/<[^>]+>/g, " "));
85
+ }
86
+ return kept.join("\n");
87
+ }
88
+
89
+ function sentenceRecords(prose) {
90
+ return [...prose.matchAll(SENTENCE_PATTERN)]
91
+ .map((match) => match[0].trim())
92
+ .filter(Boolean)
93
+ .map((sentence) => ({ sentence, wordCount: words(sentence).length }));
94
+ }
95
+
96
+ function clipped(value, length = 180) {
97
+ return value.length <= length ? value : `${value.slice(0, length - 1).trimEnd()}…`;
98
+ }
99
+
100
+ function proseParagraphs(prose) {
101
+ return prose
102
+ .split(/\n\s*\n/)
103
+ .flatMap((block) => {
104
+ const paragraphs = [];
105
+ let current = "";
106
+ for (const line of block.split("\n")) {
107
+ const startsListItem = /^\s*(?:[-*+] |\d+[.)] )/.test(line);
108
+ if (startsListItem && current.trim()) {
109
+ paragraphs.push(current.trim());
110
+ current = "";
111
+ }
112
+ current += `${line}\n`;
113
+ }
114
+ if (current.trim()) paragraphs.push(current.trim());
115
+ return paragraphs;
116
+ })
117
+ .filter(Boolean);
118
+ }
119
+
120
+ function steTermExamples(prose, terms) {
121
+ return Object.entries(terms)
122
+ .filter(([term]) => new RegExp(`\\b${term.replaceAll(" ", "\\s+")}\\b`, "i").test(prose))
123
+ .map(([term, replacement]) => `${term} → ${replacement}`);
124
+ }
125
+
126
+ function steFindings(prose, sentences, documentType) {
127
+ const findings = [];
128
+ const sentenceLimit = documentType === "descriptive" ? 25 : 20;
129
+ const longSentences = sentences.filter((item) => item.wordCount > sentenceLimit).slice(0, 6);
130
+ if (longSentences.length) findings.push({
131
+ rule: "STE-S1",
132
+ kind: "ste-sentence-length",
133
+ message: `Keep ${documentType} sentences to about ${sentenceLimit} words, then split longer sentences.`,
134
+ examples: longSentences.map((item) => `${item.wordCount} words: ${clipped(item.sentence)}`),
135
+ });
136
+ const wordExamples = steTermExamples(prose, STE_WORD_GUIDANCE);
137
+ if (wordExamples.length) findings.push({
138
+ rule: "STE-W1",
139
+ kind: "ste-word-choice",
140
+ message: "Prefer a direct, familiar word. Confirm the replacement against your licensed ASD-STE100 dictionary or project terminology list.",
141
+ examples: wordExamples,
142
+ });
143
+ const phrasalExamples = steTermExamples(prose, STE_PHRASAL_GUIDANCE);
144
+ if (phrasalExamples.length) findings.push({
145
+ rule: "STE-V1",
146
+ kind: "ste-phrasal-verb",
147
+ message: "Replace an ambiguous phrasal verb with one precise verb when the meaning permits.",
148
+ examples: phrasalExamples,
149
+ });
150
+ const modalExamples = steTermExamples(prose, STE_MODAL_GUIDANCE);
151
+ if (modalExamples.length) findings.push({
152
+ rule: "STE-M1",
153
+ kind: "ste-modal-meaning",
154
+ message: "State requirement, ability, permission, or condition precisely; do not leave the modal meaning implicit.",
155
+ examples: modalExamples,
156
+ });
157
+ if (/\band\/or\b/i.test(prose)) findings.push({
158
+ rule: "STE-C1",
159
+ kind: "ste-conjunction",
160
+ message: "Replace “and/or” with the exact allowed combination.",
161
+ examples: ["and/or"],
162
+ });
163
+ const vagueTime = [...prose.matchAll(/\b(?:as soon as possible|at this point in time|from time to time)\b/gi)]
164
+ .slice(0, 6)
165
+ .map((match) => match[0]);
166
+ if (vagueTime.length) findings.push({
167
+ rule: "STE-A1",
168
+ kind: "ste-ambiguity",
169
+ message: "Use a measurable time or condition instead of a vague time phrase.",
170
+ examples: vagueTime,
171
+ });
172
+ return findings;
173
+ }
174
+
175
+ function principle(status, finding) {
176
+ return { status, finding };
177
+ }
178
+
179
+ function sha256(value) {
180
+ return createHash("sha256").update(value, "utf8").digest("hex");
181
+ }
182
+
183
+ function diffPreview(before, after) {
184
+ const oldLines = before.replaceAll("\r\n", "\n").split("\n");
185
+ const newLines = after.replaceAll("\r\n", "\n").split("\n");
186
+ let prefix = 0;
187
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) prefix += 1;
188
+ let suffix = 0;
189
+ while (
190
+ suffix < oldLines.length - prefix
191
+ && suffix < newLines.length - prefix
192
+ && oldLines[oldLines.length - suffix - 1] === newLines[newLines.length - suffix - 1]
193
+ ) suffix += 1;
194
+ const removed = oldLines.slice(prefix, oldLines.length - suffix).map((line) => `- ${line}`);
195
+ const added = newLines.slice(prefix, newLines.length - suffix).map((line) => `+ ${line}`);
196
+ const header = `@@ lines ${prefix + 1}-${oldLines.length - suffix} -> ${prefix + 1}-${newLines.length - suffix} @@`;
197
+ return clipped([header, ...removed, ...added].join("\n"), MAX_DIFF_PREVIEW_BYTES);
198
+ }
199
+
200
+ function blockedReport(reason, extra = {}) {
201
+ return {
202
+ schema: AIDR_SCHEMA,
203
+ ok: false,
204
+ status: "blocked",
205
+ reason,
206
+ nonAuthorizing: true,
207
+ authorityCreated: false,
208
+ ...extra,
209
+ };
210
+ }
211
+
212
+ export function extractAssistantText(message) {
213
+ if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return "";
214
+ return message.content
215
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
216
+ .map((part) => part.text)
217
+ .join("\n")
218
+ .trim();
219
+ }
220
+
221
+ export function reviewText(input, {
222
+ mode = "review",
223
+ source = "text",
224
+ standard = "none",
225
+ documentType = "procedural",
226
+ } = {}) {
227
+ const text = boundedText(input);
228
+ if (!["review", "simple", "analogy", "ste"].includes(mode)) {
229
+ throw new Error("AI;DR mode must be review, simple, analogy, or ste");
230
+ }
231
+ if (!["none", "asd-ste100"].includes(standard)) throw new Error("AI;DR standard must be none or asd-ste100");
232
+ if (!STE_DOCUMENT_TYPES.has(documentType)) throw new Error("AI;DR documentType must be procedural or descriptive");
233
+ const useSte = standard === "asd-ste100" || mode === "simple" || mode === "ste";
234
+ const effectiveStandard = useSte ? "asd-ste100" : "none";
235
+ const prose = visibleProse(text);
236
+ const sentences = sentenceRecords(prose);
237
+ const paragraphs = proseParagraphs(prose);
238
+ const longSentences = sentences.filter((item) => item.wordCount > 28).slice(0, 6);
239
+ const longParagraphs = paragraphs
240
+ .map((paragraph) => ({ paragraph, wordCount: words(paragraph).length }))
241
+ .filter((item) => item.wordCount > 120)
242
+ .slice(0, 4);
243
+ const clutter = CLUTTER_PHRASES.filter((phrase) => new RegExp(`\\b${phrase}\\b`, "i").test(prose));
244
+ const passive = [...prose.matchAll(PASSIVE_PATTERN)].slice(0, 6).map((match) => match[0]);
245
+ const hasLists = /(?:^|\n)\s*(?:[-*+] |\d+[.)] )/m.test(prose);
246
+ const hasStepLanguage = /\b(?:first|second|third|steps?|options?|choices?)\b/i.test(prose);
247
+ const bulletOpportunity = !hasLists && hasStepLanguage && sentences.length >= 3;
248
+ const steReviewFindings = useSte ? steFindings(prose, sentences, documentType) : [];
249
+ const findings = [];
250
+ if (longSentences.length) findings.push({
251
+ kind: "long-sentence",
252
+ message: "Break long sentences into one idea at a time.",
253
+ examples: longSentences.map((item) => `${item.wordCount} words: ${clipped(item.sentence)}`),
254
+ });
255
+ if (longParagraphs.length) findings.push({
256
+ kind: "long-paragraph",
257
+ message: "Split dense paragraphs so the reader can find the next point.",
258
+ examples: longParagraphs.map((item) => `${item.wordCount} words: ${clipped(item.paragraph, 140)}`),
259
+ });
260
+ if (clutter.length) findings.push({
261
+ kind: "clutter",
262
+ message: "Replace padded phrases with direct words.",
263
+ examples: clutter,
264
+ });
265
+ if (passive.length) findings.push({
266
+ kind: "passive-voice",
267
+ message: "Check whether an active subject would make the sentence clearer.",
268
+ examples: passive,
269
+ });
270
+ if (bulletOpportunity) findings.push({
271
+ kind: "bullet-opportunity",
272
+ message: "Turn the steps or choices into bullets to reduce working-memory load.",
273
+ examples: [],
274
+ });
275
+ findings.push(...steReviewFindings);
276
+
277
+ const hasIssues = findings.length > 0;
278
+ const rewriteInstructions = mode === "simple" || mode === "ste"
279
+ ? [
280
+ "Start with the answer in one sentence.",
281
+ "Use plain words and short sentences.",
282
+ "Put steps, choices, and comparisons in bullets.",
283
+ "Keep technical terms, but explain each one at first use.",
284
+ "Use ASD-STE100-style controlled English as an advisory guide: choose one precise term, state one action per sentence, and make requirements, permissions, and conditions explicit.",
285
+ ]
286
+ : mode === "analogy"
287
+ ? [
288
+ "Start with the plain technical answer.",
289
+ "Use one familiar analogy, not a chain of metaphors.",
290
+ "Map each important part of the analogy back to the technical idea.",
291
+ "State where the analogy stops being exact.",
292
+ ]
293
+ : [];
294
+
295
+ return {
296
+ schema: AIDR_SCHEMA,
297
+ ok: true,
298
+ status: hasIssues ? "needs_revision" : "clear",
299
+ source,
300
+ mode,
301
+ standard: effectiveStandard,
302
+ documentType,
303
+ principles: {
304
+ clarity: principle(longSentences.length || longParagraphs.length || steReviewFindings.some((item) => item.rule === "STE-S1") ? "watch" : "clear",
305
+ "Every sentence should contain the cleanest useful idea."),
306
+ simplicity: principle(clutter.length || steReviewFindings.some((item) => ["STE-W1", "STE-V1"].includes(item.rule)) ? "watch" : "clear",
307
+ "Remove clutter and explain necessary technical terms."),
308
+ brevity: principle(longSentences.length || longParagraphs.length || steReviewFindings.some((item) => item.rule === "STE-S1") ? "watch" : "clear",
309
+ "If the same meaning fits in fewer words, use fewer words."),
310
+ humanity: principle("manual",
311
+ "Preserve an authentic voice; this cannot be measured reliably by a lint rule."),
312
+ },
313
+ metrics: {
314
+ wordCount: words(prose).length,
315
+ sentenceCount: sentences.length,
316
+ paragraphCount: paragraphs.length,
317
+ longSentenceCount: longSentences.length,
318
+ longParagraphCount: longParagraphs.length,
319
+ clutterPhraseCount: clutter.length,
320
+ passivePhraseCount: passive.length,
321
+ bulletOpportunity,
322
+ steFindingCount: steReviewFindings.length,
323
+ },
324
+ standards: useSte ? {
325
+ "ASD-STE100": {
326
+ status: steReviewFindings.length ? "watch" : "advisory_clear",
327
+ profile: "ASD-STE100-informed",
328
+ rulesApplied: [...new Set(steReviewFindings.map((item) => item.rule))],
329
+ findings: steReviewFindings,
330
+ limitation: "This bounded advisory profile does not reproduce the licensed ASD-STE100 dictionary and does not certify conformance.",
331
+ },
332
+ } : undefined,
333
+ findings,
334
+ rewriteInstructions,
335
+ analogyExample: mode === "analogy"
336
+ ? "For FIFO: imagine a bag of bread. The first slice in is the first slice out; the bag tightens around the remaining slices, but the slices are not reordered."
337
+ : undefined,
338
+ editPolicy: "Review is read-only. File rewrites require an explicit apply action, an exact replacement, native confirmation, and drift revalidation.",
339
+ nonAuthorizing: true,
340
+ authorityCreated: false,
341
+ };
342
+ }
343
+
344
+ function resolveReviewFile(requestedPath, cwd) {
345
+ if (typeof requestedPath !== "string" || !requestedPath.trim()) throw new Error("a file path is required");
346
+ const candidate = resolve(cwd, requestedPath);
347
+ const root = realpathSync(cwd);
348
+ const actual = realpathSync(candidate);
349
+ const relativePath = relative(root, actual);
350
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error("AI;DR only reviews files inside the current project");
351
+ if (!ALLOWED_FILE_EXTENSIONS.has(extname(actual).toLowerCase())) throw new Error("AI;DR only reviews Markdown, text, and HTML prose files");
352
+ return actual;
353
+ }
354
+
355
+ async function applyConfirmedFileReplacement(path, before, replacement, context) {
356
+ boundedText(before, "file");
357
+ boundedText(replacement, "replacement");
358
+ if (before === replacement) return blockedReport("the proposed replacement is identical to the current file", { action: "apply", path });
359
+ if (typeof context?.ui?.confirm !== "function") {
360
+ return blockedReport("native confirmation is unavailable; AI;DR will not write the file", { action: "apply", path });
361
+ }
362
+ const preview = diffPreview(before, replacement);
363
+ const confirmed = await context.ui.confirm(
364
+ "AI;DR: apply confirmed rewrite",
365
+ `File: ${path}\n\n${preview}\n\nApply this exact replacement?`,
366
+ );
367
+ if (!confirmed) return blockedReport("native confirmation declined", { action: "apply", path, diffPreview: preview });
368
+ const current = fs.readFileSync(path, "utf8");
369
+ if (current !== before) return blockedReport("the file changed after review; no write was performed", { action: "apply", path, diffPreview: preview });
370
+
371
+ let temporaryPath;
372
+ try {
373
+ temporaryPath = join(dirname(path), `.aidr-${randomUUID()}.tmp`);
374
+ fs.writeFileSync(temporaryPath, replacement, { encoding: "utf8", flag: "wx" });
375
+ const revalidated = fs.readFileSync(path, "utf8");
376
+ if (revalidated !== before) return blockedReport("the file changed after review; no write was performed", { action: "apply", path, diffPreview: preview });
377
+ fs.renameSync(temporaryPath, path);
378
+ temporaryPath = undefined;
379
+
380
+ const after = fs.readFileSync(path);
381
+ const expected = Buffer.from(replacement, "utf8");
382
+ if (!after.equals(expected)) {
383
+ return blockedReport("the file did not match the confirmed replacement after writing", { action: "apply", path, diffPreview: preview });
384
+ }
385
+ return {
386
+ schema: AIDR_SCHEMA,
387
+ ok: true,
388
+ status: "applied",
389
+ action: "apply",
390
+ path,
391
+ diffPreview: preview,
392
+ beforeHash: sha256(before),
393
+ afterHash: sha256(after.toString("utf8")),
394
+ nonAuthorizing: true,
395
+ authorityCreated: false,
396
+ };
397
+ } finally {
398
+ if (temporaryPath) fs.rmSync(temporaryPath, { force: true });
399
+ }
400
+ }
401
+
402
+ export function registerAidrInterface(pi) {
403
+ if (typeof pi?.registerTool !== "function") return;
404
+ let lastAssistantText = "";
405
+ pi.on?.("message_end", async (event) => {
406
+ const text = extractAssistantText(event?.message);
407
+ if (text) lastAssistantText = text;
408
+ });
409
+ pi.registerTool({
410
+ name: "agentic_aidr",
411
+ label: "AI;DR",
412
+ description: "Review the last response, supplied prose, or a Markdown/documentation file for clarity, simplicity, brevity, humanity, neurodivergent-friendly structure, and optional ASD-STE100-informed controlled English. An explicit file apply action can write an exact proposed replacement only after native confirmation.",
413
+ parameters: {
414
+ type: "object",
415
+ additionalProperties: false,
416
+ properties: {
417
+ action: { type: "string", enum: ["review", "apply"] },
418
+ source: { type: "string", enum: ["last-response", "text", "file"] },
419
+ text: { type: "string", maxLength: MAX_TEXT_BYTES },
420
+ replacement: { type: "string", maxLength: MAX_TEXT_BYTES },
421
+ path: { type: "string", maxLength: 1024 },
422
+ mode: { type: "string", enum: ["review", "simple", "analogy", "ste"] },
423
+ standard: { type: "string", enum: ["none", "asd-ste100"] },
424
+ documentType: { type: "string", enum: ["procedural", "descriptive"] },
425
+ },
426
+ required: ["source"],
427
+ },
428
+ async execute(_id, params, _signal, _update, context) {
429
+ try {
430
+ const action = params.action ?? "review";
431
+ if (!["review", "apply"].includes(action)) throw new Error("AI;DR action must be review or apply");
432
+ let text;
433
+ let source = params.source;
434
+ let path;
435
+ if (!["last-response", "text", "file"].includes(source)) throw new Error("AI;DR source must be last-response, text, or file");
436
+ if (action === "apply" && source !== "file") throw new Error("AI;DR apply is available only for files");
437
+ if (source === "last-response") text = lastAssistantText;
438
+ else if (source === "text") text = params.text;
439
+ else {
440
+ path = resolveReviewFile(params.path, context.cwd);
441
+ text = fs.readFileSync(path, "utf8");
442
+ source = path;
443
+ }
444
+ if (!text) throw new Error("there is no text available for AI;DR to review");
445
+ if (action === "apply") {
446
+ const result = await applyConfirmedFileReplacement(path, text, params.replacement, context);
447
+ if (result.ok) result.review = reviewText(params.replacement, {
448
+ mode: params.mode ?? "simple",
449
+ standard: params.standard ?? "none",
450
+ documentType: params.documentType ?? "procedural",
451
+ source: path,
452
+ });
453
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
454
+ }
455
+ const report = reviewText(text, {
456
+ mode: params.mode ?? "review",
457
+ standard: params.standard ?? "none",
458
+ documentType: params.documentType ?? "procedural",
459
+ source,
460
+ });
461
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }], details: report };
462
+ } catch (error) {
463
+ const report = blockedReport(error?.message ?? String(error));
464
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }], details: { ok: false } };
465
+ }
466
+ },
467
+ });
468
+ }