Skip to content

Commit b2bc567

Browse files
authored
feat(simplify): track file hashes to prevent repeated simplification proposals (#32)
* feat(simplify): track file hashes to prevent repeated simplification proposals * docs: update changelog
1 parent 3f2e332 commit b2bc567

2 files changed

Lines changed: 109 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ All notable changes to agent-stuff are documented here.
6060

6161

6262

63-
## docs/auto-generated-file-filtering
63+
64+
65+
## feat/persist-simplification-hashes
66+
67+
The simplify extension now persists content hashes of simplified files to prevent repeated simplification proposals (#32). After each simplification run, file hashes are stored in `$PI_CODING_AGENT_DIR/simplify-hashes.json` and checked before proposing future simplifications, ensuring proposals only appear when file content actually changes. This eliminates redundant suggestions that occurred when the simplification tool itself modified files, improving the user experience by reducing unnecessary prompts while maintaining the ability to re-simplify genuinely modified code.
68+
69+
## [1.0.20](https://github.com/kostyay/agent-stuff/pull/31) - 2026-03-07
6470

6571
Updated changelog writing rules and commit guidance to exclude auto-generated files from summaries and descriptions (#31). The changelog writer, commit message guidelines, and PR update skill now consistently ignore lock files (package-lock.json, yarn.lock, pnpm-lock.yaml, go.sum, Cargo.lock), generated code (*.pb.go, *_generated.*, *.gen.*), and build artifacts (dist/, *.min.js, *.min.css) to reduce noise and keep focus on meaningful hand-written changes. This ensures that changelog entries and commit descriptions accurately reflect actual development work rather than dependency or build system artifacts.
6672

pi-extensions/simplify.ts

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
*
1111
* Hooks into `agent_end` to propose running `/simplify` when source files
1212
* were modified during the agent's turn. Shows a timed confirmation that
13-
* auto-accepts after 5 seconds.
13+
* auto-accepts after 5 seconds. Files are only proposed once — after
14+
* simplification their content hashes are persisted and checked before
15+
* any subsequent proposal.
1416
*
1517
* To add a new language:
1618
* 1. Create `skills/<lang>-code-simplifier/SKILL.md`
1719
* 2. Add one entry to FILE_EXTENSIONS below
1820
*/
1921

2022
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
21-
import { existsSync, readFileSync, readdirSync } from "node:fs";
23+
import { createHash } from "node:crypto";
24+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2225
import { dirname, extname, join, resolve } from "node:path";
2326
import { fileURLToPath } from "node:url";
2427

@@ -142,6 +145,16 @@ function filterByLanguage(files: string[], lang: string): string[] {
142145
return files.filter((f) => FILE_EXTENSIONS[fileExtension(f)] === lang);
143146
}
144147

148+
/** SHA-256 hash of a file's content. Returns null if the file can't be read. */
149+
function hashFile(filePath: string): string | null {
150+
try {
151+
const content = readFileSync(filePath);
152+
return createHash("sha256").update(content).digest("hex");
153+
} catch {
154+
return null;
155+
}
156+
}
157+
145158
// ---------------------------------------------------------------------------
146159
// Settings
147160
// ---------------------------------------------------------------------------
@@ -180,6 +193,56 @@ function readSettings(): SimplifySettings {
180193
}
181194
}
182195

196+
// ---------------------------------------------------------------------------
197+
// Persisted simplification hashes
198+
// ---------------------------------------------------------------------------
199+
200+
/** Keyed by cwd so different projects don't collide. */
201+
type HashStore = Record<string, Record<string, string>>;
202+
203+
/** Resolve the path to the hash store file. Returns null when unconfigured. */
204+
function hashStorePath(): string | null {
205+
const configDir = process.env.PI_CODING_AGENT_DIR;
206+
if (!configDir) return null;
207+
return join(configDir, "simplify-hashes.json");
208+
}
209+
210+
/** Load persisted hashes for the current working directory. */
211+
function loadHashes(): Map<string, string> {
212+
const path = hashStorePath();
213+
if (!path || !existsSync(path)) return new Map();
214+
215+
try {
216+
const store = JSON.parse(readFileSync(path, "utf-8")) as HashStore;
217+
const project = store[process.cwd()];
218+
if (!project || typeof project !== "object") return new Map();
219+
return new Map(Object.entries(project));
220+
} catch {
221+
return new Map();
222+
}
223+
}
224+
225+
/** Persist hashes for the current working directory. */
226+
function saveHashes(hashes: Map<string, string>): void {
227+
const path = hashStorePath();
228+
if (!path) return;
229+
230+
let store: HashStore = {};
231+
try {
232+
if (existsSync(path)) {
233+
store = JSON.parse(readFileSync(path, "utf-8")) as HashStore;
234+
}
235+
} catch {
236+
store = {};
237+
}
238+
239+
store[process.cwd()] = Object.fromEntries(hashes);
240+
241+
const dir = dirname(path);
242+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
243+
writeFileSync(path, JSON.stringify(store, null, "\t"), "utf-8");
244+
}
245+
183246
// ---------------------------------------------------------------------------
184247
// Git helpers
185248
// ---------------------------------------------------------------------------
@@ -355,7 +418,7 @@ async function triggerSimplify(
355418
const relevantFiles = filterByLanguage(files, lang);
356419
notify(`Simplifying ${relevantFiles.length} ${lang.toUpperCase()} file(s)…`, "info");
357420

358-
simplifyPending = true;
421+
pendingSimplifyFiles = relevantFiles;
359422
pi.sendUserMessage(buildPrompt(skillContent, relevantFiles, options.extraInstructions));
360423
return true;
361424
}
@@ -404,17 +467,22 @@ function wasAborted(event: unknown): boolean {
404467
let statsAtStart: Map<string, FileStats> = new Map();
405468

406469
/**
407-
* Whether simplify has already been proposed since the last user-initiated
408-
* prompt. Prevents re-proposing after the simplification run itself
409-
* modifies files. Only reset when the user types a new prompt.
470+
* Content hashes of files after their last simplification pass.
471+
*
472+
* Loaded from `$PI_CODING_AGENT_DIR/simplify-hashes.json` on startup,
473+
* updated and persisted after each simplification run. Keyed by relative
474+
* file path so proposals are suppressed until the file actually changes.
410475
*/
411-
let hasProposedSimplify = false;
476+
const simplifiedHashes: Map<string, string> = loadHashes();
412477

413478
/**
414-
* Set by `triggerSimplify` before `sendUserMessage` so `before_agent_start`
415-
* can distinguish programmatic turns from user-initiated ones.
479+
* Files being simplified in the current agent turn.
480+
*
481+
* Set by `triggerSimplify` before `sendUserMessage`. When `agent_end`
482+
* sees this is non-null, it hashes the files, persists them, and skips
483+
* the proposal.
416484
*/
417-
let simplifyPending = false;
485+
let pendingSimplifyFiles: string[] | null = null;
418486

419487
/**
420488
* Files queued by the `agent_end` auto-simplify confirmation.
@@ -433,10 +501,6 @@ export default function simplifyExtension(pi: ExtensionAPI) {
433501
// ── Snapshot dirty files at turn start ──────────────────────────
434502

435503
pi.on("before_agent_start", async () => {
436-
if (!simplifyPending) {
437-
hasProposedSimplify = false;
438-
}
439-
simplifyPending = false;
440504
statsAtStart = await snapshotFileStats(pi);
441505
});
442506

@@ -472,7 +536,18 @@ export default function simplifyExtension(pi: ExtensionAPI) {
472536
// ── Auto-simplify proposal after agent turn ─────────────────────
473537

474538
pi.on("agent_end", async (event: unknown, ctx: ExtensionContext) => {
475-
if (!ctx.hasUI || hasProposedSimplify) return;
539+
// After a simplify turn, hash the output files and persist.
540+
if (pendingSimplifyFiles) {
541+
for (const file of pendingSimplifyFiles) {
542+
const hash = hashFile(file);
543+
if (hash) simplifiedHashes.set(file, hash);
544+
}
545+
pendingSimplifyFiles = null;
546+
saveHashes(simplifiedHashes);
547+
return;
548+
}
549+
550+
if (!ctx.hasUI) return;
476551
if (wasAborted(event)) return;
477552

478553
const statsAtEnd = await snapshotFileStats(pi);
@@ -488,27 +563,34 @@ export default function simplifyExtension(pi: ExtensionAPI) {
488563
const relevantFiles = filterByLanguage(sourceFiles, lang);
489564
if (relevantFiles.length === 0) return;
490565

566+
// Skip files whose content hasn't changed since last simplification.
567+
const unsimplified = relevantFiles.filter((f) => {
568+
const currentHash = hashFile(f);
569+
if (!currentHash) return false;
570+
const storedHash = simplifiedHashes.get(f);
571+
return !storedHash || currentHash !== storedHash;
572+
});
573+
if (unsimplified.length === 0) return;
574+
491575
const { minChangedLines } = readSettings();
492576
if (minChangedLines > 0) {
493577
let totalDelta = 0;
494-
for (const file of relevantFiles) {
578+
for (const file of unsimplified) {
495579
totalDelta += modified.get(file) ?? 0;
496580
}
497581
if (totalDelta < minChangedLines) return;
498582
}
499583

500-
hasProposedSimplify = true;
501-
502584
const confirmed = await timedConfirm(ctx, {
503585
title: "Simplify code",
504-
message: buildConfirmMessage(relevantFiles, lang),
586+
message: buildConfirmMessage(unsimplified, lang),
505587
seconds: 5,
506588
defaultValue: true,
507589
});
508590

509591
if (!confirmed) return;
510592

511-
pendingAutoSimplifyFiles = sourceFiles;
593+
pendingAutoSimplifyFiles = unsimplified;
512594
pi.sendUserMessage("/simplify");
513595
});
514596
}

0 commit comments

Comments
 (0)